use std::{
path::{
Path,
PathBuf,
},
time::Duration,
};
pub(crate) mod captured_output;
pub(crate) mod command_io;
pub(crate) mod error_mapping;
pub(crate) mod finished_command;
pub(crate) mod managed_child_process;
pub(crate) mod output_capture_error;
pub(crate) mod output_capture_options;
pub(crate) mod output_collector;
pub(crate) mod output_reader;
pub(crate) mod output_tee;
pub(crate) mod prepared_command;
pub(crate) mod process_launcher;
pub(crate) mod process_setup;
pub(crate) mod running_command;
pub(crate) mod stdin_pipe;
pub(crate) mod stdin_writer;
pub(crate) mod wait_policy;
use command_io::CommandIo;
use error_mapping::{
output_pipe_error,
spawn_failed,
};
use finished_command::FinishedCommand;
use output_capture_options::OutputCaptureOptions;
use output_collector::read_output_stream;
use prepared_command::PreparedCommand;
use process_launcher::spawn_child;
use running_command::RunningCommand;
use stdin_pipe::write_stdin_bytes;
use crate::{
Command,
CommandError,
CommandOutput,
OutputStream,
};
pub const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandRunner {
timeout: Option<Duration>,
working_directory: Option<PathBuf>,
success_exit_codes: Vec<i32>,
disable_logging: bool,
lossy_output: bool,
max_stdout_bytes: Option<usize>,
max_stderr_bytes: Option<usize>,
stdout_file: Option<PathBuf>,
stderr_file: Option<PathBuf>,
}
impl Default for CommandRunner {
#[inline]
fn default() -> Self {
Self {
timeout: None,
working_directory: None,
success_exit_codes: vec![0],
disable_logging: false,
lossy_output: false,
max_stdout_bytes: None,
max_stderr_bytes: None,
stdout_file: None,
stderr_file: None,
}
}
}
impl CommandRunner {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub const fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
#[inline]
pub const fn without_timeout(mut self) -> Self {
self.timeout = None;
self
}
#[inline]
pub fn working_directory<P>(mut self, working_directory: P) -> Self
where
P: Into<PathBuf>,
{
self.working_directory = Some(working_directory.into());
self
}
#[inline]
pub fn success_exit_code(mut self, exit_code: i32) -> Self {
self.success_exit_codes = vec![exit_code];
self
}
#[inline]
pub fn success_exit_codes(mut self, exit_codes: &[i32]) -> Self {
self.success_exit_codes = exit_codes.to_vec();
self
}
#[inline]
pub const fn disable_logging(mut self, disable_logging: bool) -> Self {
self.disable_logging = disable_logging;
self
}
#[inline]
pub const fn lossy_output(mut self, lossy_output: bool) -> Self {
self.lossy_output = lossy_output;
self
}
#[inline]
pub const fn max_stdout_bytes(mut self, max_bytes: usize) -> Self {
self.max_stdout_bytes = Some(max_bytes);
self
}
#[inline]
pub const fn max_stderr_bytes(mut self, max_bytes: usize) -> Self {
self.max_stderr_bytes = Some(max_bytes);
self
}
#[inline]
pub const fn max_output_bytes(mut self, max_bytes: usize) -> Self {
self.max_stdout_bytes = Some(max_bytes);
self.max_stderr_bytes = Some(max_bytes);
self
}
#[inline]
pub fn tee_stdout_to_file<P>(mut self, path: P) -> Self
where
P: Into<PathBuf>,
{
self.stdout_file = Some(path.into());
self
}
#[inline]
pub fn tee_stderr_to_file<P>(mut self, path: P) -> Self
where
P: Into<PathBuf>,
{
self.stderr_file = Some(path.into());
self
}
#[inline]
pub const fn configured_timeout(&self) -> Option<Duration> {
self.timeout
}
#[inline]
pub fn configured_working_directory(&self) -> Option<&Path> {
self.working_directory.as_deref()
}
#[inline]
pub fn configured_success_exit_codes(&self) -> &[i32] {
&self.success_exit_codes
}
#[inline]
pub const fn is_logging_disabled(&self) -> bool {
self.disable_logging
}
#[inline]
pub const fn is_lossy_output_enabled(&self) -> bool {
self.lossy_output
}
#[inline]
pub const fn configured_max_stdout_bytes(&self) -> Option<usize> {
self.max_stdout_bytes
}
#[inline]
pub const fn configured_max_stderr_bytes(&self) -> Option<usize> {
self.max_stderr_bytes
}
#[inline]
pub fn configured_stdout_file(&self) -> Option<&Path> {
self.stdout_file.as_deref()
}
#[inline]
pub fn configured_stderr_file(&self) -> Option<&Path> {
self.stderr_file.as_deref()
}
pub fn run(&self, command: Command) -> Result<CommandOutput, CommandError> {
let PreparedCommand {
command_text,
process_command,
stdin_bytes,
stdout_file,
stderr_file,
stdout_file_path,
stderr_file_path,
} = PreparedCommand::prepare(
command,
self.working_directory.as_deref(),
self.stdout_file.as_deref(),
self.stderr_file.as_deref(),
)?;
if !self.disable_logging {
log::info!("Running command: {command_text}");
}
let mut child_process = match spawn_child(process_command, self.timeout.is_some()) {
Ok(child_process) => child_process,
Err(source) => return Err(spawn_failed(&command_text, source)),
};
let stdin_writer = write_stdin_bytes(&command_text, child_process.as_mut(), stdin_bytes)?;
let stdout = match child_process.stdout().take() {
Some(stdout) => stdout,
None => return Err(output_pipe_error(&command_text, OutputStream::Stdout)),
};
let stderr = match child_process.stderr().take() {
Some(stderr) => stderr,
None => return Err(output_pipe_error(&command_text, OutputStream::Stderr)),
};
let stdout_reader = read_output_stream(
Box::new(stdout),
OutputCaptureOptions::new(self.max_stdout_bytes, stdout_file, stdout_file_path),
);
let stderr_reader = read_output_stream(
Box::new(stderr),
OutputCaptureOptions::new(self.max_stderr_bytes, stderr_file, stderr_file_path),
);
let command_io = CommandIo::new(stdout_reader, stderr_reader, stdin_writer);
let finished =
RunningCommand::new(command_text, child_process, command_io, self.lossy_output)
.wait_for_completion(self.timeout)?;
let FinishedCommand {
command_text,
output,
} = finished;
if output
.exit_code()
.is_some_and(|exit_code| self.success_exit_codes.contains(&exit_code))
{
if !self.disable_logging {
log::info!(
"Finished command `{}` in {:?}.",
command_text,
output.elapsed()
);
}
Ok(output)
} else {
if !self.disable_logging {
log::error!(
"Command `{}` exited with code {:?}.",
command_text,
output.exit_code()
);
}
Err(CommandError::UnexpectedExit {
command: command_text,
exit_code: output.exit_code(),
expected: self.success_exit_codes.clone(),
output: Box::new(output),
})
}
}
}