#![warn(clippy::pedantic)]
#![allow(clippy::redundant_else)]
use std::ffi::OsStr;
use std::process::{Child, Command, Stdio};
use std::thread::sleep;
use std::time::{Duration, Instant};
use anyhow::Context;
use camino::Utf8Path;
use serde::Serialize;
use tracing::{Level, debug, span, trace};
use crate::Result;
use crate::console::Console;
use crate::interrupt::check_interrupted;
use crate::output::ScenarioOutput;
const WAIT_POLL_INTERVAL: Duration = Duration::from_millis(50);
#[cfg(windows)]
mod windows;
#[cfg(windows)]
use windows::{configure_command, terminate_child};
#[cfg(unix)]
mod unix;
#[cfg(unix)]
use unix::{configure_command, terminate_child};
pub struct Process {
child: Child,
start: Instant,
timeout: Option<Duration>,
}
impl Process {
pub fn run(
argv: &[String],
env: &[(String, String)],
cwd: &Utf8Path,
timeout: Option<Duration>,
jobserver: Option<&jobserver::Client>,
scenario_output: &mut ScenarioOutput,
console: &Console,
) -> Result<Exit> {
let mut child = Process::start(argv, env, cwd, timeout, jobserver, scenario_output)?;
let process_status = loop {
if let Some(exit_status) = child.poll()? {
break exit_status;
}
console.tick();
sleep(WAIT_POLL_INTERVAL);
};
scenario_output.message(&format!("result: {process_status:?}"))?;
Ok(process_status)
}
pub fn start(
argv: &[String],
env: &[(String, String)],
cwd: &Utf8Path,
timeout: Option<Duration>,
jobserver: Option<&jobserver::Client>,
scenario_output: &mut ScenarioOutput,
) -> Result<Process> {
let start = Instant::now();
let quoted_argv = quote_argv(argv);
scenario_output.message("ed_argv)?;
debug!(%quoted_argv, "start process");
let os_env = env.iter().map(|(k, v)| (OsStr::new(k), OsStr::new(v)));
let mut command = Command::new(&argv[0]);
command
.args(&argv[1..])
.envs(os_env)
.stdin(Stdio::null())
.stdout(scenario_output.open_log_append()?)
.stderr(scenario_output.open_log_append()?)
.current_dir(cwd);
if let Some(js) = jobserver {
js.configure(&mut command);
}
configure_command(&mut command);
let child = command
.spawn()
.with_context(|| format!("failed to spawn {}", argv.join(" ")))?;
Ok(Process {
child,
start,
timeout,
})
}
#[mutants::skip] pub fn poll(&mut self) -> Result<Option<Exit>> {
if self.timeout.is_some_and(|t| self.start.elapsed() > t) {
debug!("timeout, terminating child process...",);
self.terminate()?;
Ok(Some(Exit::Timeout))
} else if let Err(e) = check_interrupted() {
debug!("interrupted, terminating child process...");
self.terminate()?;
Err(e)
} else if let Some(status) = self.child.try_wait()? {
Ok(Some(status.into()))
} else {
Ok(None)
}
}
#[mutants::skip] fn terminate(&mut self) -> Result<()> {
let _span = span!(Level::DEBUG, "terminate_child", pid = self.child.id()).entered();
debug!("terminating child process");
terminate_child(&mut self.child)?;
trace!("wait for child after termination");
match self.child.wait() {
Err(err) => debug!(?err, "Failed to wait for child after termination"),
Ok(exit) => debug!("terminated child exit status {exit:?}"),
}
Ok(())
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)]
pub enum Exit {
Success,
Failure(i32),
Timeout,
#[cfg(unix)]
Signalled(i32),
Other,
}
impl Exit {
pub fn is_success(self) -> bool {
self == Exit::Success
}
pub fn is_timeout(self) -> bool {
self == Exit::Timeout
}
pub fn is_failure(self) -> bool {
matches!(self, Exit::Failure(_))
}
}
fn quote_argv<S: AsRef<str>, I: IntoIterator<Item = S>>(argv: I) -> String {
let mut r = String::new();
for s in argv {
if !r.is_empty() {
r.push(' ');
}
for c in s.as_ref().chars() {
match c {
'\t' => r.push_str(r"\t"),
'\n' => r.push_str(r"\n"),
'\r' => r.push_str(r"\r"),
' ' | '\\' | '\'' | '"' => {
r.push('\\');
r.push(c);
}
_ => r.push(c),
}
}
}
r
}
#[cfg(test)]
mod test {
use super::quote_argv;
#[test]
fn shell_quoting() {
assert_eq!(quote_argv(["foo".to_string()]), "foo");
assert_eq!(
quote_argv(["foo bar", r"\blah\x", r#""quoted""#]),
r#"foo\ bar \\blah\\x \"quoted\""#
);
assert_eq!(quote_argv([""]), "");
assert_eq!(
quote_argv(["with whitespace", "\r\n\t\t"]),
r"with\ whitespace \r\n\t\t"
);
}
}