From f6e9195503d551f2f17e101551b771f81396cc56 Mon Sep 17 00:00:00 2001 From: Chip Senkbeil Date: Sun, 15 Aug 2021 16:18:31 -0500 Subject: [PATCH] Update error response to include kind, refactor kind to be from a defined set, support new exists request/response --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 +- src/cli/subcommand/action/inner.rs | 13 ++- src/cli/subcommand/listen/handler.rs | 83 ++++++++------ src/core/data.rs | 163 +++++++++++++++++++++++++-- src/main.rs | 14 +++ 7 files changed, 229 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6bace43..a89b819 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,7 +179,7 @@ dependencies = [ [[package]] name = "distant" -version = "0.10.1" +version = "0.11.0" dependencies = [ "bytes", "derive_more", diff --git a/Cargo.toml b/Cargo.toml index 6c4bc1b..295144e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "distant" description = "Operate on a remote computer through file and process manipulation" categories = ["command-line-utilities"] -version = "0.10.1" +version = "0.11.0" authors = ["Chip Senkbeil "] edition = "2018" homepage = "https://github.com/chipsenkbeil/distant" diff --git a/README.md b/README.md index 63a9c1a..59e220a 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ distant launch my.example.com # After the session is established, you can perform different operations # on the remote machine via `distant action {command} [args]` distant action copy path/to/file new/path/to/file -distant action proc-run -- echo 'Hello, this is from the other side' +distant action run -- echo 'Hello, this is from the other side' ``` ## License diff --git a/src/cli/subcommand/action/inner.rs b/src/cli/subcommand/action/inner.rs index e0ff7a7..a41b664 100644 --- a/src/cli/subcommand/action/inner.rs +++ b/src/cli/subcommand/action/inner.rs @@ -2,7 +2,7 @@ use crate::{ cli::opt::Mode, core::{ constants::MAX_PIPE_CHUNK_SIZE, - data::{Request, RequestData, Response, ResponseData}, + data::{Error, Request, RequestData, Response, ResponseData}, net::{Client, DataStream}, utils::StringBuf, }, @@ -289,8 +289,8 @@ pub fn format_response(mode: Mode, res: Response) -> io::Result { fn format_shell(data: ResponseData) -> ResponseOut { match data { ResponseData::Ok => ResponseOut::None, - ResponseData::Error { description } => { - ResponseOut::StderrLine(format!("Failed: '{}'.", description)) + ResponseData::Error(Error { kind, description }) => { + ResponseOut::StderrLine(format!("Failed ({}): '{}'.", kind, description)) } ResponseData::Blob { data } => { ResponseOut::StdoutLine(String::from_utf8_lossy(&data).to_string()) @@ -320,6 +320,13 @@ fn format_shell(data: ResponseData) -> ResponseOut { .collect::>() .join("\n"), )), + ResponseData::Exists(exists) => { + if exists { + ResponseOut::StdoutLine("Does exist.".to_string()) + } else { + ResponseOut::StdoutLine("Does not exist.".to_string()) + } + } ResponseData::Metadata { canonicalized_path, file_type, diff --git a/src/cli/subcommand/listen/handler.rs b/src/cli/subcommand/listen/handler.rs index 15afadc..4a51725 100644 --- a/src/cli/subcommand/listen/handler.rs +++ b/src/cli/subcommand/listen/handler.rs @@ -5,11 +5,11 @@ use crate::core::{ }, state::{Process, ServerState}, }; +use derive_more::{Display, Error, From}; use futures::future; use log::*; use std::{ env, - error::Error, net::SocketAddr, path::{Path, PathBuf}, process::Stdio, @@ -26,6 +26,21 @@ use walkdir::WalkDir; pub type Reply = mpsc::Sender; type HState = Arc>>; +#[derive(Debug, Display, Error, From)] +pub enum ServerError { + IoError(io::Error), + WalkDirError(walkdir::Error), +} + +impl From for ResponseData { + fn from(x: ServerError) -> Self { + match x { + ServerError::IoError(x) => Self::from(x), + ServerError::WalkDirError(x) => Self::from(x), + } + } +} + /// Processes the provided request, sending replies using the given sender pub(super) async fn process( addr: SocketAddr, @@ -39,7 +54,7 @@ pub(super) async fn process( state: HState, data: RequestData, tx: Reply, - ) -> Result> { + ) -> Result { match data { RequestData::FileRead { path } => file_read(path).await, RequestData::FileReadText { path } => file_read_text(path).await, @@ -58,6 +73,7 @@ pub(super) async fn process( RequestData::Remove { path, force } => remove(path, force).await, RequestData::Copy { src, dst } => copy(src, dst).await, RequestData::Rename { src, dst } => rename(src, dst).await, + RequestData::Exists { path } => exists(path).await, RequestData::Metadata { path, canonicalize } => metadata(path, canonicalize).await, RequestData::ProcRun { cmd, args } => { proc_run(tenant.to_string(), addr, state, tx, cmd, args).await @@ -80,9 +96,7 @@ pub(super) async fn process( payload_tasks.push(tokio::spawn(async move { match inner(tenant_2, addr, state_2, data, tx_2).await { Ok(data) => data, - Err(x) => ResponseData::Error { - description: x.to_string(), - }, + Err(x) => ResponseData::from(x), } })); } @@ -93,9 +107,7 @@ pub(super) async fn process( .into_iter() .map(|x| match x { Ok(x) => x, - Err(x) => ResponseData::Error { - description: x.to_string(), - }, + Err(x) => ResponseData::from(x), }) .collect(); @@ -112,27 +124,24 @@ pub(super) async fn process( tx.send(res).await } -async fn file_read(path: PathBuf) -> Result> { +async fn file_read(path: PathBuf) -> Result { Ok(ResponseData::Blob { data: tokio::fs::read(path).await?, }) } -async fn file_read_text(path: PathBuf) -> Result> { +async fn file_read_text(path: PathBuf) -> Result { Ok(ResponseData::Text { data: tokio::fs::read_to_string(path).await?, }) } -async fn file_write(path: PathBuf, data: impl AsRef<[u8]>) -> Result> { +async fn file_write(path: PathBuf, data: impl AsRef<[u8]>) -> Result { tokio::fs::write(path, data).await?; Ok(ResponseData::Ok) } -async fn file_append( - path: PathBuf, - data: impl AsRef<[u8]>, -) -> Result> { +async fn file_append(path: PathBuf, data: impl AsRef<[u8]>) -> Result { let mut file = tokio::fs::OpenOptions::new() .append(true) .open(path) @@ -147,7 +156,7 @@ async fn dir_read( absolute: bool, canonicalize: bool, include_root: bool, -) -> Result> { +) -> Result { // Canonicalize our provided path to ensure that it is exists, not a loop, and absolute let root_path = tokio::fs::canonicalize(path).await?; @@ -172,7 +181,7 @@ async fn dir_read( } else if ft.is_file() { FileType::File } else { - FileType::SymLink + FileType::Symlink } } @@ -229,7 +238,7 @@ async fn dir_read( Ok(ResponseData::DirEntries { entries, errors }) } -async fn dir_create(path: PathBuf, all: bool) -> Result> { +async fn dir_create(path: PathBuf, all: bool) -> Result { if all { tokio::fs::create_dir_all(path).await?; } else { @@ -239,7 +248,7 @@ async fn dir_create(path: PathBuf, all: bool) -> Result Result> { +async fn remove(path: PathBuf, force: bool) -> Result { let path_metadata = tokio::fs::metadata(path.as_path()).await?; if path_metadata.is_dir() { if force { @@ -254,7 +263,7 @@ async fn remove(path: PathBuf, force: bool) -> Result Result> { +async fn copy(src: PathBuf, dst: PathBuf) -> Result { let src_metadata = tokio::fs::metadata(src.as_path()).await?; if src_metadata.is_dir() { for entry in WalkDir::new(src.as_path()) @@ -294,13 +303,23 @@ async fn copy(src: PathBuf, dst: PathBuf) -> Result Ok(ResponseData::Ok) } -async fn rename(src: PathBuf, dst: PathBuf) -> Result> { +async fn rename(src: PathBuf, dst: PathBuf) -> Result { tokio::fs::rename(src, dst).await?; Ok(ResponseData::Ok) } -async fn metadata(path: PathBuf, canonicalize: bool) -> Result> { +async fn exists(path: PathBuf) -> Result { + // Following experimental `std::fs::try_exists`, which checks the error kind of the + // metadata lookup to see if it is not found and filters accordingly + Ok(match tokio::fs::metadata(path.as_path()).await { + Ok(_) => ResponseData::Exists(true), + Err(x) if x.kind() == io::ErrorKind::NotFound => ResponseData::Exists(false), + Err(x) => return Err(ServerError::from(x)), + }) +} + +async fn metadata(path: PathBuf, canonicalize: bool) -> Result { let metadata = tokio::fs::metadata(path.as_path()).await?; let canonicalized_path = if canonicalize { Some(tokio::fs::canonicalize(path).await?) @@ -332,7 +351,7 @@ async fn metadata(path: PathBuf, canonicalize: bool) -> Result, -) -> Result> { +) -> Result { let id = rand::random(); let mut child = Command::new(cmd.to_string()) @@ -472,9 +491,7 @@ async fn proc_run( } } Err(x) => { - let res = Response::new(tenant.as_str(), None, vec![ResponseData::Error { - description: x.to_string() - }]); + let res = Response::new(tenant.as_str(), None, vec![ResponseData::from(x)]); debug!( " Sending response of type{} {}", addr, @@ -534,7 +551,7 @@ async fn proc_run( Ok(ResponseData::ProcStart { id }) } -async fn proc_kill(state: HState, id: usize) -> Result> { +async fn proc_kill(state: HState, id: usize) -> Result { if let Some(process) = state.lock().await.processes.remove(&id) { process.kill_tx.send(()).map_err(|_| { io::Error::new( @@ -547,11 +564,7 @@ async fn proc_kill(state: HState, id: usize) -> Result Result> { +async fn proc_stdin(state: HState, id: usize, data: String) -> Result { if let Some(process) = state.lock().await.processes.get(&id) { process.stdin_tx.send(data).await.map_err(|_| { io::Error::new(io::ErrorKind::BrokenPipe, "Unable to send stdin to process") @@ -561,7 +574,7 @@ async fn proc_stdin( Ok(ResponseData::Ok) } -async fn proc_list(state: HState) -> Result> { +async fn proc_list(state: HState) -> Result { Ok(ResponseData::ProcEntries { entries: state .lock() @@ -577,7 +590,7 @@ async fn proc_list(state: HState) -> Result> { }) } -async fn system_info() -> Result> { +async fn system_info() -> Result { Ok(ResponseData::SystemInfo { family: env::consts::FAMILY.to_string(), os: env::consts::OS.to_string(), diff --git a/src/core/data.rs b/src/core/data.rs index e92b6ca..a622ff2 100644 --- a/src/core/data.rs +++ b/src/core/data.rs @@ -1,4 +1,4 @@ -use derive_more::IsVariant; +use derive_more::{Display, IsVariant}; use serde::{Deserialize, Serialize}; use std::{io, path::PathBuf}; use structopt::StructOpt; @@ -179,6 +179,12 @@ pub enum RequestData { dst: PathBuf, }, + /// Checks whether the given path exists + Exists { + /// The path to the file or directory on the remote machine + path: PathBuf, + }, + /// Retrieves filesystem metadata for the specified path on the remote machine Metadata { /// The path to the file, directory, or symlink on the remote machine @@ -284,10 +290,7 @@ pub enum ResponseData { Ok, /// General-purpose failure that occurred from some request - Error { - /// Details about the error - description: String, - }, + Error(Error), /// Response containing some arbitrary, binary data Blob { @@ -310,6 +313,9 @@ pub enum ResponseData { errors: Vec, }, + /// Response to checking if a path exists + Exists(bool), + /// Represents metadata about some filesystem object (file, directory, symlink) on remote machine Metadata { /// Canonicalized path to the file or directory, resolving symlinks, only included @@ -425,7 +431,7 @@ pub struct DirEntry { pub enum FileType { Dir, File, - SymLink, + Symlink, } /// Represents information about a running process @@ -444,12 +450,30 @@ pub struct RunningProcess { pub id: usize, } +impl From for ResponseData { + fn from(x: io::Error) -> Self { + Self::Error(Error::from(x)) + } +} + +impl From for ResponseData { + fn from(x: walkdir::Error) -> Self { + Self::Error(Error::from(x)) + } +} + +impl From for ResponseData { + fn from(x: tokio::task::JoinError) -> Self { + Self::Error(Error::from(x)) + } +} + /// General purpose error type that can be sent across the wire #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case", deny_unknown_fields)] pub struct Error { /// Label describing the kind of error - pub kind: String, + pub kind: ErrorKind, /// Description of the error itself pub description: String, @@ -458,7 +482,7 @@ pub struct Error { impl From for Error { fn from(x: io::Error) -> Self { Self { - kind: format!("{:?}", x.kind()), + kind: ErrorKind::from(x.kind()), description: format!("{}", x), } } @@ -470,13 +494,134 @@ impl From for Error { x.into_io_error().map(Self::from).unwrap() } else { Self { - kind: String::from("Loop"), + kind: ErrorKind::Loop, description: format!("{}", x), } } } } +impl From for Error { + fn from(x: tokio::task::JoinError) -> Self { + Self { + kind: if x.is_cancelled() { + ErrorKind::TaskCancelled + } else if x.is_panic() { + ErrorKind::TaskPanicked + } else { + ErrorKind::Unknown + }, + description: format!("{}", x), + } + } +} + +/// All possible kinds of errors that can be returned +#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", deny_unknown_fields)] +pub enum ErrorKind { + /// An entity was not found, often a file + NotFound, + + /// The operation lacked the necessary privileges to complete + PermissionDenied, + + /// The connection was refused by the remote server + ConnectionRefused, + + /// The connection was reset by the remote server + ConnectionReset, + + /// The connection was aborted (terminated) by the remote server + ConnectionAborted, + + /// The network operation failed because it was not connected yet + NotConnected, + + /// A socket address could not be bound because the address is already in use elsewhere + AddrInUse, + + /// A nonexistent interface was requested or the requested address was not local + AddrNotAvailable, + + /// The operation failed because a pipe was closed + BrokenPipe, + + /// An entity already exists, often a file + AlreadyExists, + + /// The operation needs to block to complete, but the blocking operation was requested to not + /// occur + WouldBlock, + + /// A parameter was incorrect + InvalidInput, + + /// Data not valid for the operation were encountered + InvalidData, + + /// The I/O operation's timeout expired, causing it to be cancelled + TimedOut, + + /// An error returned when an operation could not be completed because a + /// call to `write` returned `Ok(0)` + WriteZero, + + /// This operation was interrupted + Interrupted, + + /// Any I/O error not part of this list + Other, + + /// An error returned when an operation could not be completed because an "end of file" was + /// reached prematurely + UnexpectedEof, + + /// This operation is unsupported on this platform + Unsupported, + + /// When a loop is encountered when walking a directory + Loop, + + /// When a task is cancelled + TaskCancelled, + + /// When a task panics + TaskPanicked, + + /// Catchall for an error that has no specific type + Unknown, +} + +impl From for ErrorKind { + fn from(kind: io::ErrorKind) -> Self { + match kind { + io::ErrorKind::NotFound => Self::NotFound, + io::ErrorKind::PermissionDenied => Self::PermissionDenied, + io::ErrorKind::ConnectionRefused => Self::ConnectionRefused, + io::ErrorKind::ConnectionReset => Self::ConnectionReset, + io::ErrorKind::ConnectionAborted => Self::ConnectionAborted, + io::ErrorKind::NotConnected => Self::NotConnected, + io::ErrorKind::AddrInUse => Self::AddrInUse, + io::ErrorKind::AddrNotAvailable => Self::AddrNotAvailable, + io::ErrorKind::BrokenPipe => Self::BrokenPipe, + io::ErrorKind::AlreadyExists => Self::AlreadyExists, + io::ErrorKind::WouldBlock => Self::WouldBlock, + io::ErrorKind::InvalidInput => Self::InvalidInput, + io::ErrorKind::InvalidData => Self::InvalidData, + io::ErrorKind::TimedOut => Self::TimedOut, + io::ErrorKind::WriteZero => Self::WriteZero, + io::ErrorKind::Interrupted => Self::Interrupted, + io::ErrorKind::Other => Self::Other, + io::ErrorKind::UnexpectedEof => Self::UnexpectedEof, + io::ErrorKind::Unsupported => Self::Unsupported, + + // This exists because io::ErrorKind is non_exhaustive + _ => Self::Unknown, + } + } +} + /// Used to provide a default serde value of 1 const fn one() -> usize { 1 diff --git a/src/main.rs b/src/main.rs index 0cdcf85..7b33e34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,17 @@ +//! # distant +//! +//! ### Exit codes +//! +//! * EX_USAGE (64) - being used when arguments missing or bad arguments provided to CLI +//! * EX_DATAERR (65) - being used when bad data received not in UTF-8 format or transport data is bad +//! * EX_NOINPUT (66) - being used when not getting expected data from launch +//! * EX_NOHOST (68) - being used when failed to resolve a host +//! * EX_UNAVAILABLE (69) - being used when IO error encountered where connection is problem +//! * EX_OSERR (71) - being used when fork failed +//! * EX_IOERR (74) - being used as catchall for IO errors +//! * EX_TEMPFAIL (75) - being used when we get a timeout +//! * EX_PROTOCOL (76) - being used as catchall for transport errors + fn main() { distant::run(); }