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::logger::{LogLevel, Logger};
use crate::shell_interface::command_to_string;

View File

@ -9,14 +9,13 @@ mod server;
mod shell_interface;
use crate::action::{Action, FileAction, ServerActions};
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;
use crate::os_string_builder::ReplaceWithOsStr;
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 lazy_regex::{lazy_regex, Lazy, Regex};
use server::{Server, ServerReference};
@ -32,8 +31,6 @@ const SERVERS_ENV_VAR: &str = "MSSH_SERVERS";
const EDITOR_ENV_VAR: &str = "MSSH_EDITOR";
const FILE_PLACEHOLDER: &str = "<file>";
type ShellCmd = std::process::Command;
/// 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
@ -155,7 +152,11 @@ where
}
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 {
//all the below options are conflicting with each other so an if else is fine
@ -231,23 +232,29 @@ where
files = files
.iter()
.map(|file| {
let output = ShellCmd::new("ssh")
.arg(ssh_address)
.arg(osf!("realpath -e ") + file_server.server_directory_path.join(file))
.collect_full_output()
.map_err(|e| format!("Failed to canonicalize files: {e}"))?;
let output = ShellCommand::Ssh {
address: ssh_address.to_string(),
server_command: ServerCommand::Realpath {
path: file_server.server_directory_path.join(file),
},
}
.in_env(env!())
.output()
.into_result()
.map_err(|e| format!("Failed to canonicalize files: {e}"))?;
if !output.status.success() {
if !output.status.success {
Err(format!(
"Path doesn't match any files on file-server: {}",
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
.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)
.collect::<Vec<_>>();
@ -305,19 +312,23 @@ where
server,
actions: {
let present_file_names: Vec<OsString> = match &server.address {
ServerAddress::Ssh { ssh_address } => os_string_from_ssh_output(
ShellCmd::new("ssh")
.arg(ssh_address)
.arg(osf!("ls ") + &working_directory)
.collect_output()
.map_err(|e| {
format!(
"Failed to query present files on server {}: {e}",
server.get_name()
)
})?
.stdout,
)
ServerAddress::Ssh { ssh_address } => ShellCommand::Ssh {
address: ssh_address.to_string(),
server_command: ServerCommand::Ls {
dir: working_directory.clone(),
},
}
.in_env(env!())
.output()
.and_expect_success()
.into_result_with_error_logging(&logger)
.map_err(|e| {
format!(
"Failed to query present files on server {}: {e}",
server.get_name()
)
})?
.stdout
.split(b'\n')
.into_iter()
.map(OsString::from)
@ -452,32 +463,37 @@ where
for file_action in server_actions.actions {
match file_action.kind {
Action::Add | Action::Replace => {
let scp_source = match &file_server {
Some(file_server) => {
osf!(match &file_server.address {
ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"),
ServerAddress::Localhost => "".to_string(),
}) + file_server.server_directory_path.join(&file_action.file)
}
None => osf!(&file_action.file),
let source = match &file_server {
Some(file_server) => ScpParam::from((
file_server,
file_server.server_directory_path.join(&file_action.file),
)),
None => ScpParam::from(file_action.file.as_path()),
};
let scp_target = osf!(match &server.address {
ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"),
ServerAddress::Localhost => "".to_string(),
}) + &server_actions.working_directory;
ShellCmd::new("scp")
.arg(scp_source)
.arg(scp_target)
.run(&logger)
.map_err(|e| format!("upload failure: {e}"))?;
let destination = ScpParam::from((server, &server_actions.working_directory));
ShellCommand::Scp {
source,
destination,
}
.in_env(env!())
.run_logged(&logger)
.and_expect_success()
.into_result_with_error_logging(&logger)
.map_err(|e| format!("upload failure: {e}"))?;
}
Action::Delete => match &server.address {
ServerAddress::Ssh { ssh_address } => {
ShellCmd::new("ssh")
.arg(ssh_address)
.arg(osf!("rm ") + server_actions.working_directory.join(&file_action.file))
.run(&logger)
.map_err(|e| format!("failed to delete old version: {e}"))?;
ShellCommand::Ssh {
address: ssh_address.to_string(),
server_command: ServerCommand::Rm {
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}"))?;
}
ServerAddress::Localhost => {
fs::remove_file(server_actions.working_directory.join(&file_action.file))
@ -486,16 +502,18 @@ where
},
Action::Rename { new_name } => match &server.address {
ServerAddress::Ssh { ssh_address } => {
ShellCmd::new("ssh")
.arg(ssh_address)
.arg(
osf!("mv ")
+ server_actions.working_directory.join(&file_action.file)
+ " "
+ server_actions.working_directory.join(&new_name),
)
.run(&logger)
.map_err(|e| format!("failed to rename: {e}"))?;
ShellCommand::Ssh {
address: ssh_address.to_string(),
server_command: ServerCommand::Mv {
source: server_actions.working_directory.join(&file_action.file),
destination: server_actions.working_directory.join(&new_name),
},
}
.in_env(env!())
.run_logged(&logger)
.and_expect_success()
.into_result_with_error_logging(&logger)
.map_err(|e| format!("failed to rename: {e}"))?;
}
ServerAddress::Localhost => {
let dir = &server_actions.working_directory;
@ -516,20 +534,33 @@ where
log!(logger, "Running command on '{}'...", server.get_name());
match &server.address {
ServerAddress::Ssh { ssh_address } => {
ShellCmd::new("ssh")
.arg(ssh_address)
.arg(osf!("cd ") + server.server_directory_path + "; " + &command)
.run(&logger)
.map_err(|e| format!("{e}"))?;
ShellCommand::Ssh {
address: ssh_address.to_string(),
server_command: ServerCommand::Execute {
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}"))?;
}
ServerAddress::Localhost => {
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)
.run(&logger)
.map_err(|e| format!("{e}"))?;
let command = shell_words::split(&command)
.map_err(|e| format!("failed to parse command: {e}"))?
.into_iter()
.map(OsString::from)
.collect();
ShellCommand::Execute {
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 {
log!(logger, "Getting file from {}...", server.get_name());
let file_source = osf!(match &server.address {
ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"),
ServerAddress::Localhost => "".to_string(),
}) + server.server_directory_path.join(&file);
ShellCmd::new("scp")
.arg(&file_source)
.arg(&download_directory)
.run(&logger)
.map_err(|e| format!("download failure: {e}"))?;
let source = ScpParam::from((&server, server.server_directory_path.join(&file)));
ShellCommand::Scp {
source: source.clone(),
destination: ScpParam::from(download_directory.as_path()),
}
.in_env(env!())
.run_logged(&logger)
.and_expect_success()
.into_result_with_error_logging(&logger)
.map_err(|e| format!("download failure: {e}"))?;
//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}"))?
.into_iter()
.map(|part| {
@ -616,18 +648,23 @@ where
})
.collect::<Vec<_>>();
let command = editor_command_args.remove(0);
ShellCmd::new(command)
.args(editor_command_args)
.run(&logger)
ShellCommand::Editor(editor_command)
.in_env(env!())
.run_logged(&logger)
.and_expect_success()
.into_result_with_error_logging(&logger)
.map_err(|e| format!("failed to open file in editor: {e}"))?;
//upload file again
ShellCmd::new("scp")
.arg(download_directory.join(file_name))
.arg(&file_source)
.run(&logger)
.map_err(|e| format!("failed to re-upload file: {e}"))?;
ShellCommand::Scp {
source: ScpParam::from(download_directory.join(file_name).as_path()),
destination: source,
}
.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}"))?;
}
log!(logger, "Done!");
@ -669,25 +706,30 @@ where
let env = &mut self.environment;
//start the ssh agent
let agent_output = ShellCmd::new("ssh-agent")
.arg("-s")
.collect_output()
.map_err(|e| format!("failed to start ssh agent: {e}"))?;
let agent_stdout = String::from_utf8_lossy(&agent_output.stdout);
if !agent_output.status.success() {
return Err("failed to start ssh agent; maybe try to run ssh-agent manually?".to_string());
}
let agent_output = ShellCommand::SshAgent
.in_env(env)
.output()
.and_expect_success()
.into_result_with_error_logging(logger)
.map_err(|e| format!("Failed to start ssh agent: {e}"))?;
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
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();
env.set_var(env_var, value);
}
//add the ssh key
ShellCmd::new("ssh-add")
.run(logger)
ShellCommand::ShhAdd
.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}"))?;
Ok(())
}

View File

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