agentbridge/
subprocess.rs1use std::io;
16use std::process::{Command, Output};
17use std::sync::Mutex;
18
19#[derive(Default)]
22pub struct TrackedSubprocess {
23 active_pid: Mutex<Option<u32>>,
24}
25
26impl TrackedSubprocess {
27 pub fn new() -> Self {
28 Self::default()
29 }
30
31 pub fn output(&self, cmd: &mut Command) -> io::Result<Output> {
35 let child = cmd.spawn()?;
36
37 if let Ok(mut active) = self.active_pid.lock() {
38 *active = Some(child.id());
39 }
40
41 let result = child.wait_with_output();
42
43 if let Ok(mut active) = self.active_pid.lock() {
44 *active = None;
45 }
46
47 result
48 }
49
50 pub fn kill(&self) {
53 let pid = match self.active_pid.lock() {
54 Ok(active) => *active,
55 Err(_) => None,
56 };
57 if let Some(pid) = pid {
58 terminate(pid);
59 }
60 }
61}
62
63#[cfg(unix)]
65fn terminate(pid: u32) {
66 unsafe {
71 libc::kill(pid as libc::pid_t, libc::SIGTERM);
72 }
73}
74
75#[cfg(windows)]
78fn terminate(pid: u32) {
79 use windows_sys::Win32::Foundation::CloseHandle;
80 use windows_sys::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
81
82 unsafe {
86 let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
87 if !handle.is_null() {
88 TerminateProcess(handle, 1);
89 CloseHandle(handle);
90 }
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use std::sync::Arc;
98
99 #[cfg(unix)]
101 fn exits_now() -> Command {
102 Command::new("true")
103 }
104
105 #[cfg(windows)]
106 fn exits_now() -> Command {
107 let mut cmd = Command::new("cmd");
108 cmd.args(["/C", "exit", "0"]);
109 cmd
110 }
111
112 #[cfg(unix)]
114 fn stays_alive() -> Command {
115 let mut cmd = Command::new("sleep");
116 cmd.arg("30");
117 cmd
118 }
119
120 #[cfg(windows)]
121 fn stays_alive() -> Command {
122 let mut cmd = Command::new("ping");
123 cmd.args(["-n", "31", "127.0.0.1"]);
124 cmd
125 }
126
127 #[test]
128 fn output_runs_command_and_clears_pid_after() {
129 let tracked = TrackedSubprocess::new();
130 let mut cmd = exits_now();
131 let output = tracked.output(&mut cmd).expect("spawn should succeed");
132 assert!(output.status.success());
133 assert!(tracked.active_pid.lock().unwrap().is_none());
134 }
135
136 #[test]
137 fn output_surfaces_spawn_failure_without_stranding_a_pid() {
138 let tracked = TrackedSubprocess::new();
139 let mut cmd = Command::new("agentbridge-no-such-binary");
140 assert!(tracked.output(&mut cmd).is_err());
141 assert!(tracked.active_pid.lock().unwrap().is_none());
142 }
143
144 #[test]
145 fn kill_on_idle_tracker_is_a_harmless_no_op() {
146 let tracked = TrackedSubprocess::new();
147 tracked.kill(); }
149
150 #[test]
151 fn kill_terminates_a_running_child() {
152 let tracked = TrackedSubprocess::new();
153 let mut child = stays_alive().spawn().expect("spawn long-running child");
154 let pid = child.id();
155 *tracked
156 .active_pid
157 .lock()
158 .expect("fresh tracker is not poisoned") = Some(pid);
159 tracked.kill();
160 let status = child.wait().expect("wait after kill");
161 assert!(!status.success());
162 }
163
164 #[test]
165 fn a_poisoned_tracker_still_runs_and_still_shuts_down() {
166 let tracked = Arc::new(TrackedSubprocess::new());
167 let poisoner = Arc::clone(&tracked);
168 let _ = std::thread::spawn(move || {
169 let _held = poisoner.active_pid.lock().unwrap();
170 panic!("poison the tracker's lock");
171 })
172 .join();
173 assert!(
174 tracked.active_pid.lock().is_err(),
175 "lock should be poisoned"
176 );
177
178 let mut cmd = exits_now();
181 let output = tracked.output(&mut cmd).expect("command still runs");
182 assert!(output.status.success());
183 tracked.kill();
184 }
185}