Add shell_interface and environment and prepare main application for those changes
This commit is contained in:
parent
8fa7fdb88b
commit
85b0b3dbbf
44
src/environment.rs
Normal file
44
src/environment.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use std::env;
|
||||
use std::env::VarError;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub trait Environment {
|
||||
fn args_os(&self) -> Vec<OsString>;
|
||||
fn var<K>(&self, key: K) -> Result<String, VarError>
|
||||
where
|
||||
K: AsRef<OsStr>;
|
||||
fn set_var<K, V>(&self, key: K, value: V)
|
||||
where
|
||||
K: AsRef<OsStr>,
|
||||
V: AsRef<OsStr>;
|
||||
fn get_home_directory(&self) -> Option<PathBuf>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Prod;
|
||||
|
||||
impl Environment for Prod {
|
||||
fn args_os(&self) -> Vec<OsString> {
|
||||
env::args_os().collect()
|
||||
}
|
||||
|
||||
fn var<K>(&self, key: K) -> Result<String, VarError>
|
||||
where
|
||||
K: AsRef<OsStr>,
|
||||
{
|
||||
env::var(key)
|
||||
}
|
||||
|
||||
fn set_var<K, V>(&self, key: K, value: V)
|
||||
where
|
||||
K: AsRef<OsStr>,
|
||||
V: AsRef<OsStr>
|
||||
{
|
||||
env::set_var(key, value);
|
||||
}
|
||||
|
||||
fn get_home_directory(&self) -> Option<PathBuf> {
|
||||
homedir::my_home().ok().flatten()
|
||||
}
|
||||
}
|
||||
184
src/main.rs
184
src/main.rs
@ -1,13 +1,16 @@
|
||||
mod action;
|
||||
mod command;
|
||||
mod environment;
|
||||
mod file;
|
||||
mod logger;
|
||||
mod os_string_builder;
|
||||
mod os_str_extension;
|
||||
mod os_string_builder;
|
||||
mod server;
|
||||
mod shell_interface;
|
||||
|
||||
use crate::action::{Action, FileAction, ServerActions};
|
||||
use crate::command::{CommandSpecificError, ExecutionError, LogRunnable};
|
||||
use crate::command::LogRunnable;
|
||||
use crate::environment::{Environment, Prod};
|
||||
use crate::file::{FileMatcher, FileNameInfo};
|
||||
use crate::logger::{LogLevel, Logger};
|
||||
use crate::os_str_extension::OsStrExtension;
|
||||
@ -41,11 +44,12 @@ type ShellCmd = std::process::Command;
|
||||
/// Use `MSSH_SERVERS="crea:home/crea,sky:sky,lobby:,city:city2"` to configure servers.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about)]
|
||||
struct Args {
|
||||
pub struct Args {
|
||||
/// The action to perform
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
/// The ssh names and optionally home directories of the servers to perform the action on
|
||||
//TODO from_str always uses Prod environment -> handwrite that section
|
||||
#[arg(num_args = 0.., value_parser = ServerReference::from_str)]
|
||||
servers: Vec<ServerReference>,
|
||||
/// How verbose logging output should be
|
||||
@ -137,6 +141,8 @@ enum OldVersionPolicy {
|
||||
Delete,
|
||||
}
|
||||
|
||||
//TODO IO would also need to be handled by the environment
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! input {
|
||||
($prompt: tt) => {{
|
||||
@ -153,9 +159,21 @@ macro_rules! input {
|
||||
};
|
||||
}
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
let args = Args::parse();
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Application<E> {
|
||||
pub environment: E,
|
||||
}
|
||||
|
||||
impl<E> Application<E>
|
||||
where
|
||||
E: Environment,
|
||||
{
|
||||
pub fn run(&self) -> Result<(), String> {
|
||||
let args = Args::try_parse_from(self.environment.args_os()).map_err(|e| e.to_string())?;
|
||||
self.run_with_args(args)
|
||||
}
|
||||
|
||||
pub fn run_with_args(&self, args: Args) -> Result<(), String> {
|
||||
let logger = Logger {
|
||||
//all the below options are conflicting with each other so an if else is fine
|
||||
level: if args.quiet {
|
||||
@ -167,7 +185,7 @@ fn main() -> Result<(), String> {
|
||||
},
|
||||
};
|
||||
|
||||
let mut configured_servers = LazyCell::new(parse_server_configuration_from_env);
|
||||
let mut configured_servers = LazyCell::new(|| self.parse_server_configuration_from_env());
|
||||
let servers = args
|
||||
.servers
|
||||
.iter()
|
||||
@ -196,9 +214,9 @@ fn main() -> Result<(), String> {
|
||||
pure,
|
||||
exclude,
|
||||
} => {
|
||||
require_non_empty_servers(&servers)?;
|
||||
require_non_empty(&files, "files to upload")?;
|
||||
start_ssh_agent(&logger)?;
|
||||
Self::require_non_empty_servers(&servers)?;
|
||||
Self::require_non_empty(&files, "files to upload")?;
|
||||
self.start_ssh_agent(&logger)?;
|
||||
|
||||
//resolve file server
|
||||
let file_server = match file_server {
|
||||
@ -250,9 +268,9 @@ fn main() -> Result<(), String> {
|
||||
ServerAddress::Localhost => files
|
||||
.iter()
|
||||
.map(|file| file_server.server_directory_path.join(file))
|
||||
.try_for_each(check_local_file_exists)?,
|
||||
.try_for_each(Self::check_local_file_exists)?,
|
||||
},
|
||||
None => files.iter().try_for_each(check_local_file_exists)?,
|
||||
None => files.iter().try_for_each(Self::check_local_file_exists)?,
|
||||
}
|
||||
|
||||
let file_details = files
|
||||
@ -313,7 +331,9 @@ fn main() -> Result<(), String> {
|
||||
.collect(),
|
||||
ServerAddress::Localhost => fs::read_dir(&working_directory)
|
||||
.map_err(|e| format!("Failed to get files in working directory: {e}"))?
|
||||
.map(|entry| entry.map_err(|e| format!("Failed to access directory entry: {e}")))
|
||||
.map(|entry| {
|
||||
entry.map_err(|e| format!("Failed to access directory entry: {e}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
@ -341,7 +361,8 @@ fn main() -> Result<(), String> {
|
||||
|
||||
let file_name = file_name_info.to_full_file_name();
|
||||
|
||||
let add_action = FileAction::new(file, Action::Add).expect("path points to file");
|
||||
let add_action =
|
||||
FileAction::new(file, Action::Add).expect("path points to file");
|
||||
|
||||
if pure && present_file_names.iter().any(|file| *file == file_name) {
|
||||
log!(
|
||||
@ -359,8 +380,7 @@ fn main() -> Result<(), String> {
|
||||
if !present_file_names.iter().any(|file| *file == file_name) {
|
||||
vec![add_action] //file doesn't exist yet
|
||||
} else {
|
||||
vec![FileAction::new(file, Action::Replace)
|
||||
.expect("path points to file")]
|
||||
vec![FileAction::new(file, Action::Replace).expect("path points to file")]
|
||||
}
|
||||
}
|
||||
OldVersionPolicy::Archive => present_file_names
|
||||
@ -392,7 +412,8 @@ fn main() -> Result<(), String> {
|
||||
if *present_file == file_name {
|
||||
FileAction::new(file, Action::Replace).expect("path points to file")
|
||||
} else {
|
||||
FileAction::new(present_file, Action::Delete).expect("path points to file")
|
||||
FileAction::new(present_file, Action::Delete)
|
||||
.expect("path points to file")
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@ -501,8 +522,8 @@ fn main() -> Result<(), String> {
|
||||
log!(logger, "Done!");
|
||||
}
|
||||
Command::Command { command } => {
|
||||
start_ssh_agent(&logger)?;
|
||||
require_non_empty_servers(&servers)?;
|
||||
self.start_ssh_agent(&logger)?;
|
||||
Self::require_non_empty_servers(&servers)?;
|
||||
for server in servers {
|
||||
log!(logger, "Running command on '{}'...", server.get_name());
|
||||
match &server.address {
|
||||
@ -514,8 +535,8 @@ fn main() -> Result<(), String> {
|
||||
.map_err(|e| format!("{e}"))?;
|
||||
}
|
||||
ServerAddress::Localhost => {
|
||||
let mut command_args =
|
||||
shell_words::split(&command).map_err(|e| format!("failed to parse command: {e}"))?;
|
||||
let mut command_args = shell_words::split(&command)
|
||||
.map_err(|e| format!("failed to parse command: {e}"))?;
|
||||
ShellCmd::new(command_args.remove(0))
|
||||
.args(&command_args)
|
||||
.current_dir(&server.server_directory_path)
|
||||
@ -536,8 +557,8 @@ fn main() -> Result<(), String> {
|
||||
let download_directory = match download_directory {
|
||||
Some(download_directory) => download_directory,
|
||||
None => {
|
||||
let home_dir =
|
||||
get_home_directory().map_err(|e| format!("Can't determine download directory: {e}"))?;
|
||||
let home_dir = self.get_home_directory()
|
||||
.map_err(|e| format!("Missing download-directory: {e}"))?;
|
||||
home_dir.join("Downloads")
|
||||
}
|
||||
};
|
||||
@ -583,8 +604,8 @@ fn main() -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
require_non_empty_servers(&servers)?;
|
||||
start_ssh_agent(&logger)?;
|
||||
Self::require_non_empty_servers(&servers)?;
|
||||
self.start_ssh_agent(&logger)?;
|
||||
|
||||
for server in servers {
|
||||
log!(logger, "Getting file from {}...", server.get_name());
|
||||
@ -626,26 +647,12 @@ fn main() -> Result<(), String> {
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn osstring_from_ssh_output(output: Vec<u8>) -> OsString {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
OsString::from_vec(output)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
OsString::from_wide(output.iter().map(|&b| b as u16).collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn check_local_file_exists<P>(path: P) -> Result<(), String>
|
||||
where
|
||||
fn check_local_file_exists<P>(path: P) -> Result<(), String>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
{
|
||||
let path = path.as_ref();
|
||||
if !path.is_file() {
|
||||
return Err(format!(
|
||||
@ -655,59 +662,24 @@ where
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn check_file_exists_on_server<P, S, D>(
|
||||
path: P,
|
||||
ssh_address: S,
|
||||
server_directory: D,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
S: AsRef<str>,
|
||||
D: AsRef<Path>,
|
||||
{
|
||||
let full_path = server_directory.as_ref().join(path);
|
||||
match &ShellCmd::new("ssh")
|
||||
.arg(ssh_address.as_ref())
|
||||
.arg(osf!("test -f ") + &full_path)
|
||||
.collect_output()
|
||||
{
|
||||
Ok(_) => Ok(()), //file exists on file server
|
||||
Err(CommandSpecificError {
|
||||
error: ExecutionError::BadExitStatus(_), //test failed
|
||||
..
|
||||
}) => Err(format!(
|
||||
"File '{}' doesn't exist on file-server",
|
||||
full_path.to_string_lossy()
|
||||
)),
|
||||
Err(e) => Err(format!(
|
||||
"Failed to check whether file exists on file-server: {e}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_home_directory() -> Result<PathBuf, String> {
|
||||
homedir::my_home()
|
||||
.map_err(|e| format!("Failed to determine home directory: {e}"))
|
||||
.and_then(|home_dir| home_dir.ok_or("Failed to find home directory".to_string()))
|
||||
}
|
||||
fn require_non_empty_servers<T>(servers: &[T]) -> Result<(), String> {
|
||||
Self::require_non_empty(servers, "servers for this operation")
|
||||
}
|
||||
|
||||
fn require_non_empty_servers<T>(servers: &[T]) -> Result<(), String> {
|
||||
require_non_empty(servers, "servers for this operation")
|
||||
}
|
||||
|
||||
fn require_non_empty<T>(slice: &[T], slice_name: &str) -> Result<(), String> {
|
||||
fn require_non_empty<T>(slice: &[T], slice_name: &str) -> Result<(), String> {
|
||||
if slice.is_empty() {
|
||||
Err(format!(
|
||||
"You did not provide any {slice_name}. Please see --help"
|
||||
))?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn start_ssh_agent(&self, logger: &Logger) -> Result<(), String> {
|
||||
let env = &self.environment;
|
||||
|
||||
fn start_ssh_agent(logger: &Logger) -> Result<(), String> {
|
||||
//start the ssh agent
|
||||
let agent_output = ShellCmd::new("ssh-agent")
|
||||
.arg("-s")
|
||||
@ -722,7 +694,7 @@ fn start_ssh_agent(logger: &Logger) -> Result<(), String> {
|
||||
static ENV_VAR_REGEX: Lazy<Regex> = lazy_regex!("(.+?)=(.+?);");
|
||||
for capture in ENV_VAR_REGEX.captures_iter(&agent_stdout) {
|
||||
let (_, [env_var, value]) = capture.extract();
|
||||
env::set_var(env_var, value);
|
||||
env.set_var(env_var, value);
|
||||
}
|
||||
|
||||
//add the ssh key
|
||||
@ -730,19 +702,45 @@ fn start_ssh_agent(logger: &Logger) -> Result<(), String> {
|
||||
.run(logger)
|
||||
.map_err(|e| format!("failed to add ssh-key: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_server_configuration_from_env() -> Result<Vec<Server>, String> {
|
||||
env::var(SERVERS_ENV_VAR)
|
||||
fn parse_server_configuration_from_env(&self) -> Result<Vec<Server>, String> {
|
||||
self
|
||||
.environment
|
||||
.var(SERVERS_ENV_VAR)
|
||||
.map_err(|_| format!("Missing environment variable {}", SERVERS_ENV_VAR))
|
||||
.and_then(|value| parse_server_configuration(&value))
|
||||
.and_then(|value| parse_server_configuration(&value, || self.get_home_directory()))
|
||||
}
|
||||
|
||||
fn get_home_directory(&self) -> Result<PathBuf, String> {
|
||||
self.environment.get_home_directory().ok_or("Failed to find your home directory".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_server_configuration(config_str: &str) -> Result<Vec<Server>, String> {
|
||||
fn main() -> Result<(), String> {
|
||||
Application::<Prod>::default().run()
|
||||
}
|
||||
|
||||
//This will be moved into the shell-interface
|
||||
fn osstring_from_ssh_output(output: Vec<u8>) -> OsString {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
OsString::from_vec(output)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
OsString::from_wide(output.iter().map(|&b| b as u16).collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_server_configuration<F>(config_str: &str, get_home_directory: F) -> Result<Vec<Server>, String> where F: Fn() -> Result<PathBuf, String> {
|
||||
config_str
|
||||
.split(',')
|
||||
.map(|server_entry| {
|
||||
Server::from_str(server_entry, RelativeLocalPathAnker::Home)
|
||||
Server::from_str(server_entry, RelativeLocalPathAnker::Home, &get_home_directory)
|
||||
.map_err(|e| format!("Invalid server entry '{server_entry}': {e}"))
|
||||
})
|
||||
.collect()
|
||||
@ -758,7 +756,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_parse_server_configuration() {
|
||||
let servers =
|
||||
parse_server_configuration("foo:bar,.:fizz/buzz").expect("valid server configuration");
|
||||
parse_server_configuration("foo:bar,.:fizz/buzz", || Ok(PathBuf::from("/test"))).expect("valid server configuration");
|
||||
assert_eq!(
|
||||
vec![
|
||||
Server {
|
||||
@ -769,7 +767,7 @@ mod test {
|
||||
},
|
||||
Server {
|
||||
address: ServerAddress::Localhost,
|
||||
server_directory_path: PathBuf::from("fizz/buzz"),
|
||||
server_directory_path: PathBuf::from("/test/fizz/buzz"),
|
||||
}
|
||||
],
|
||||
servers
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::get_home_directory;
|
||||
use crate::environment::{Environment, Prod};
|
||||
use std::cell::LazyCell;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
@ -72,7 +72,11 @@ impl FromStr for ServerReference {
|
||||
type Err = ServerReferenceParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Server::from_str(s, RelativeLocalPathAnker::CurrentDirectory)
|
||||
Server::from_str(s, RelativeLocalPathAnker::CurrentDirectory, || {
|
||||
Prod::default()
|
||||
.get_home_directory()
|
||||
.ok_or("missing home directory".to_string())
|
||||
})
|
||||
.map(Self::Resolved)
|
||||
.or_else(|_| Ok(Self::Identifier(s.to_string())))
|
||||
}
|
||||
@ -126,10 +130,14 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(
|
||||
pub fn from_str<F>(
|
||||
s: &str,
|
||||
relative_local_path_anker: RelativeLocalPathAnker,
|
||||
) -> Result<Self, ServerParseError> {
|
||||
get_home_directory: F,
|
||||
) -> Result<Self, ServerParseError>
|
||||
where
|
||||
F: Fn() -> Result<PathBuf, String>,
|
||||
{
|
||||
s.split_once(':')
|
||||
.ok_or(ServerParseError::MissingServerDirectory)
|
||||
.and_then(|(identifier, server_directory)| {
|
||||
|
||||
313
src/shell_interface.rs
Normal file
313
src/shell_interface.rs
Normal file
@ -0,0 +1,313 @@
|
||||
use crate::log;
|
||||
use crate::logger::{LogLevel, Logger};
|
||||
use std::error::Error;
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use std::io;
|
||||
use std::iter::once;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EnvCommand<'a, E> {
|
||||
command: ShellCommand,
|
||||
environment: &'a E,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ShellCommand {
|
||||
Ssh {
|
||||
address: String,
|
||||
server_command: ServerCommand,
|
||||
},
|
||||
Scp {
|
||||
source: ScpParam,
|
||||
destination: ScpParam,
|
||||
},
|
||||
SshAgent,
|
||||
ShhAdd,
|
||||
Editor(Vec<OsString>),
|
||||
Execute {
|
||||
working_directory: PathBuf,
|
||||
command: OsString,
|
||||
},
|
||||
}
|
||||
|
||||
impl ShellCommand {
|
||||
pub fn at<E>(self, environment: &E) -> EnvCommand<E> {
|
||||
EnvCommand {
|
||||
command: self,
|
||||
environment,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ServerCommand {
|
||||
Realpath {
|
||||
path: PathBuf,
|
||||
},
|
||||
Ls {
|
||||
dir: PathBuf,
|
||||
},
|
||||
Rm {
|
||||
file: PathBuf,
|
||||
},
|
||||
Mv {
|
||||
source: PathBuf,
|
||||
destination: PathBuf,
|
||||
},
|
||||
Execute {
|
||||
working_directory: PathBuf,
|
||||
command: OsString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScpParam {
|
||||
pub server: Option<String>,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl Display for ShellCommand {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
command_to_string(&build_command_from_shell_command(self))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn command_to_string(command: &Command) -> String {
|
||||
once(command.get_program().to_string_lossy().to_string())
|
||||
.chain(command.get_args().map(|arg| {
|
||||
let arg_str = arg.to_string_lossy();
|
||||
if arg_str.contains(' ') {
|
||||
format!("\"{arg_str}\"")
|
||||
} else {
|
||||
arg_str.to_string()
|
||||
}
|
||||
}))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn build_command_from_shell_command(shell_command: &ShellCommand) -> Command {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub trait ShellInterface {
|
||||
fn run(self) -> CommandResult<ExitStatus, StartError>;
|
||||
fn output(self) -> CommandResult<CommandOutput, StartError>;
|
||||
fn run_logged(self, logger: &Logger) -> CommandResult<LoggedRunOutput, StartError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match logger.level {
|
||||
LogLevel::Debug | LogLevel::Info => {
|
||||
let res = self.run();
|
||||
CommandResult {
|
||||
result: res.result.map(LoggedRunOutput::from),
|
||||
command: res.command,
|
||||
}
|
||||
}
|
||||
LogLevel::Error => {
|
||||
let res = self.output();
|
||||
CommandResult {
|
||||
result: res.result.map(LoggedRunOutput::from),
|
||||
command: res.command,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MaybeCast<T> {
|
||||
fn maybe_cast(&self) -> Option<&T>;
|
||||
}
|
||||
|
||||
impl<T> MaybeCast<T> for T {
|
||||
fn maybe_cast(&self) -> Option<&T> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoggedRunOutput {
|
||||
ExitStatus(ExitStatus),
|
||||
CommandOutput(CommandOutput),
|
||||
}
|
||||
|
||||
impl From<ExitStatus> for LoggedRunOutput {
|
||||
fn from(value: ExitStatus) -> Self {
|
||||
Self::ExitStatus(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CommandOutput> for LoggedRunOutput {
|
||||
fn from(value: CommandOutput) -> Self {
|
||||
Self::CommandOutput(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<ExitStatus> for LoggedRunOutput {
|
||||
fn as_ref(&self) -> &ExitStatus {
|
||||
match self {
|
||||
LoggedRunOutput::ExitStatus(status) => status,
|
||||
LoggedRunOutput::CommandOutput(output) => output.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MaybeCast<CommandOutput> for LoggedRunOutput {
|
||||
fn maybe_cast(&self) -> Option<&CommandOutput> {
|
||||
match self {
|
||||
LoggedRunOutput::ExitStatus(_) => None,
|
||||
LoggedRunOutput::CommandOutput(output) => Some(output),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandResult<T, E> {
|
||||
pub command: ShellCommand,
|
||||
pub result: Result<T, E>,
|
||||
}
|
||||
|
||||
impl<T, E> CommandResult<T, E> {
|
||||
pub fn into_result(self) -> Result<T, CommandError<E>> {
|
||||
self.result.map_err(|error| CommandError {
|
||||
command: self.command,
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> CommandResult<T, StartError> {
|
||||
pub fn and_expect_success(self) -> CommandResult<T, ExecutionError<T>>
|
||||
where
|
||||
T: AsRef<ExitStatus>,
|
||||
{
|
||||
CommandResult {
|
||||
result: self.result.map_err(ExecutionError::from).and_then(|t| {
|
||||
if t.as_ref().success {
|
||||
Ok(t)
|
||||
} else {
|
||||
Err(ExecutionError::BadExitStatus(t))
|
||||
}
|
||||
}),
|
||||
command: self.command,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandResult<CommandOutput, ExecutionError<CommandOutput>> {
|
||||
pub fn into_result_with_error_logging(
|
||||
self,
|
||||
logger: &Logger,
|
||||
) -> Result<CommandOutput, CommandError<ExecutionError<CommandOutput>>> {
|
||||
self.result.map_err(|error| {
|
||||
if let ExecutionError::BadExitStatus(output) = &error {
|
||||
log!(logger, error, "{}", output.stdout.to_string_lossy());
|
||||
log!(logger, error, "{}", output.stderr.to_string_lossy());
|
||||
}
|
||||
CommandError {
|
||||
command: self.command,
|
||||
error,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CommandOutput {
|
||||
pub stdout: OsString,
|
||||
pub stderr: OsString,
|
||||
pub status: ExitStatus,
|
||||
}
|
||||
|
||||
impl AsRef<ExitStatus> for CommandOutput {
|
||||
fn as_ref(&self) -> &ExitStatus {
|
||||
&self.status
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExitStatus {
|
||||
pub success: bool,
|
||||
pub string_form: String,
|
||||
pub code: Option<i32>,
|
||||
}
|
||||
|
||||
impl AsRef<ExitStatus> for ExitStatus {
|
||||
fn as_ref(&self) -> &ExitStatus {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ExitStatus {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&self.string_form, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandError<E> {
|
||||
pub command: ShellCommand,
|
||||
pub error: E,
|
||||
}
|
||||
|
||||
impl<E> Display for CommandError<E>
|
||||
where
|
||||
E: Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Error while running command '{}': {}",
|
||||
self.command, self.error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Error for CommandError<E> where E: Error {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StartError(io::Error);
|
||||
|
||||
impl Display for StartError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Failed to run command: {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for StartError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ExecutionError<T> {
|
||||
StartError(StartError),
|
||||
BadExitStatus(T),
|
||||
}
|
||||
|
||||
impl<T> From<StartError> for ExecutionError<T> {
|
||||
fn from(value: StartError) -> Self {
|
||||
Self::StartError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Display for ExecutionError<T>
|
||||
where
|
||||
T: AsRef<ExitStatus>,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ExecutionError::StartError(e) => Display::fmt(e, f),
|
||||
ExecutionError::BadExitStatus(status) => {
|
||||
write!(f, "execution failed with {}", status.as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Error for ExecutionError<T> where T: AsRef<ExitStatus> + Debug {}
|
||||
Loading…
Reference in New Issue
Block a user