use std::io;
use std::process::{Command, Output};
use std::sync::Mutex;
#[derive(Default)]
pub struct TrackedSubprocess {
active_pid: Mutex<Option<u32>>,
}
impl TrackedSubprocess {
pub fn new() -> Self {
Self::default()
}
pub fn output(&self, cmd: &mut Command) -> io::Result<Output> {
let child = cmd.spawn()?;
if let Ok(mut active) = self.active_pid.lock() {
*active = Some(child.id());
}
let result = child.wait_with_output();
if let Ok(mut active) = self.active_pid.lock() {
*active = None;
}
result
}
pub fn kill(&self) {
let pid = match self.active_pid.lock() {
Ok(active) => *active,
Err(_) => None,
};
if let Some(pid) = pid {
terminate(pid);
}
}
}
#[cfg(unix)]
fn terminate(pid: u32) {
unsafe {
libc::kill(pid as libc::pid_t, libc::SIGTERM);
}
}
#[cfg(windows)]
fn terminate(pid: u32) {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
if !handle.is_null() {
TerminateProcess(handle, 1);
CloseHandle(handle);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[cfg(unix)]
fn exits_now() -> Command {
Command::new("true")
}
#[cfg(windows)]
fn exits_now() -> Command {
let mut cmd = Command::new("cmd");
cmd.args(["/C", "exit", "0"]);
cmd
}
#[cfg(unix)]
fn stays_alive() -> Command {
let mut cmd = Command::new("sleep");
cmd.arg("30");
cmd
}
#[cfg(windows)]
fn stays_alive() -> Command {
let mut cmd = Command::new("ping");
cmd.args(["-n", "31", "127.0.0.1"]);
cmd
}
#[test]
fn output_runs_command_and_clears_pid_after() {
let tracked = TrackedSubprocess::new();
let mut cmd = exits_now();
let output = tracked.output(&mut cmd).expect("spawn should succeed");
assert!(output.status.success());
assert!(tracked.active_pid.lock().unwrap().is_none());
}
#[test]
fn output_surfaces_spawn_failure_without_stranding_a_pid() {
let tracked = TrackedSubprocess::new();
let mut cmd = Command::new("agentbridge-no-such-binary");
assert!(tracked.output(&mut cmd).is_err());
assert!(tracked.active_pid.lock().unwrap().is_none());
}
#[test]
fn kill_on_idle_tracker_is_a_harmless_no_op() {
let tracked = TrackedSubprocess::new();
tracked.kill(); }
#[test]
fn kill_terminates_a_running_child() {
let tracked = TrackedSubprocess::new();
let mut child = stays_alive().spawn().expect("spawn long-running child");
let pid = child.id();
*tracked
.active_pid
.lock()
.expect("fresh tracker is not poisoned") = Some(pid);
tracked.kill();
let status = child.wait().expect("wait after kill");
assert!(!status.success());
}
#[test]
fn a_poisoned_tracker_still_runs_and_still_shuts_down() {
let tracked = Arc::new(TrackedSubprocess::new());
let poisoner = Arc::clone(&tracked);
let _ = std::thread::spawn(move || {
let _held = poisoner.active_pid.lock().unwrap();
panic!("poison the tracker's lock");
})
.join();
assert!(
tracked.active_pid.lock().is_err(),
"lock should be poisoned"
);
let mut cmd = exits_now();
let output = tracked.output(&mut cmd).expect("command still runs");
assert!(output.status.success());
tracked.kill();
}
}