use std::{ ffi::OsStr, fmt::Debug, io::{Error, ErrorKind, Result}, process::{Command, Stdio}, time::{Duration, Instant}, }; use process_control::{ChildExt, Control}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CommandOutput { pub stdout: String, pub stderr: String, } /// # Attempt to resolve `binary_name` from and creates a new `Command` pointing at it /// # This allows executing cmd files on Windows and prevents running executable from cwd on Windows /// # This function also initializes std{err,out,in} to protect against processes changing the console mode /// # /// # Errors /// fn create_command>(binary_name: T) -> Result { let binary_name = binary_name.as_ref(); log::trace!("Creating Command for binary {:?}", binary_name); let full_path = match which::which(binary_name) { Ok(full_path) => { log::trace!("Using {:?} as {:?}", full_path, binary_name); full_path } Err(error) => { log::trace!("Unable to find {:?} in PATH, {:?}", binary_name, error); return Err(Error::new(ErrorKind::NotFound, error)); } }; let mut cmd = Command::new(full_path); cmd.stderr(Stdio::piped()) .stdout(Stdio::piped()) .stdin(Stdio::null()); Ok(cmd) } /// Execute a command and return the output on stdout and stderr if successful pub fn exec_cmd + Debug, U: AsRef + Debug>( cmd: T, args: &[U], time_limit: Duration, ) -> Option { log::trace!("Executing command {:?} with args {:?}", cmd, args); internal_exec_cmd(cmd, args, time_limit) } fn internal_exec_cmd + Debug, U: AsRef + Debug>( cmd: T, args: &[U], time_limit: Duration, ) -> Option { let mut cmd = create_command(cmd).ok()?; cmd.args(args); exec_timeout(&mut cmd, time_limit) } fn exec_timeout(cmd: &mut Command, time_limit: Duration) -> Option { let start = Instant::now(); let process = match cmd.spawn() { Ok(process) => process, Err(error) => { log::trace!("Unable to run {:?}, {:?}", cmd.get_program(), error); return None; } }; match process .controlled_with_output() .time_limit(time_limit) .terminate_for_timeout() .wait() { Ok(Some(output)) => { let stdout_string = match String::from_utf8(output.stdout) { Ok(stdout) => stdout, Err(error) => { log::warn!("Unable to decode stdout: {:?}", error); return None; } }; let stderr_string = match String::from_utf8(output.stderr) { Ok(stderr) => stderr, Err(error) => { log::warn!("Unable to decode stderr: {:?}", error); return None; } }; log::trace!( "stdout: {:?}, stderr: {:?}, exit code: \"{:?}\", took {:?}", stdout_string, stderr_string, output.status.code(), start.elapsed() ); if !output.status.success() { return None; } Some(CommandOutput { stdout: stdout_string, stderr: stderr_string, }) } Ok(None) => { log::warn!("Executing command {:?} timed out.", cmd.get_program()); log::warn!("You can set command_timeout in your config to a higher value to allow longer-running commands to keep executing."); None } Err(error) => { log::trace!( "Executing command {:?} failed by: {:?}", cmd.get_program(), error ); None } } }