Update error response to include kind, refactor kind to be from a defined set, support new exists request/response

pull/38/head v0.11.0
Chip Senkbeil 3 years ago
parent e2fd3a9bae
commit f6e9195503
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131

2
Cargo.lock generated

@ -179,7 +179,7 @@ dependencies = [
[[package]]
name = "distant"
version = "0.10.1"
version = "0.11.0"
dependencies = [
"bytes",
"derive_more",

@ -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 <chip@senkbeil.org>"]
edition = "2018"
homepage = "https://github.com/chipsenkbeil/distant"

@ -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

@ -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<ResponseOut> {
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::<Vec<String>>()
.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,

@ -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<Response>;
type HState = Arc<Mutex<ServerState<SocketAddr>>>;
#[derive(Debug, Display, Error, From)]
pub enum ServerError {
IoError(io::Error),
WalkDirError(walkdir::Error),
}
impl From<ServerError> 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<ResponseData, Box<dyn std::error::Error>> {
) -> Result<ResponseData, ServerError> {
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<ResponseData, Box<dyn Error>> {
async fn file_read(path: PathBuf) -> Result<ResponseData, ServerError> {
Ok(ResponseData::Blob {
data: tokio::fs::read(path).await?,
})
}
async fn file_read_text(path: PathBuf) -> Result<ResponseData, Box<dyn Error>> {
async fn file_read_text(path: PathBuf) -> Result<ResponseData, ServerError> {
Ok(ResponseData::Text {
data: tokio::fs::read_to_string(path).await?,
})
}
async fn file_write(path: PathBuf, data: impl AsRef<[u8]>) -> Result<ResponseData, Box<dyn Error>> {
async fn file_write(path: PathBuf, data: impl AsRef<[u8]>) -> Result<ResponseData, ServerError> {
tokio::fs::write(path, data).await?;
Ok(ResponseData::Ok)
}
async fn file_append(
path: PathBuf,
data: impl AsRef<[u8]>,
) -> Result<ResponseData, Box<dyn Error>> {
async fn file_append(path: PathBuf, data: impl AsRef<[u8]>) -> Result<ResponseData, ServerError> {
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<ResponseData, Box<dyn Error>> {
) -> Result<ResponseData, ServerError> {
// 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<ResponseData, Box<dyn Error>> {
async fn dir_create(path: PathBuf, all: bool) -> Result<ResponseData, ServerError> {
if all {
tokio::fs::create_dir_all(path).await?;
} else {
@ -239,7 +248,7 @@ async fn dir_create(path: PathBuf, all: bool) -> Result<ResponseData, Box<dyn Er
Ok(ResponseData::Ok)
}
async fn remove(path: PathBuf, force: bool) -> Result<ResponseData, Box<dyn Error>> {
async fn remove(path: PathBuf, force: bool) -> Result<ResponseData, ServerError> {
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<ResponseData, Box<dyn Erro
Ok(ResponseData::Ok)
}
async fn copy(src: PathBuf, dst: PathBuf) -> Result<ResponseData, Box<dyn Error>> {
async fn copy(src: PathBuf, dst: PathBuf) -> Result<ResponseData, ServerError> {
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<ResponseData, Box<dyn Error>
Ok(ResponseData::Ok)
}
async fn rename(src: PathBuf, dst: PathBuf) -> Result<ResponseData, Box<dyn Error>> {
async fn rename(src: PathBuf, dst: PathBuf) -> Result<ResponseData, ServerError> {
tokio::fs::rename(src, dst).await?;
Ok(ResponseData::Ok)
}
async fn metadata(path: PathBuf, canonicalize: bool) -> Result<ResponseData, Box<dyn Error>> {
async fn exists(path: PathBuf) -> Result<ResponseData, ServerError> {
// 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<ResponseData, ServerError> {
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<ResponseData, Box
} else if metadata.is_file() {
FileType::File
} else {
FileType::SymLink
FileType::Symlink
},
})
}
@ -344,7 +363,7 @@ async fn proc_run(
tx: Reply,
cmd: String,
args: Vec<String>,
) -> Result<ResponseData, Box<dyn Error>> {
) -> Result<ResponseData, ServerError> {
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!(
"<Client @ {}> 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<ResponseData, Box<dyn Error>> {
async fn proc_kill(state: HState, id: usize) -> Result<ResponseData, ServerError> {
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<ResponseData, Box<dyn Err
Ok(ResponseData::Ok)
}
async fn proc_stdin(
state: HState,
id: usize,
data: String,
) -> Result<ResponseData, Box<dyn Error>> {
async fn proc_stdin(state: HState, id: usize, data: String) -> Result<ResponseData, ServerError> {
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<ResponseData, Box<dyn Error>> {
async fn proc_list(state: HState) -> Result<ResponseData, ServerError> {
Ok(ResponseData::ProcEntries {
entries: state
.lock()
@ -577,7 +590,7 @@ async fn proc_list(state: HState) -> Result<ResponseData, Box<dyn Error>> {
})
}
async fn system_info() -> Result<ResponseData, Box<dyn Error>> {
async fn system_info() -> Result<ResponseData, ServerError> {
Ok(ResponseData::SystemInfo {
family: env::consts::FAMILY.to_string(),
os: env::consts::OS.to_string(),

@ -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<Error>,
},
/// 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<io::Error> for ResponseData {
fn from(x: io::Error) -> Self {
Self::Error(Error::from(x))
}
}
impl From<walkdir::Error> for ResponseData {
fn from(x: walkdir::Error) -> Self {
Self::Error(Error::from(x))
}
}
impl From<tokio::task::JoinError> 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<io::Error> 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<walkdir::Error> for Error {
x.into_io_error().map(Self::from).unwrap()
} else {
Self {
kind: String::from("Loop"),
kind: ErrorKind::Loop,
description: format!("{}", x),
}
}
}
}
impl From<tokio::task::JoinError> 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<io::ErrorKind> 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

@ -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();
}

Loading…
Cancel
Save