use crate::logger::{LogLevel, Logger}; use crate::server::{Server, ServerAddress}; use crate::{log, osf}; use std::error::Error; use std::ffi::OsString; use std::fmt::{Debug, Display, Formatter}; use std::iter::once; use std::path::{Path, PathBuf}; use std::process::{Command, Output}; use std::{io, process}; #[derive(Debug)] pub struct EnvCommand<'a, E> { command: ShellCommand, environment: &'a mut E, } impl EnvCommand<'_, E> where E: ShellInterface, { pub fn run(self) -> CommandResult { self.environment.run_command(self.command) } pub fn output(self) -> CommandResult { self.environment.collect_command_output(self.command) } pub fn run_logged(self, logger: &Logger) -> CommandResult 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, } } } } } #[derive(Debug, Clone)] pub enum ShellCommand { Ssh { address: String, server_command: ServerCommand, }, Scp { source: ScpParam, destination: ScpParam, }, SshAgent, ShhAdd, Editor(Vec), Execute { working_directory: PathBuf, command: Vec, }, } impl ShellCommand { pub fn in_env(self, environment: &mut E) -> EnvCommand { 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, pub path: PathBuf, } impl

From<(&Server, P)> for ScpParam where P: AsRef, { fn from((server, path): (&Server, P)) -> Self { Self { server: match &server.address { ServerAddress::Ssh { ssh_address } => Some(ssh_address.into()), ServerAddress::Localhost => None, }, path: PathBuf::from(path.as_ref()), } } } impl From<&Path> for ScpParam { fn from(value: &Path) -> Self { Self { server: None, path: PathBuf::from(value), } } } impl From<&ScpParam> for OsString { fn from(value: &ScpParam) -> Self { let mut builder = osf!(); if let Some(server) = &value.server { builder += format!("{server}:"); } builder += &value.path; builder.build() } } 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::>() .join(" ") } macro_rules! cmd { ($programm: expr $(, $arg:expr )*)=> {{ #[allow(unused_mut)] let mut cmd = Command::new($programm); $( cmd.arg($arg); )* cmd }}; } pub fn build_command_from_shell_command(shell_command: &ShellCommand) -> Command { match shell_command { ShellCommand::Ssh { address, server_command, } => cmd!( "ssh", address, match server_command { ServerCommand::Realpath { path } => osf!("realpath -e ") + path, ServerCommand::Ls { dir } => osf!("ls ") + dir, ServerCommand::Rm { file } => osf!("rm ") + file, ServerCommand::Mv { source, destination, } => osf!("mv ") + source + " " + destination, ServerCommand::Execute { working_directory, command, } => osf!("cd ") + working_directory + "; " + command, } ), ShellCommand::Scp { source, destination, } => cmd!("scp", OsString::from(source), OsString::from(destination)), ShellCommand::SshAgent => cmd!("ssh-agent", "-s"), ShellCommand::ShhAdd => cmd!("ssh-add"), ShellCommand::Editor(args) => { let mut args = args.clone(); let mut cmd = cmd!(args.remove(0)); cmd.args(args); cmd } ShellCommand::Execute { working_directory, command, } => { let mut args = command.clone(); let mut cmd = cmd!(args.remove(0)); cmd.args(args).current_dir(working_directory); cmd } } } pub trait ShellInterface { fn run_command(&mut self, command: ShellCommand) -> CommandResult; fn collect_command_output( &mut self, command: ShellCommand, ) -> CommandResult; } pub trait MaybeCast { fn maybe_cast(&self) -> Option<&T>; } impl MaybeCast for T { fn maybe_cast(&self) -> Option<&T> { Some(self) } } #[derive(Debug)] pub enum LoggedRunOutput { ExitStatus(ExitStatus), CommandOutput(CommandOutput), } impl From for LoggedRunOutput { fn from(value: ExitStatus) -> Self { Self::ExitStatus(value) } } impl From for LoggedRunOutput { fn from(value: CommandOutput) -> Self { Self::CommandOutput(value) } } impl AsRef for LoggedRunOutput { fn as_ref(&self) -> &ExitStatus { match self { LoggedRunOutput::ExitStatus(status) => status, LoggedRunOutput::CommandOutput(output) => output.as_ref(), } } } impl MaybeCast for LoggedRunOutput { fn maybe_cast(&self) -> Option<&CommandOutput> { match self { LoggedRunOutput::ExitStatus(_) => None, LoggedRunOutput::CommandOutput(output) => Some(output), } } } #[derive(Debug)] pub struct CommandResult { pub command: ShellCommand, pub result: Result, } impl CommandResult { pub fn into_result(self) -> Result> { self.result.map_err(|error| CommandError { command: self.command, error, }) } } impl CommandResult { pub fn and_expect_success(self) -> CommandResult> where T: AsRef, { 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> { pub fn into_result_with_error_logging( self, logger: &Logger, ) -> Result>> where T: MaybeCast, { self.result.map_err(|error| { if let ExecutionError::BadExitStatus(t) = &error { if let Some(output) = t.maybe_cast() { 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 From for CommandOutput { fn from(value: Output) -> Self { Self { stdout: os_string_from_ssh_output(value.stdout), stderr: os_string_from_ssh_output(value.stderr), status: value.status.into(), } } } pub fn os_string_from_ssh_output(output: Vec) -> 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()) } } impl AsRef for CommandOutput { fn as_ref(&self) -> &ExitStatus { &self.status } } #[derive(Debug, Clone)] pub struct ExitStatus { pub success: bool, pub string_form: String, #[allow(dead_code)] pub code: Option, } impl From for ExitStatus { fn from(value: process::ExitStatus) -> Self { Self { success: value.success(), string_form: value.to_string(), code: value.code(), } } } impl AsRef 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 { pub command: ShellCommand, pub error: E, } impl From> for String where E: Display, { fn from(value: CommandError) -> Self { value.to_string() } } impl Display for CommandError where E: Display, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "Error while running command '{}': {}", self.command, self.error ) } } impl Error for CommandError where E: Error {} #[derive(Debug)] pub struct StartError(io::Error); impl From for StartError { fn from(value: io::Error) -> Self { StartError(value) } } 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 { StartError(StartError), BadExitStatus(T), } impl From for ExecutionError { fn from(value: StartError) -> Self { Self::StartError(value) } } impl Display for ExecutionError where T: AsRef, { 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 Error for ExecutionError where T: AsRef + Debug {} #[cfg(test)] mod test_commands { use crate::shell_interface::{ScpParam, ServerCommand, ShellCommand}; use std::path::PathBuf; #[test] fn test_to_string() { assert_eq!( ShellCommand::Ssh { address: "crea".to_string(), server_command: ServerCommand::Realpath { path: PathBuf::from("plugins/*.jar") } } .to_string(), r#"ssh crea "realpath -e plugins/*.jar""# ); assert_eq!( ShellCommand::Ssh { address: "crea".to_string(), server_command: ServerCommand::Ls { dir: PathBuf::from("creative/plugins") } } .to_string(), r#"ssh crea "ls creative/plugins""# ); assert_eq!( ShellCommand::Ssh { address: "crea".to_string(), server_command: ServerCommand::Rm { file: PathBuf::from("foo.txt") }, } .to_string(), r#"ssh crea "rm foo.txt""# ); assert_eq!( ShellCommand::Ssh { address: "crea".to_string(), server_command: ServerCommand::Mv { source: PathBuf::from("foo"), destination: PathBuf::from("bar") } } .to_string(), r#"ssh crea "mv foo bar""# ); assert_eq!( ShellCommand::Ssh { address: "crea".to_string(), server_command: ServerCommand::Execute { working_directory: PathBuf::from(".."), command: "sudo rm -rf *".into(), } } .to_string(), r#"ssh crea "cd ..; sudo rm -rf *""# ); assert_eq!( ShellCommand::Scp { source: ScpParam { server: None, path: PathBuf::from("target/mssh") }, destination: ScpParam { server: Some("crea".into()), path: PathBuf::from("/usr/bin") }, } .to_string(), r#"scp target/mssh crea:/usr/bin"# ); assert_eq!(ShellCommand::SshAgent.to_string(), r#"ssh-agent -s"#); assert_eq!(ShellCommand::ShhAdd.to_string(), r#"ssh-add"#); assert_eq!( ShellCommand::Editor(vec!["kate".into(), "-b".into(), "test.txt".into()]).to_string(), r#"kate -b test.txt"# ); assert_eq!( ShellCommand::Execute { working_directory: PathBuf::from("/home/me/server"), command: vec!["java".into(), "-jar".into(), "paper.jar".into()] } .to_string(), r#"java -jar paper.jar"# ); } }