use crate::process::termination::TrackedProcessGroup;
use crate::process::{WaitTimeout as _, configure_process_group, kill_process_tree, termination};
pub(crate) enum Deadline {
Exited(std::process::ExitStatus),
Expired,
}
pub(crate) struct GroupChild {
child: std::process::Child,
_tracked: TrackedProcessGroup,
}
impl GroupChild {
pub(crate) fn spawn(command: &mut std::process::Command) -> std::io::Result<Self> {
configure_process_group(command);
let child = command.spawn()?;
let tracked = termination::track(&child);
Ok(Self {
child,
_tracked: tracked,
})
}
pub(crate) fn take_stdout(&mut self) -> Option<std::process::ChildStdout> {
self.child.stdout.take()
}
pub(crate) fn take_stderr(&mut self) -> Option<std::process::ChildStderr> {
self.child.stderr.take()
}
pub(crate) fn kill_tree(&mut self) {
kill_process_tree(&mut self.child);
let _ = self.child.wait();
}
pub(crate) fn wait_within(
&mut self,
budget: std::time::Duration,
command: &impl std::fmt::Debug,
) -> std::io::Result<Deadline> {
match self.child.wait_timeout(budget) {
Ok(Some(status)) => Ok(Deadline::Exited(status)),
Ok(None) => {
tracing::warn!(
command = ?command,
budget_seconds = budget.as_secs(),
"command exceeded its timeout; killing its process group"
);
self.kill_tree();
Ok(Deadline::Expired)
}
Err(error) => {
self.kill_tree();
Err(error)
}
}
}
}
#[cfg(all(test, unix))]
mod tests {
use super::{Deadline, GroupChild};
use std::time::Duration;
const SETTLE_POLL: Duration = Duration::from_millis(20);
const SETTLE_LIMIT: Duration = Duration::from_secs(5);
fn is_alive(pid: i32) -> bool {
unsafe { libc::kill(pid, 0) == 0 }
}
fn wait_until_gone(pid: i32) -> bool {
let deadline = std::time::Instant::now() + SETTLE_LIMIT;
while std::time::Instant::now() < deadline {
if !is_alive(pid) {
return true;
}
std::thread::sleep(SETTLE_POLL);
}
!is_alive(pid)
}
fn spawn_with_grandchild(directory: &std::path::Path) -> (GroupChild, i32) {
let marker = directory.join("grandchild.pid");
let mut command = std::process::Command::new("sh");
command.args(["-c", &format!("sleep 60 & echo $! > {}; sleep 60", marker.display())]);
let child = GroupChild::spawn(&mut command).expect("spawn the process group");
let deadline = std::time::Instant::now() + SETTLE_LIMIT;
let grandchild = loop {
assert!(
std::time::Instant::now() < deadline,
"grandchild never announced itself"
);
if let Ok(contents) = std::fs::read_to_string(&marker)
&& let Ok(pid) = contents.trim().parse::<i32>()
&& is_alive(pid)
{
break pid;
}
std::thread::sleep(SETTLE_POLL);
};
(child, grandchild)
}
#[test]
fn an_expired_deadline_kills_the_grandchild_too() {
let directory = tempfile::tempdir().expect("scratch directory");
let (mut child, grandchild) = spawn_with_grandchild(directory.path());
let outcome = child
.wait_within(Duration::from_millis(200), &"sleep 60 & sleep 60")
.expect("waiting on a live child is not an error");
assert!(matches!(outcome, Deadline::Expired));
assert!(
wait_until_gone(grandchild),
"grandchild {grandchild} survived its parent's deadline"
);
}
#[test]
fn a_child_that_beats_its_deadline_reports_its_own_exit_status() {
let mut command = std::process::Command::new("sh");
command.args(["-c", "exit 7"]);
let mut child = GroupChild::spawn(&mut command).expect("spawn the process group");
let outcome = child
.wait_within(Duration::from_secs(30), &"exit 7")
.expect("waiting on a live child is not an error");
match outcome {
Deadline::Exited(status) => assert_eq!(status.code(), Some(7)),
Deadline::Expired => panic!("a command that exits immediately must not report expiry"),
}
}
}