use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process::Command;
use std::time::{Duration, Instant};
use running_process_core::ContainedProcessGroup;
fn testbin_path(name: &str) -> PathBuf {
let output = Command::new(env!("CARGO"))
.args(["build", "-p", name, "--message-format=json"])
.stderr(std::process::Stdio::inherit())
.output()
.expect("failed to run cargo build");
assert!(
output.status.success(),
"`cargo build -p {name}` failed with status {}",
output.status,
);
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if !line.contains("\"compiler-artifact\"") || !line.contains(name) {
continue;
}
if let Ok(v) = serde_json::from_str::<serde_json::Value>(line) {
if v["reason"] == "compiler-artifact"
&& v["target"]["kind"]
.as_array()
.is_some_and(|a| a.iter().any(|k| k == "bin"))
{
if let Some(exe) = v["executable"].as_str() {
let p = PathBuf::from(exe);
assert!(p.exists(), "cargo reported {p:?} but it does not exist");
return p;
}
}
}
}
panic!("`cargo build -p {name}` succeeded but no binary artifact found in JSON output");
}
#[cfg(windows)]
fn is_pid_alive(pid: u32) -> bool {
unsafe {
let handle = winapi::um::processthreadsapi::OpenProcess(
winapi::um::winnt::PROCESS_QUERY_LIMITED_INFORMATION,
0,
pid,
);
if handle.is_null() {
return false;
}
let mut exit_code: u32 = 0;
let ok =
winapi::um::processthreadsapi::GetExitCodeProcess(handle, &mut exit_code as *mut u32);
winapi::um::handleapi::CloseHandle(handle);
if ok == 0 {
return false;
}
exit_code == 259
}
}
#[cfg(unix)]
fn is_pid_alive(pid: u32) -> bool {
unsafe {
let mut status: i32 = 0;
let ret = libc::waitpid(pid as i32, &mut status, libc::WNOHANG);
if ret == pid as i32 {
return false; }
}
unsafe { libc::kill(pid as i32, 0) == 0 }
}
fn wait_until_dead(pid: u32, timeout: Duration) {
let start = Instant::now();
while is_pid_alive(pid) {
if start.elapsed() > timeout {
panic!("PID {pid} still alive after {timeout:?}");
}
std::thread::sleep(Duration::from_millis(50));
}
}
#[cfg(windows)]
fn force_kill(pid: u32) {
unsafe {
let handle = winapi::um::processthreadsapi::OpenProcess(
winapi::um::winnt::PROCESS_TERMINATE,
0,
pid,
);
if !handle.is_null() {
winapi::um::processthreadsapi::TerminateProcess(handle, 1);
winapi::um::handleapi::CloseHandle(handle);
}
}
}
#[cfg(unix)]
fn force_kill(pid: u32) {
unsafe {
libc::kill(pid as i32, libc::SIGKILL);
}
}
fn parse_pid_line(line: &str, prefix: &str) -> Option<u32> {
line.strip_prefix(prefix)
.and_then(|s| s.trim().parse::<u32>().ok())
}
#[test]
fn test_contained_group_kills_on_drop() {
let sleeper = testbin_path("testbin-sleeper");
let group = ContainedProcessGroup::new().expect("create group");
let mut cmd1 = Command::new(&sleeper);
cmd1.stdout(std::process::Stdio::piped());
let child1 = group.spawn(&mut cmd1).expect("spawn 1");
let pid1 = child1.child.id();
let mut cmd2 = Command::new(&sleeper);
cmd2.stdout(std::process::Stdio::piped());
let child2 = group.spawn(&mut cmd2).expect("spawn 2");
let pid2 = child2.child.id();
std::thread::sleep(Duration::from_millis(200));
assert!(
is_pid_alive(pid1),
"child 1 (PID {pid1}) should be alive after spawn"
);
assert!(
is_pid_alive(pid2),
"child 2 (PID {pid2}) should be alive after spawn"
);
drop(group);
let timeout = Duration::from_secs(10);
wait_until_dead(pid1, timeout);
wait_until_dead(pid2, timeout);
}
#[test]
fn test_contained_group_kills_grandchildren() {
let sleeper = testbin_path("testbin-sleeper");
let spawner = testbin_path("testbin-spawner");
let group = ContainedProcessGroup::new().expect("create group");
let mut cmd = Command::new(&spawner);
cmd.arg("2").arg(&sleeper);
cmd.stdout(std::process::Stdio::piped());
let mut child = group.spawn(&mut cmd).expect("spawn spawner");
let stdout = child.child.stdout.take().expect("stdout");
let reader = BufReader::new(stdout);
let mut spawner_pid: Option<u32> = None;
let mut grandchild_pids: Vec<u32> = Vec::new();
let start = Instant::now();
for line in reader.lines() {
if start.elapsed() > Duration::from_secs(10) {
panic!("timed out reading spawner output");
}
let line = line.expect("read line");
if let Some(pid) = parse_pid_line(&line, "SPAWNER_PID=") {
spawner_pid = Some(pid);
} else if let Some(pid) = parse_pid_line(&line, "CHILD_PID=") {
grandchild_pids.push(pid);
} else if line.trim() == "READY" {
break;
}
}
let spawner_pid = spawner_pid.expect("spawner should print its PID");
assert_eq!(
grandchild_pids.len(),
2,
"spawner should have spawned 2 children"
);
assert!(is_pid_alive(spawner_pid), "spawner should be alive");
for &pid in &grandchild_pids {
assert!(is_pid_alive(pid), "grandchild {pid} should be alive");
}
drop(group);
let timeout = Duration::from_secs(10);
wait_until_dead(spawner_pid, timeout);
for &pid in &grandchild_pids {
wait_until_dead(pid, timeout);
}
}
#[test]
fn test_detached_survives_group_drop() {
let sleeper = testbin_path("testbin-sleeper");
let group = ContainedProcessGroup::new().expect("create group");
let mut cmd_contained = Command::new(&sleeper);
cmd_contained.stdout(std::process::Stdio::piped());
let contained = group.spawn(&mut cmd_contained).expect("spawn contained");
let contained_pid = contained.child.id();
let mut cmd_detached = Command::new(&sleeper);
cmd_detached.stdout(std::process::Stdio::piped());
let detached = group
.spawn_detached(&mut cmd_detached)
.expect("spawn detached");
let detached_pid = detached.child.id();
std::thread::sleep(Duration::from_millis(200));
assert!(
is_pid_alive(contained_pid),
"contained child should be alive"
);
assert!(is_pid_alive(detached_pid), "detached child should be alive");
drop(group);
wait_until_dead(contained_pid, Duration::from_secs(10));
std::thread::sleep(Duration::from_millis(500));
assert!(
is_pid_alive(detached_pid),
"detached child (PID {detached_pid}) should survive group drop"
);
force_kill(detached_pid);
wait_until_dead(detached_pid, Duration::from_secs(5));
}