1#[cfg(windows)]
9mod windows_imp {
10 use std::io;
11 use std::os::windows::io::{AsRawHandle, RawHandle};
12 use std::process::Child;
13 use windows::Win32::Foundation::{CloseHandle, HANDLE};
14 use windows::Win32::System::JobObjects::{
15 AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
16 SetInformationJobObject, TerminateJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
17 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
18 };
19
20 pub struct JobObject {
21 handle: Option<HANDLE>,
22 }
23
24 unsafe impl Send for JobObject {}
31 unsafe impl Sync for JobObject {}
32
33 impl JobObject {
34 pub fn new() -> io::Result<Self> {
35 let handle = unsafe { CreateJobObjectW(None, None) }
37 .map_err(|e| io::Error::other(format!("CreateJobObjectW: {e}")))?;
38 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
39 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
40 unsafe {
42 SetInformationJobObject(
43 handle,
44 JobObjectExtendedLimitInformation,
45 &info as *const _ as *const core::ffi::c_void,
46 std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
47 )
48 }
49 .map_err(|e| io::Error::other(format!("SetInformationJobObject: {e}")))?;
50 Ok(JobObject {
51 handle: Some(handle),
52 })
53 }
54
55 pub fn assign(&self, child: &Child) -> io::Result<()> {
56 self.assign_raw_handle(child.as_raw_handle())
57 }
58
59 pub fn assign_raw_handle(&self, handle: RawHandle) -> io::Result<()> {
62 let job = self
63 .handle
64 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
65 let child_handle = HANDLE(handle);
66 unsafe { AssignProcessToJobObject(job, child_handle) }
68 .map_err(|e| io::Error::other(format!("AssignProcessToJobObject: {e}")))
69 }
70
71 pub fn kill_group(&mut self) -> io::Result<()> {
74 let Some(handle) = self.handle.take() else {
75 return Ok(());
76 };
77 let res = unsafe { TerminateJobObject(handle, 1) }
79 .map_err(|e| io::Error::other(format!("TerminateJobObject: {e}")));
80 unsafe {
82 let _ = CloseHandle(handle);
83 }
84 res
85 }
86
87 pub fn release(&mut self) -> io::Result<()> {
92 let Some(handle) = self.handle.take() else {
93 return Ok(());
94 };
95 let info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
96 let cleared = unsafe {
98 SetInformationJobObject(
99 handle,
100 JobObjectExtendedLimitInformation,
101 &info as *const _ as *const core::ffi::c_void,
102 std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
103 )
104 };
105 match cleared {
106 Ok(()) => {
107 unsafe {
109 let _ = CloseHandle(handle);
110 }
111 Ok(())
112 }
113 Err(e) => {
114 self.handle = Some(handle);
115 Err(io::Error::other(format!("SetInformationJobObject: {e}")))
116 }
117 }
118 }
119
120 pub fn is_armed(&self) -> bool {
122 self.handle.is_some()
123 }
124 }
125
126 impl Drop for JobObject {
127 fn drop(&mut self) {
128 if let Some(handle) = self.handle.take() {
131 unsafe {
133 let _ = CloseHandle(handle);
134 }
135 }
136 }
137 }
138}
139
140#[cfg(unix)]
141mod unix_imp {
142 use nix::sys::signal;
143 use nix::unistd::Pid;
144 use std::io;
145 use std::os::unix::process::CommandExt;
146 use std::process::{Child, Command};
147
148 pub struct ProcessGroupGuard {
152 pid: Option<u32>,
153 }
154
155 impl ProcessGroupGuard {
156 pub fn new() -> io::Result<Self> {
158 Ok(ProcessGroupGuard { pid: None })
159 }
160
161 pub fn configure_command(&self, cmd: &mut Command) {
164 unsafe {
168 cmd.pre_exec(|| {
169 nix::unistd::setsid().map_err(|e| io::Error::from_raw_os_error(e as i32))?;
170 Ok(())
171 });
172 }
173 }
174
175 pub fn assign(&mut self, child: &Child) {
177 self.pid = Some(child.id());
178 }
179
180 pub fn assign_pid(&mut self, pid: u32) {
182 self.pid = Some(pid);
183 }
184
185 pub fn kill_group(&mut self) -> io::Result<()> {
191 let Some(pid) = self.pid else {
192 return Ok(());
193 };
194 let res = match signal::killpg(Pid::from_raw(pid as i32), signal::Signal::SIGKILL) {
195 Ok(()) => Ok(()),
196 Err(nix::errno::Errno::ESRCH) => Ok(()),
197 Err(e) => Err(io::Error::from_raw_os_error(e as i32)),
198 };
199 self.pid.take();
200 res
201 }
202
203 pub fn release(&mut self) -> io::Result<()> {
205 self.pid.take();
206 Ok(())
207 }
208
209 pub fn is_armed(&self) -> bool {
211 self.pid.is_some()
212 }
213 }
214
215 impl Drop for ProcessGroupGuard {
216 fn drop(&mut self) {
217 let _ = self.kill_group();
220 }
221 }
222}
223
224#[cfg(not(any(windows, unix)))]
225mod fallback_imp {
226 use std::io;
227 use std::process::Child;
228
229 pub struct JobObject;
231 impl JobObject {
232 pub fn new() -> io::Result<Self> {
233 Ok(JobObject)
234 }
235 pub fn assign(&self, _child: &Child) -> io::Result<()> {
236 Ok(())
237 }
238 pub fn assign_pid(&mut self, _pid: u32) {}
240 pub fn kill_group(&mut self) -> io::Result<()> {
242 Ok(())
243 }
244 pub fn release(&mut self) -> io::Result<()> {
246 Ok(())
247 }
248 pub fn is_armed(&self) -> bool {
250 false
251 }
252 }
253}
254
255#[cfg(windows)]
256pub use self::windows_imp::JobObject;
257
258#[cfg(unix)]
259pub use self::unix_imp::ProcessGroupGuard as JobObject;
260
261#[cfg(not(any(windows, unix)))]
262pub use self::fallback_imp::JobObject;
263
264#[cfg(all(test, windows))]
265mod tests {
266 use super::*;
267 use std::process::Command;
268 use std::time::Duration;
269
270 #[test]
274 fn dropping_job_kills_grandchild_tree() {
275 let mut child = Command::new("cmd.exe")
276 .args(["/c", "ping -n 60 127.0.0.1 >NUL"])
277 .spawn()
278 .expect("spawn cmd");
279 let pid = child.id();
280 {
281 let job = JobObject::new().expect("create job");
282 job.assign(&child).expect("assign child");
283 }
285 std::thread::sleep(Duration::from_millis(500));
286 let status = child.try_wait().expect("try_wait");
288 assert!(
289 status.is_some(),
290 "child pid {pid} should be killed by job close"
291 );
292 let _ = child.kill();
293 }
294
295 #[test]
299 fn release_lets_the_tree_outlive_the_job() {
300 let mut child = Command::new("cmd.exe")
301 .args(["/c", "ping -n 60 127.0.0.1 >NUL"])
302 .spawn()
303 .expect("spawn cmd");
304 {
305 let mut job = JobObject::new().expect("create job");
306 job.assign(&child).expect("assign child");
307 job.release().expect("release the job");
308 assert!(!job.is_armed(), "release must disarm the guard");
309 }
311 std::thread::sleep(Duration::from_millis(500));
312 let status = child.try_wait().expect("try_wait");
313 assert!(status.is_none(), "a released job must not kill the child");
314 let _ = Command::new("taskkill")
316 .args(["/F", "/T", "/PID", &child.id().to_string()])
317 .output();
318 let _ = child.wait();
319 }
320}
321
322#[cfg(all(test, unix))]
323mod tests {
324 use super::unix_imp::ProcessGroupGuard;
325 use std::process::Command;
326 use std::time::{Duration, Instant};
327
328 #[test]
333 fn dropping_process_group_kills_leader() {
334 let mut cmd = Command::new("sh");
335 cmd.args(["-c", "sleep 60"]);
336
337 let mut guard = ProcessGroupGuard::new().expect("create guard");
338 guard.configure_command(&mut cmd);
339
340 let mut child = cmd.spawn().expect("spawn sh");
341 let pid = child.id();
342
343 guard.assign(&child);
344
345 std::thread::sleep(Duration::from_millis(100));
347
348 assert!(
350 child.try_wait().expect("try_wait").is_none(),
351 "child should still be running"
352 );
353
354 drop(guard);
356
357 let deadline = Instant::now() + Duration::from_secs(1);
359 loop {
360 if child.try_wait().expect("try_wait").is_some() {
361 break;
362 }
363 assert!(
364 Instant::now() < deadline,
365 "child pid {pid} should be killed when the guard drops"
366 );
367 std::thread::sleep(Duration::from_millis(50));
368 }
369 }
370
371 #[test]
374 fn release_lets_the_group_outlive_the_guard() {
375 let mut cmd = Command::new("sh");
376 cmd.args(["-c", "sleep 60"]);
377 let mut guard = ProcessGroupGuard::new().expect("create guard");
378 guard.configure_command(&mut cmd);
379 let mut child = cmd.spawn().expect("spawn sh");
380 guard.assign(&child);
381 guard.release().expect("release the guard");
382 assert!(!guard.is_armed(), "release must disarm the guard");
383 drop(guard);
384 std::thread::sleep(Duration::from_millis(300));
385 let status = child.try_wait().expect("try_wait");
386 let _ = child.kill();
387 let _ = child.wait();
388 assert!(status.is_none(), "a released guard must not kill the group");
389 }
390
391 #[test]
395 fn assign_pid_kills_group_on_drop() {
396 use nix::errno::Errno;
397 use nix::sys::signal;
398 use nix::unistd::Pid;
399 use std::io::{BufRead, BufReader};
400 use std::process::Stdio;
401
402 let mut cmd = Command::new("sh");
403 cmd.arg("-c").arg("sleep 30 & echo $!; wait");
404 cmd.stdout(Stdio::piped());
405
406 let mut guard = ProcessGroupGuard::new().expect("create guard");
407 guard.configure_command(&mut cmd);
408
409 let mut child = cmd.spawn().expect("spawn sh");
410
411 let stdout = child.stdout.take().expect("piped stdout");
412 let mut reader = BufReader::new(stdout);
413 let mut line = String::new();
414 reader.read_line(&mut line).expect("read grandchild pid");
415 let grandchild: i32 = line.trim().parse().expect("parse grandchild pid");
416
417 guard.assign_pid(child.id());
418
419 drop(guard);
420
421 let deadline = Instant::now() + Duration::from_secs(1);
425 loop {
426 match signal::kill(Pid::from_raw(grandchild), None) {
427 Err(Errno::ESRCH) => break,
428 _ =>
429 {
430 #[cfg(target_os = "linux")]
431 if is_zombie(grandchild) {
432 break;
433 }
434 }
435 }
436 assert!(
437 Instant::now() < deadline,
438 "grandchild pid {grandchild} should die with its process group"
439 );
440 std::thread::sleep(Duration::from_millis(50));
441 }
442
443 let _ = child.wait();
445 }
446
447 #[test]
451 fn kill_group_esrch_is_ok() {
452 let mut cmd = Command::new("sh");
453 cmd.arg("-c").arg("exit 0");
454
455 let mut guard = ProcessGroupGuard::new().expect("create guard");
456 guard.configure_command(&mut cmd);
457
458 let mut child = cmd.spawn().expect("spawn sh");
459 let pid = child.id();
460 child.wait().expect("wait for leader");
462
463 guard.assign_pid(pid);
464 assert!(guard.is_armed(), "guard should be armed after assign_pid");
465
466 assert!(
467 guard.kill_group().is_ok(),
468 "first kill_group on a gone group maps ESRCH to Ok"
469 );
470 assert!(!guard.is_armed(), "kill_group must clear the pid");
471 assert!(
472 guard.kill_group().is_ok(),
473 "second kill_group is a no-op Ok"
474 );
475 }
476
477 #[cfg(target_os = "linux")]
478 fn is_zombie(pid: i32) -> bool {
479 match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
480 Ok(stat) => stat
482 .rsplit(") ")
483 .next()
484 .is_some_and(|rest| rest.starts_with('Z')),
485 Err(_) => false,
486 }
487 }
488}