#![cfg(unix)]
use std::{
fs,
num::NonZeroU32,
path::Path,
process::{Command, Stdio},
thread,
time::{Duration, Instant},
};
use rightkit_process::{AdoptedProcess, OwnedCommand, WaitOutcome};
fn shell(script: &str) -> Command {
let mut command = Command::new("/bin/sh");
command.args(["-c", script]);
command
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
command
}
fn wait_for_pid(path: &Path) -> u32 {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
if let Ok(text) = fs::read_to_string(path) {
if let Ok(pid) = text.trim().parse() {
return pid;
}
}
assert!(Instant::now() < deadline, "child PID file was not created");
thread::sleep(Duration::from_millis(20));
}
}
fn tree_command(pid_file: &Path) -> Command {
let quoted = pid_file.display().to_string().replace('\'', "'\\''");
shell(&format!(
"sleep 60 & child=$!; printf '%s' \"$child\" > '{quoted}'; wait $child"
))
}
fn unkillable_tree_command(pid_file: &Path) -> Command {
let quoted = pid_file.display().to_string().replace('\'', "'\\''");
shell(&format!(
"trap '' TERM; /bin/sh -c 'trap \"\" TERM; sleep 60' & child=$!; \
printf '%s' \"$child\" > '{quoted}'; wait $child"
))
}
fn wait_until_stopped(process: &AdoptedProcess) {
let deadline = Instant::now() + Duration::from_secs(5);
while process.is_running().unwrap_or(false) {
assert!(
Instant::now() < deadline,
"process {} remained alive",
process.id()
);
thread::sleep(Duration::from_millis(20));
}
}
fn temp_pid_file(tag: &str) -> std::path::PathBuf {
let path = std::env::temp_dir().join(format!("rightkit-process-{tag}-{}", std::process::id()));
let _ = fs::remove_file(&path);
path
}
#[test]
fn terminating_owned_tree_kills_parent_and_grandchild_but_not_sibling() {
let temp = temp_pid_file("tree");
let mut sibling = shell("sleep 60").spawn().unwrap();
let sibling_pid = NonZeroU32::new(sibling.id()).unwrap();
let mut owned = OwnedCommand::from_command(tree_command(&temp))
.spawn()
.unwrap();
let parent = AdoptedProcess::new(NonZeroU32::new(owned.id()).unwrap());
let grandchild = AdoptedProcess::new(NonZeroU32::new(wait_for_pid(&temp)).unwrap());
owned.terminate_tree().unwrap();
wait_until_stopped(&parent);
wait_until_stopped(&grandchild);
assert!(AdoptedProcess::new(sibling_pid).is_running().unwrap());
sibling.kill().unwrap();
sibling.wait().unwrap();
let _ = fs::remove_file(temp);
}
#[test]
fn graceful_exit_does_not_report_a_forced_kill() {
let mut child = OwnedCommand::from_command(shell("sleep 0.05"))
.spawn()
.unwrap();
assert!(matches!(
child.wait_or_kill(Duration::from_secs(2)).unwrap(),
WaitOutcome::Exited(_)
));
}
#[test]
fn sigterm_shutdown_takes_the_grandchild_down_too() {
let temp = temp_pid_file("sigterm");
let mut child = OwnedCommand::from_command(tree_command(&temp))
.spawn()
.unwrap();
let grandchild = AdoptedProcess::new(NonZeroU32::new(wait_for_pid(&temp)).unwrap());
assert!(matches!(
child.wait_or_kill(Duration::from_secs(2)).unwrap(),
WaitOutcome::Exited(_)
));
wait_until_stopped(&grandchild);
let _ = fs::remove_file(temp);
}
#[test]
fn grace_expiry_terminates_the_whole_tree() {
let temp = temp_pid_file("grace");
let mut child = OwnedCommand::from_command(unkillable_tree_command(&temp))
.spawn()
.unwrap();
let grandchild = AdoptedProcess::new(NonZeroU32::new(wait_for_pid(&temp)).unwrap());
assert!(matches!(
child.wait_or_kill(Duration::from_millis(50)).unwrap(),
WaitOutcome::Terminated(_)
));
wait_until_stopped(&grandchild);
let _ = fs::remove_file(temp);
}
#[test]
fn dropping_owned_child_leaves_no_orphan() {
let temp = temp_pid_file("drop");
let grandchild = {
let mut child = OwnedCommand::from_command(tree_command(&temp))
.spawn()
.unwrap();
let pid = wait_for_pid(&temp);
assert!(child.try_wait().unwrap().is_none());
AdoptedProcess::new(NonZeroU32::new(pid).unwrap())
};
wait_until_stopped(&grandchild);
let _ = fs::remove_file(temp);
}