Use new ShellCommand interface in main application

This commit is contained in:
Leonard Steppy 2025-02-05 23:55:06 +01:00
parent 85a9cd9ae5
commit 7c64383d72
3 changed files with 188 additions and 105 deletions

View File

@ -1,3 +1,5 @@
#[deprecated]
use crate::log; use crate::log;
use crate::logger::{LogLevel, Logger}; use crate::logger::{LogLevel, Logger};
use crate::shell_interface::command_to_string; use crate::shell_interface::command_to_string;

View File

@ -9,14 +9,13 @@ mod server;
mod shell_interface; mod shell_interface;
use crate::action::{Action, FileAction, ServerActions}; use crate::action::{Action, FileAction, ServerActions};
use crate::command::LogRunnable;
use crate::environment::{Environment, Prod}; use crate::environment::{Environment, Prod};
use crate::file::{FileMatcher, FileNameInfo}; use crate::file::{FileMatcher, FileNameInfo};
use crate::logger::{LogLevel, Logger}; use crate::logger::{LogLevel, Logger};
use crate::os_str_extension::OsStrExtension; use crate::os_str_extension::OsStrExtension;
use crate::os_string_builder::ReplaceWithOsStr; use crate::os_string_builder::ReplaceWithOsStr;
use crate::server::{RelativeLocalPathAnker, ServerAddress}; use crate::server::{RelativeLocalPathAnker, ServerAddress};
use crate::shell_interface::os_string_from_ssh_output; use crate::shell_interface::{ScpParam, ServerCommand, ShellCommand, ShellInterface};
use clap::{Parser, Subcommand, ValueEnum}; use clap::{Parser, Subcommand, ValueEnum};
use lazy_regex::{lazy_regex, Lazy, Regex}; use lazy_regex::{lazy_regex, Lazy, Regex};
use server::{Server, ServerReference}; use server::{Server, ServerReference};
@ -32,8 +31,6 @@ const SERVERS_ENV_VAR: &str = "MSSH_SERVERS";
const EDITOR_ENV_VAR: &str = "MSSH_EDITOR"; const EDITOR_ENV_VAR: &str = "MSSH_EDITOR";
const FILE_PLACEHOLDER: &str = "<file>"; const FILE_PLACEHOLDER: &str = "<file>";
type ShellCmd = std::process::Command;
/// Uploads a file or executes a command on multiple configured servers /// Uploads a file or executes a command on multiple configured servers
/// ///
/// Servers must either be configured via environment variable or denote their server directory with /// Servers must either be configured via environment variable or denote their server directory with
@ -155,7 +152,11 @@ where
} }
pub fn run_with_args(&mut self, args: Args) -> Result<(), String> { pub fn run_with_args(&mut self, args: Args) -> Result<(), String> {
let _env = &mut self.environment; macro_rules! env {
() => {
&mut self.environment
};
}
let logger = Logger { let logger = Logger {
//all the below options are conflicting with each other so an if else is fine //all the below options are conflicting with each other so an if else is fine
@ -231,23 +232,29 @@ where
files = files files = files
.iter() .iter()
.map(|file| { .map(|file| {
let output = ShellCmd::new("ssh") let output = ShellCommand::Ssh {
.arg(ssh_address) address: ssh_address.to_string(),
.arg(osf!("realpath -e ") + file_server.server_directory_path.join(file)) server_command: ServerCommand::Realpath {
.collect_full_output() path: file_server.server_directory_path.join(file),
},
}
.in_env(env!())
.output()
.into_result()
.map_err(|e| format!("Failed to canonicalize files: {e}"))?; .map_err(|e| format!("Failed to canonicalize files: {e}"))?;
if !output.status.success() { if !output.status.success {
Err(format!( Err(format!(
"Path doesn't match any files on file-server: {}", "Path doesn't match any files on file-server: {}",
file.to_string_lossy() file.to_string_lossy()
))?; ))?;
} }
let denoted_files = os_string_from_ssh_output(output.stdout) let denoted_files = output
.stdout
.split(b'\n') //split at line breaks .split(b'\n') //split at line breaks
.into_iter() .into_iter()
.filter(|bytes| !bytes.is_empty()) //needed since realpath sometimes gives us empty lines .filter(|file_name| !file_name.is_empty()) //needed since realpath sometimes gives us empty lines
.map(PathBuf::from) .map(PathBuf::from)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -305,19 +312,23 @@ where
server, server,
actions: { actions: {
let present_file_names: Vec<OsString> = match &server.address { let present_file_names: Vec<OsString> = match &server.address {
ServerAddress::Ssh { ssh_address } => os_string_from_ssh_output( ServerAddress::Ssh { ssh_address } => ShellCommand::Ssh {
ShellCmd::new("ssh") address: ssh_address.to_string(),
.arg(ssh_address) server_command: ServerCommand::Ls {
.arg(osf!("ls ") + &working_directory) dir: working_directory.clone(),
.collect_output() },
}
.in_env(env!())
.output()
.and_expect_success()
.into_result_with_error_logging(&logger)
.map_err(|e| { .map_err(|e| {
format!( format!(
"Failed to query present files on server {}: {e}", "Failed to query present files on server {}: {e}",
server.get_name() server.get_name()
) )
})? })?
.stdout, .stdout
)
.split(b'\n') .split(b'\n')
.into_iter() .into_iter()
.map(OsString::from) .map(OsString::from)
@ -452,31 +463,36 @@ where
for file_action in server_actions.actions { for file_action in server_actions.actions {
match file_action.kind { match file_action.kind {
Action::Add | Action::Replace => { Action::Add | Action::Replace => {
let scp_source = match &file_server { let source = match &file_server {
Some(file_server) => { Some(file_server) => ScpParam::from((
osf!(match &file_server.address { file_server,
ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"), file_server.server_directory_path.join(&file_action.file),
ServerAddress::Localhost => "".to_string(), )),
}) + file_server.server_directory_path.join(&file_action.file) None => ScpParam::from(file_action.file.as_path()),
}
None => osf!(&file_action.file),
}; };
let scp_target = osf!(match &server.address { let destination = ScpParam::from((server, &server_actions.working_directory));
ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"), ShellCommand::Scp {
ServerAddress::Localhost => "".to_string(), source,
}) + &server_actions.working_directory; destination,
ShellCmd::new("scp") }
.arg(scp_source) .in_env(env!())
.arg(scp_target) .run_logged(&logger)
.run(&logger) .and_expect_success()
.into_result_with_error_logging(&logger)
.map_err(|e| format!("upload failure: {e}"))?; .map_err(|e| format!("upload failure: {e}"))?;
} }
Action::Delete => match &server.address { Action::Delete => match &server.address {
ServerAddress::Ssh { ssh_address } => { ServerAddress::Ssh { ssh_address } => {
ShellCmd::new("ssh") ShellCommand::Ssh {
.arg(ssh_address) address: ssh_address.to_string(),
.arg(osf!("rm ") + server_actions.working_directory.join(&file_action.file)) server_command: ServerCommand::Rm {
.run(&logger) file: server_actions.working_directory.join(&file_action.file),
},
}
.in_env(env!())
.run_logged(&logger)
.and_expect_success()
.into_result_with_error_logging(&logger)
.map_err(|e| format!("failed to delete old version: {e}"))?; .map_err(|e| format!("failed to delete old version: {e}"))?;
} }
ServerAddress::Localhost => { ServerAddress::Localhost => {
@ -486,15 +502,17 @@ where
}, },
Action::Rename { new_name } => match &server.address { Action::Rename { new_name } => match &server.address {
ServerAddress::Ssh { ssh_address } => { ServerAddress::Ssh { ssh_address } => {
ShellCmd::new("ssh") ShellCommand::Ssh {
.arg(ssh_address) address: ssh_address.to_string(),
.arg( server_command: ServerCommand::Mv {
osf!("mv ") source: server_actions.working_directory.join(&file_action.file),
+ server_actions.working_directory.join(&file_action.file) destination: server_actions.working_directory.join(&new_name),
+ " " },
+ server_actions.working_directory.join(&new_name), }
) .in_env(env!())
.run(&logger) .run_logged(&logger)
.and_expect_success()
.into_result_with_error_logging(&logger)
.map_err(|e| format!("failed to rename: {e}"))?; .map_err(|e| format!("failed to rename: {e}"))?;
} }
ServerAddress::Localhost => { ServerAddress::Localhost => {
@ -516,20 +534,33 @@ where
log!(logger, "Running command on '{}'...", server.get_name()); log!(logger, "Running command on '{}'...", server.get_name());
match &server.address { match &server.address {
ServerAddress::Ssh { ssh_address } => { ServerAddress::Ssh { ssh_address } => {
ShellCmd::new("ssh") ShellCommand::Ssh {
.arg(ssh_address) address: ssh_address.to_string(),
.arg(osf!("cd ") + server.server_directory_path + "; " + &command) server_command: ServerCommand::Execute {
.run(&logger) working_directory: server.server_directory_path.clone(),
command: OsString::from(&command),
},
}
.in_env(env!())
.run_logged(&logger)
.and_expect_success()
.into_result_with_error_logging(&logger)
.map_err(|e| format!("{e}"))?; .map_err(|e| format!("{e}"))?;
} }
ServerAddress::Localhost => { ServerAddress::Localhost => {
let mut command_args = shell_words::split(&command) let command = shell_words::split(&command)
.map_err(|e| format!("failed to parse command: {e}"))?; .map_err(|e| format!("failed to parse command: {e}"))?
ShellCmd::new(command_args.remove(0)) .into_iter()
.args(&command_args) .map(OsString::from)
.current_dir(&server.server_directory_path) .collect();
.run(&logger) ShellCommand::Execute {
.map_err(|e| format!("{e}"))?; working_directory: server.server_directory_path.clone(),
command,
}
.in_env(env!())
.run_logged(&logger)
.and_expect_success()
.into_result_with_error_logging(&logger)?;
} }
} }
} }
@ -597,18 +628,19 @@ where
for server in servers { for server in servers {
log!(logger, "Getting file from {}...", server.get_name()); log!(logger, "Getting file from {}...", server.get_name());
let file_source = osf!(match &server.address { let source = ScpParam::from((&server, server.server_directory_path.join(&file)));
ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"), ShellCommand::Scp {
ServerAddress::Localhost => "".to_string(), source: source.clone(),
}) + server.server_directory_path.join(&file); destination: ScpParam::from(download_directory.as_path()),
ShellCmd::new("scp") }
.arg(&file_source) .in_env(env!())
.arg(&download_directory) .run_logged(&logger)
.run(&logger) .and_expect_success()
.into_result_with_error_logging(&logger)
.map_err(|e| format!("download failure: {e}"))?; .map_err(|e| format!("download failure: {e}"))?;
//open file in editor //open file in editor
let mut editor_command_args = shell_words::split(&editor) let editor_command = shell_words::split(&editor)
.map_err(|e| format!("failed to parse editor command: {e}"))? .map_err(|e| format!("failed to parse editor command: {e}"))?
.into_iter() .into_iter()
.map(|part| { .map(|part| {
@ -616,17 +648,22 @@ where
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let command = editor_command_args.remove(0); ShellCommand::Editor(editor_command)
ShellCmd::new(command) .in_env(env!())
.args(editor_command_args) .run_logged(&logger)
.run(&logger) .and_expect_success()
.into_result_with_error_logging(&logger)
.map_err(|e| format!("failed to open file in editor: {e}"))?; .map_err(|e| format!("failed to open file in editor: {e}"))?;
//upload file again //upload file again
ShellCmd::new("scp") ShellCommand::Scp {
.arg(download_directory.join(file_name)) source: ScpParam::from(download_directory.join(file_name).as_path()),
.arg(&file_source) destination: source,
.run(&logger) }
.in_env(env!())
.run_logged(&logger)
.and_expect_success()
.into_result_with_error_logging(&logger)
.map_err(|e| format!("failed to re-upload file: {e}"))?; .map_err(|e| format!("failed to re-upload file: {e}"))?;
} }
@ -669,25 +706,30 @@ where
let env = &mut self.environment; let env = &mut self.environment;
//start the ssh agent //start the ssh agent
let agent_output = ShellCmd::new("ssh-agent") let agent_output = ShellCommand::SshAgent
.arg("-s") .in_env(env)
.collect_output() .output()
.map_err(|e| format!("failed to start ssh agent: {e}"))?; .and_expect_success()
let agent_stdout = String::from_utf8_lossy(&agent_output.stdout); .into_result_with_error_logging(logger)
if !agent_output.status.success() { .map_err(|e| format!("Failed to start ssh agent: {e}"))?;
return Err("failed to start ssh agent; maybe try to run ssh-agent manually?".to_string()); let agent_stdout = &agent_output
} .stdout
.into_string()
.map_err(|_| "ssh-agent returned invalid utf-8 - how did this even happen?")?;
//set the env vars from the agent //set the env vars from the agent
static ENV_VAR_REGEX: Lazy<Regex> = lazy_regex!("(.+?)=(.+?);"); static ENV_VAR_REGEX: Lazy<Regex> = lazy_regex!("(.+?)=(.+?);");
for capture in ENV_VAR_REGEX.captures_iter(&agent_stdout) { for capture in ENV_VAR_REGEX.captures_iter(agent_stdout) {
let (_, [env_var, value]) = capture.extract(); let (_, [env_var, value]) = capture.extract();
env.set_var(env_var, value); env.set_var(env_var, value);
} }
//add the ssh key //add the ssh key
ShellCmd::new("ssh-add") ShellCommand::ShhAdd
.run(logger) .in_env(env)
.run_logged(logger)
.and_expect_success()
.into_result_with_error_logging(logger)
.map_err(|e| format!("failed to add ssh-key: {e}"))?; .map_err(|e| format!("failed to add ssh-key: {e}"))?;
Ok(()) Ok(())
} }

View File

@ -1,16 +1,20 @@
use crate::logger::{LogLevel, Logger}; use crate::logger::{LogLevel, Logger};
use crate::server::{Server, ServerAddress};
use crate::{log, osf}; use crate::{log, osf};
use std::error::Error; use std::error::Error;
use std::ffi::OsString; use std::ffi::OsString;
use std::fmt::{Debug, Display, Formatter}; use std::fmt::{Debug, Display, Formatter};
use std::iter::once; use std::iter::once;
use std::path::PathBuf; use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::process::{Command, Output}; use std::process::{Command, Output};
use std::{io, process}; use std::{io, process};
#[derive(Debug)] #[derive(Debug)]
pub struct EnvCommand<'a, E> { pub struct EnvCommand<'a, E> {
command: ShellCommand, command: ShellCommand,
phantom_data: PhantomData<&'a E>,
#[cfg(test)]
environment: &'a mut E, environment: &'a mut E,
} }
@ -34,10 +38,12 @@ pub enum ShellCommand {
} }
impl ShellCommand { impl ShellCommand {
pub fn in_env<E>(self, environment: &mut E) -> EnvCommand<E> { pub fn in_env<E>(self, _environment: &mut E) -> EnvCommand<E> {
EnvCommand { EnvCommand {
command: self, command: self,
environment, phantom_data: Default::default(),
#[cfg(test)]
environment: _environment,
} }
} }
} }
@ -69,6 +75,30 @@ pub struct ScpParam {
pub path: PathBuf, pub path: PathBuf,
} }
impl<P> From<(&Server, P)> for ScpParam
where
P: AsRef<Path>,
{
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 { impl From<&ScpParam> for OsString {
fn from(value: &ScpParam) -> Self { fn from(value: &ScpParam) -> Self {
let mut builder = osf!(); let mut builder = osf!();
@ -327,8 +357,7 @@ impl From<Output> for CommandOutput {
} }
} }
//TODO remove super visibility once it is not needed anymore pub fn os_string_from_ssh_output(output: Vec<u8>) -> OsString {
pub(super) fn os_string_from_ssh_output(output: Vec<u8>) -> OsString {
#[cfg(unix)] #[cfg(unix)]
{ {
use std::os::unix::ffi::OsStringExt; use std::os::unix::ffi::OsStringExt;
@ -352,6 +381,7 @@ impl AsRef<ExitStatus> for CommandOutput {
pub struct ExitStatus { pub struct ExitStatus {
pub success: bool, pub success: bool,
pub string_form: String, pub string_form: String,
#[allow(dead_code)]
pub code: Option<i32>, pub code: Option<i32>,
} }
@ -383,6 +413,15 @@ pub struct CommandError<E> {
pub error: E, pub error: E,
} }
impl<E> From<CommandError<E>> for String
where
E: Display,
{
fn from(value: CommandError<E>) -> Self {
value.to_string()
}
}
impl<E> Display for CommandError<E> impl<E> Display for CommandError<E>
where where
E: Display, E: Display,