use std::time::{Duration, Instant};
const SETTLE_POLL: Duration = Duration::from_millis(20);
const SETTLE_LIMIT: Duration = Duration::from_secs(10);
const PROBE_BUDGET_SECS: u64 = 3;
const ORPHAN_PROBE_MARKER: &str = "ALEF_PIPELINE_ORPHAN_PROBE";
const ORPHAN_PROBE_NAME: &str = "cli::pipeline::helpers::timeout_tests::pipeline_orphan_probe_child";
fn is_alive(pid: i32) -> bool {
unsafe { libc::kill(pid, 0) == 0 }
}
fn wait_until_gone(pid: i32) -> bool {
let deadline = Instant::now() + SETTLE_LIMIT;
while Instant::now() < deadline {
if !is_alive(pid) {
return true;
}
std::thread::sleep(SETTLE_POLL);
}
!is_alive(pid)
}
fn announced_pid(marker: &std::path::Path) -> i32 {
let deadline = Instant::now() + SETTLE_LIMIT;
loop {
assert!(
Instant::now() < deadline,
"no pid was ever announced in the marker file"
);
if let Ok(contents) = std::fs::read_to_string(marker)
&& let Ok(pid) = contents.trim().parse::<i32>()
&& pid > 0
{
return pid;
}
std::thread::sleep(SETTLE_POLL);
}
}
fn script_leaking_a_grandchild(marker: &std::path::Path) -> String {
format!("sleep 300 & echo $! > {}; sleep 300", marker.display())
}
fn assert_timed_out(outcome: &anyhow::Error) {
let message = outcome.to_string();
assert!(
message.contains(&format!("timed out after {PROBE_BUDGET_SECS}s")),
"unexpected error: {message}"
);
}
#[test]
fn a_streamed_command_that_times_out_kills_its_grandchild_too() {
let directory = tempfile::tempdir().expect("scratch directory");
let marker = directory.path().join("grandchild.pid");
let script = script_leaking_a_grandchild(&marker);
let grandchild = std::thread::scope(|scope| {
let running = scope.spawn(|| {
super::run_command_streamed_with_cwd_and_timeout(&script, Some("probe"), Some(PROBE_BUDGET_SECS), None)
});
let grandchild = announced_pid(&marker);
let outcome = running.join().expect("the streamed command returns");
assert_timed_out(&outcome.expect_err("a 300s script under a short deadline must fail"));
grandchild
});
assert!(
wait_until_gone(grandchild),
"grandchild {grandchild} survived the deadline that killed its parent"
);
}
#[test]
fn a_captured_command_that_times_out_kills_its_grandchild_too() {
let directory = tempfile::tempdir().expect("scratch directory");
let marker = directory.path().join("grandchild.pid");
let script = script_leaking_a_grandchild(&marker);
let grandchild = std::thread::scope(|scope| {
let running = scope.spawn(|| super::run_command_captured_with_timeout(&script, Some(PROBE_BUDGET_SECS)));
let grandchild = announced_pid(&marker);
let outcome = running.join().expect("the captured command returns");
assert_timed_out(&outcome.expect_err("a 300s script under a short deadline must fail"));
grandchild
});
assert!(
wait_until_gone(grandchild),
"grandchild {grandchild} survived the deadline that killed its parent"
);
}
#[test]
fn a_leaked_descendant_holding_the_pipes_cannot_outlast_the_drain_grace() {
let grace = crate::process::capture::OUTPUT_DRAIN_GRACE;
let started = Instant::now();
let outcome = super::run_command_captured_with_timeout("sleep 300 & exit 0", Some(120));
let elapsed = started.elapsed();
assert!(outcome.is_ok(), "the command itself exits zero: {outcome:?}");
assert!(
elapsed < grace * 3,
"draining took {elapsed:?}, which is not bounded by the {grace:?} grace"
);
}
#[test]
#[ignore = "spawned as a subprocess by a_signalled_alef_does_not_orphan_a_pipeline_child_tree"]
fn pipeline_orphan_probe_child() {
let Ok(marker) = std::env::var(ORPHAN_PROBE_MARKER) else {
return;
};
let script = format!("echo $$ > {marker}; sleep 120");
let _ = super::run_command_streamed_with_cwd_and_timeout(&script, None, Some(120), None);
}
#[test]
fn a_signalled_alef_does_not_orphan_a_pipeline_child_tree() {
let directory = tempfile::tempdir().expect("scratch directory");
let marker = directory.path().join("group.pid");
let mut probe = std::process::Command::new(std::env::current_exe().expect("the test binary"))
.args(["--exact", ORPHAN_PROBE_NAME, "--ignored", "--test-threads=1"])
.env(ORPHAN_PROBE_MARKER, &marker)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn the probe");
let orphan = announced_pid(&marker);
assert!(is_alive(orphan), "the probe's child must be running before the signal");
let signalled = unsafe { libc::kill(probe.id().cast_signed(), libc::SIGINT) };
assert_eq!(signalled, 0, "signalling the probe");
probe.wait().expect("the probe exits");
let survived = !wait_until_gone(orphan);
if survived {
unsafe {
libc::kill(-orphan, libc::SIGKILL);
}
}
assert!(
!survived,
"child group {orphan} outlived the alef process that spawned it -- the tree was orphaned"
);
}