use std::io::{self, Read as _};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::time::Duration;
use wait_timeout::ChildExt;
pub(crate) struct Output {
pub status: ExitStatus,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
pub(crate) enum Run {
Completed(Output),
TimedOut,
}
pub(crate) fn run_process(cmd: &mut Command, timeout: Option<Duration>) -> io::Result<Run> {
match timeout {
None => run_blocking(cmd),
Some(timeout) => run_with_timeout(cmd, timeout),
}
}
fn run_blocking(cmd: &mut Command) -> io::Result<Run> {
let output = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).output()?;
Ok(Run::Completed(Output {
status: output.status,
stdout: output.stdout,
stderr: output.stderr,
}))
}
fn run_with_timeout(cmd: &mut Command, timeout: Duration) -> io::Result<Run> {
set_own_process_group(cmd);
let mut child = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?;
let mut stdout_pipe = child.stdout.take().expect("stdout piped");
let mut stderr_pipe = child.stderr.take().expect("stderr piped");
let stdout_thread = std::thread::spawn(move || {
let mut buf = Vec::new();
stdout_pipe.read_to_end(&mut buf).ok();
buf
});
let stderr_thread = std::thread::spawn(move || {
let mut buf = Vec::new();
stderr_pipe.read_to_end(&mut buf).ok();
buf
});
match child.wait_timeout(timeout)? {
Some(status) => {
let stdout = stdout_thread.join().unwrap_or_default();
let stderr = stderr_thread.join().unwrap_or_default();
Ok(Run::Completed(Output {
status,
stdout,
stderr,
}))
}
None => {
kill_child_tree(&mut child);
stdout_thread.join().ok();
stderr_thread.join().ok();
Ok(Run::TimedOut)
}
}
}
#[cfg(unix)]
fn set_own_process_group(cmd: &mut Command) {
use std::os::unix::process::CommandExt as _;
cmd.process_group(0);
}
#[cfg(not(unix))]
fn set_own_process_group(_cmd: &mut Command) {}
#[cfg(unix)]
fn kill_child_tree(child: &mut Child) {
use nix::sys::signal::{killpg, Signal};
use nix::unistd::Pid;
let pgid = Pid::from_raw(child.id() as i32);
let _ = killpg(pgid, Signal::SIGKILL);
child.wait().ok();
}
#[cfg(not(unix))]
fn kill_child_tree(child: &mut Child) {
child.kill().ok();
child.wait().ok();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analyzer::test_spawn_guard;
#[test]
fn no_timeout_runs_to_completion() {
let _lock = test_spawn_guard();
let mut cmd = Command::new("/bin/echo");
cmd.arg("hello");
match run_process(&mut cmd, None).unwrap() {
Run::Completed(out) => {
assert!(out.status.success());
assert_eq!(out.stdout, b"hello\n");
}
Run::TimedOut => panic!("must not time out when no timeout is set"),
}
}
#[test]
fn completes_within_timeout() {
let _lock = test_spawn_guard();
let mut cmd = Command::new("/bin/echo");
cmd.arg("ok");
match run_process(&mut cmd, Some(Duration::from_secs(10))).unwrap() {
Run::Completed(out) => {
assert!(out.status.success());
assert_eq!(out.stdout, b"ok\n");
}
Run::TimedOut => panic!("a fast process should complete before the deadline"),
}
}
#[test]
fn timeout_kills_slow_process() {
let _lock = test_spawn_guard();
let mut cmd = Command::new("sleep");
cmd.arg("60");
match run_process(&mut cmd, Some(Duration::from_secs(1))).unwrap() {
Run::TimedOut => {}
Run::Completed(_) => panic!("a slow process should have timed out"),
}
}
#[cfg(unix)]
#[test]
fn timeout_kills_whole_process_tree_promptly() {
use std::time::Instant;
let _lock = test_spawn_guard();
let mut cmd = Command::new("/bin/sh");
cmd.arg("-c").arg("sleep 30 | cat");
let start = Instant::now();
match run_process(&mut cmd, Some(Duration::from_secs(1))).unwrap() {
Run::TimedOut => {}
Run::Completed(_) => panic!("a slow process tree should have timed out"),
}
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_secs(5),
"timeout must fire promptly, not wait for orphaned grandchildren; took {elapsed:?}"
);
}
}