1use crate::process_handle::{lock_child, ProcessHandle};
2use std::{
3 ffi::OsStr,
4 io,
5 num::NonZeroU32,
6 process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus},
7 sync::{Arc, Mutex, MutexGuard},
8 thread,
9 time::{Duration, Instant, SystemTime},
10};
11
12#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
13pub enum JobAssignmentMode {
14 #[default]
15 Strict,
16 BestEffort,
17}
18
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
20pub enum UnixContainment {
21 #[default]
22 ProcessGroup,
23 Session,
24}
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub struct TerminationOptions {
28 pub grace: Duration,
30 pub windows_exit_code: u32,
32}
33impl Default for TerminationOptions {
34 fn default() -> Self {
35 Self {
36 grace: Duration::ZERO,
37 windows_exit_code: 1,
38 }
39 }
40}
41impl TerminationOptions {
42 pub fn with_grace(mut self, grace: Duration) -> Self {
43 self.grace = grace;
44 self
45 }
46 pub fn with_windows_exit_code(mut self, code: u32) -> Self {
47 self.windows_exit_code = code;
48 self
49 }
50}
51
52#[derive(Debug)]
54pub struct JobAssignmentFailure {
55 pub stage: &'static str,
56 pub error: io::Error,
57}
58
59#[derive(Debug)]
60pub struct OwnedCommand {
61 command: Command,
62 windows_hidden: bool,
63 caller_job: CallerJob,
64 job_assignment: JobAssignmentMode,
65 unix_containment: UnixContainment,
66 partial_spawn_cleanup_timeout: Option<Duration>,
68}
69#[cfg(windows)]
70type CallerJob = Option<std::os::windows::io::OwnedHandle>;
71#[cfg(not(windows))]
72type CallerJob = Option<std::convert::Infallible>;
73
74impl OwnedCommand {
75 pub fn new(program: impl AsRef<OsStr>) -> Self {
76 Self::from_command(Command::new(program))
77 }
78 pub fn from_command(command: Command) -> Self {
80 Self {
81 command,
82 windows_hidden: true,
83 caller_job: None,
84 job_assignment: JobAssignmentMode::Strict,
85 unix_containment: UnixContainment::ProcessGroup,
86 partial_spawn_cleanup_timeout: None,
87 }
88 }
89 pub fn command_mut(&mut self) -> &mut Command {
90 &mut self.command
91 }
92 pub fn env_allowlist<I, S>(&mut self, keys: I) -> &mut Self
94 where
95 I: IntoIterator<Item = S>,
96 S: AsRef<OsStr>,
97 {
98 let kept: Vec<_> = keys
99 .into_iter()
100 .filter_map(|k| std::env::var_os(k.as_ref()).map(|v| (k.as_ref().to_os_string(), v)))
101 .collect();
102 self.command.env_clear();
103 for (k, v) in kept {
104 self.command.env(k, v);
105 }
106 self
107 }
108 pub fn env_strip<I, S>(&mut self, keys: I) -> &mut Self
109 where
110 I: IntoIterator<Item = S>,
111 S: AsRef<OsStr>,
112 {
113 for k in keys {
114 self.command.env_remove(k);
115 }
116 self
117 }
118 pub fn windows_hide(&mut self) -> &mut Self {
120 self.windows_hidden = true;
121 self
122 }
123 #[cfg(windows)]
125 pub fn windows_job(
126 &mut self,
127 job: std::os::windows::io::BorrowedHandle<'_>,
128 ) -> io::Result<&mut Self> {
129 self.caller_job = Some(job.try_clone_to_owned()?);
130 Ok(self)
131 }
132 pub fn job_assignment_mode(&mut self, mode: JobAssignmentMode) -> &mut Self {
134 self.job_assignment = mode;
135 self
136 }
137 pub fn unix_containment(&mut self, mode: UnixContainment) -> &mut Self {
139 self.unix_containment = mode;
140 self
141 }
142 pub fn partial_spawn_cleanup_timeout(&mut self, timeout: Duration) -> &mut Self {
144 self.partial_spawn_cleanup_timeout = Some(timeout);
145 self
146 }
147 pub fn spawn(self) -> io::Result<OwnedChild> {
148 self.spawn_inner(false, false, None)
149 }
150 pub fn spawn_uncontained_detached(self) -> io::Result<OwnedChild> {
154 self.spawn_inner(true, false, None)
155 }
156 fn spawn_inner(
157 mut self,
158 detached: bool,
159 fail_after_spawn: bool,
160 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
161 ) -> io::Result<OwnedChild> {
162 if detached && self.caller_job.is_some() {
163 return Err(io::Error::new(
164 io::ErrorKind::InvalidInput,
165 "detached launch cannot join a caller job",
166 ));
167 }
168 #[cfg(unix)]
169 {
170 use std::os::unix::process::CommandExt;
171 let session = detached || self.unix_containment == UnixContainment::Session;
172 unsafe {
174 self.command.pre_exec(move || {
175 let result = if session {
176 nix::libc::setsid()
177 } else {
178 nix::libc::setpgid(0, 0)
179 };
180 if result < 0 {
181 Err(io::Error::last_os_error())
182 } else {
183 Ok(())
184 }
185 });
186 }
187 }
188 #[cfg(windows)]
189 crate::process_windows::configure(&mut self.command, detached, self.windows_hidden);
190 #[cfg(unix)]
191 let _ = (self.windows_hidden, self.job_assignment);
192 #[cfg(windows)]
193 let _ = self.unix_containment;
194 let spawned_at = SystemTime::now();
195 let child = self.command.spawn()?;
196 let pid = child.id();
197 if let Some(captured) = captured_pid {
198 captured.store(pid, std::sync::atomic::Ordering::SeqCst);
199 }
200 let child = Arc::new(Mutex::new(child));
201 let handle = match ProcessHandle::new(&child, spawned_at) {
202 Ok(handle) => handle,
203 Err(error) => {
204 cleanup_direct(child, self.partial_spawn_cleanup_timeout);
205 return Err(error);
206 }
207 };
208 let mut partial = PartialChild {
209 child: Some(OwnedChild {
210 child,
211 handle,
212 pid,
213 detached,
214 terminate_on_drop: !detached,
215 tree_terminated: false,
216 assignment_failures: Vec::new(),
217 #[cfg(windows)]
218 job: None,
219 }),
220 timeout: self.partial_spawn_cleanup_timeout,
221 };
222 #[cfg(windows)]
223 if !detached {
224 let owned = partial.child.as_mut().expect("partial child is present");
225 crate::process_windows::assign_jobs(
226 &owned.handle,
227 self.caller_job.take(),
228 self.job_assignment,
229 &mut owned.job,
230 &mut owned.assignment_failures,
231 )?;
232 crate::process_windows::resume(pid)?;
233 }
234 if fail_after_spawn {
235 return Err(io::Error::other("injected post-spawn wrapping failure"));
236 }
237 Ok(partial.child.take().expect("partial child is present"))
238 }
239}
240
241pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
242 "PATH",
243 "HOME",
244 "USER",
245 "LOGNAME",
246 "LANG",
247 "LC_ALL",
248 "TMPDIR",
249 "TEMP",
250 "TMP",
251 "SystemRoot",
252 "SYSTEMROOT",
253 "USERPROFILE",
254 "APPDATA",
255 "LOCALAPPDATA",
256 "ProgramData",
257 "COMSPEC",
258 "PATHEXT",
259 "XDG_DATA_HOME",
260 "XDG_RUNTIME_DIR",
261];
262
263#[derive(Debug)]
264pub struct OwnedChild {
265 child: Arc<Mutex<Child>>,
266 handle: ProcessHandle,
267 pid: u32,
269 detached: bool,
270 terminate_on_drop: bool,
271 tree_terminated: bool,
272 assignment_failures: Vec<JobAssignmentFailure>,
273 #[cfg(windows)]
274 job: Option<crate::process_windows::KillOnCloseJob>,
275}
276#[derive(Clone, Copy, Debug, Eq, PartialEq)]
277pub enum ReapOutcome {
278 Exited(ExitStatus),
279 TimedOut,
280}
281#[derive(Clone, Copy, Debug, Eq, PartialEq)]
282pub enum WaitOutcome {
283 Exited(ExitStatus),
284 Terminated(ExitStatus),
285}
286
287impl OwnedChild {
288 pub fn id(&self) -> u32 {
289 self.pid
290 }
291 pub fn child(&self) -> MutexGuard<'_, Child> {
294 lock_child(&self.child)
295 }
296 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
297 self.child().stdin.take()
298 }
299 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
300 self.child().stdout.take()
301 }
302 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
303 self.child().stderr.take()
304 }
305 pub fn job_assignment_failures(&self) -> &[JobAssignmentFailure] {
306 &self.assignment_failures
307 }
308 pub fn process_handle(&self) -> io::Result<ProcessHandle> {
309 self.handle.try_clone()
310 }
311 pub fn creation_time(&self) -> Option<SystemTime> {
312 self.handle.creation_time()
313 }
314 #[cfg(windows)]
315 pub fn creation_time_ticks(&self) -> Option<u64> {
316 self.handle.creation_time_ticks()
317 }
318 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
319 self.child().try_wait()
320 }
321 pub fn wait(&mut self) -> io::Result<ExitStatus> {
324 drop(self.child().stdin.take());
325 self.handle.wait()
326 }
327 pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
328 self.handle.wait_timeout(timeout)
329 }
330 pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
331 #[cfg(unix)]
332 if !self.tree_terminated {
333 self.signal(nix::libc::SIGTERM)?;
334 }
335 if let Some(status) = self.wait_timeout(grace)? {
336 return Ok(WaitOutcome::Exited(status));
337 }
338 self.terminate_tree().map(WaitOutcome::Terminated)
339 }
340 pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
343 self.force_kill(1)?;
344 self.wait()
345 }
346 pub fn terminate_tree_with_options(
349 &mut self,
350 options: TerminationOptions,
351 ) -> io::Result<ExitStatus> {
352 self.start_termination(options)?;
353 self.wait()
354 }
355 pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
356 self.terminate_tree_bounded_with_options(timeout, TerminationOptions::default())
357 }
358 pub fn terminate_tree_bounded_with_options(
360 &mut self,
361 timeout: Duration,
362 mut options: TerminationOptions,
363 ) -> io::Result<ReapOutcome> {
364 let started = Instant::now();
365 options.grace = options.grace.min(timeout);
366 self.start_termination(options)?;
367 Ok(
368 match self.wait_timeout(timeout.saturating_sub(started.elapsed()))? {
369 Some(status) => ReapOutcome::Exited(status),
370 None => ReapOutcome::TimedOut,
371 },
372 )
373 }
374 fn start_termination(&mut self, options: TerminationOptions) -> io::Result<()> {
375 #[cfg(unix)]
376 if !self.tree_terminated {
377 self.signal(nix::libc::SIGTERM)?;
378 let started = Instant::now();
379 while started.elapsed() < options.grace {
380 let _ = self.handle.try_wait()?;
383 if !self.tree_exists()? {
384 break;
385 }
386 thread::sleep(
387 Duration::from_millis(5).min(options.grace.saturating_sub(started.elapsed())),
388 );
389 }
390 }
391 self.force_kill(options.windows_exit_code)
392 }
393 #[cfg(unix)]
394 fn signal(&self, signal: i32) -> io::Result<()> {
395 if self.detached && self.handle.try_wait()?.is_some() {
398 return Ok(());
399 }
400 let target = if self.detached {
401 self.pid as i32
402 } else {
403 -(self.pid as i32)
404 };
405 if unsafe { nix::libc::kill(target, signal) } == 0 {
406 return Ok(());
407 }
408 let error = io::Error::last_os_error();
409 if error.raw_os_error() == Some(nix::libc::ESRCH) || self.group_eperm_after_exit(&error)? {
410 Ok(())
411 } else {
412 Err(error)
413 }
414 }
415 #[cfg(unix)]
416 fn tree_exists(&self) -> io::Result<bool> {
417 if self.detached && self.handle.try_wait()?.is_some() {
418 return Ok(false);
419 }
420 let target = if self.detached {
421 self.pid as i32
422 } else {
423 -(self.pid as i32)
424 };
425 if unsafe { nix::libc::kill(target, 0) } == 0 {
426 return Ok(true);
427 }
428 let error = io::Error::last_os_error();
429 if error.raw_os_error() == Some(nix::libc::ESRCH) || self.group_eperm_after_exit(&error)? {
430 Ok(false)
431 } else {
432 Err(error)
433 }
434 }
435 #[cfg(unix)]
438 fn group_eperm_after_exit(&self, error: &io::Error) -> io::Result<bool> {
439 Ok(!self.detached
440 && error.raw_os_error() == Some(nix::libc::EPERM)
441 && self.handle.try_wait()?.is_some())
442 }
443 fn force_kill(&mut self, code: u32) -> io::Result<()> {
444 if self.tree_terminated {
445 return Ok(());
446 }
447 #[cfg(unix)]
448 {
449 let _ = code;
450 self.signal(nix::libc::SIGKILL)?;
451 }
452 #[cfg(windows)]
453 crate::process_windows::terminate(&self.handle, self.job.as_ref(), code)?;
454 self.tree_terminated = true;
455 Ok(())
456 }
457}
458impl Drop for OwnedChild {
459 fn drop(&mut self) {
460 if self.terminate_on_drop {
461 let _ = self.terminate_tree();
462 } else if self.detached {
463 #[cfg(unix)]
464 {
465 {
468 let mut child = self.child();
469 drop(child.stdin.take());
470 drop(child.stdout.take());
471 drop(child.stderr.take());
472 }
473 if let Ok(reader) = self.handle.try_clone() {
474 let _ = thread::Builder::new()
475 .name("rightkit-detached-reap".into())
476 .spawn(move || {
477 let _ = reader.wait();
478 });
479 }
480 }
481 }
482 }
483}
484
485struct PartialChild {
486 child: Option<OwnedChild>,
487 timeout: Option<Duration>,
488}
489impl Drop for PartialChild {
490 fn drop(&mut self) {
491 if let Some(mut child) = self.child.take() {
492 child.terminate_on_drop = false;
494 let _ = child.force_kill(1);
495 cleanup_direct(Arc::clone(&child.child), self.timeout);
496 }
497 }
498}
499fn cleanup_direct(child: Arc<Mutex<Child>>, timeout: Option<Duration>) {
500 let _ = lock_child(&child).kill();
501 let Some(timeout) = timeout else {
502 let _ = lock_child(&child).wait();
503 return;
504 };
505 let started = Instant::now();
506 loop {
507 match lock_child(&child).try_wait() {
508 Ok(Some(_)) => return,
509 Err(_) => break,
510 Ok(None) => {}
511 }
512 if started.elapsed() >= timeout {
513 break;
514 }
515 thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
516 }
517 let _ = thread::Builder::new()
519 .name("rightkit-partial-reap".into())
520 .spawn(move || {
521 let _ = lock_child(&child).wait();
522 });
523}
524
525#[derive(Clone, Copy, Debug, Eq, PartialEq)]
526pub struct AdoptedProcess {
527 pid: NonZeroU32,
528}
529impl AdoptedProcess {
530 pub fn new(pid: NonZeroU32) -> Self {
531 Self { pid }
532 }
533 pub fn id(&self) -> u32 {
534 self.pid.get()
535 }
536 pub fn is_running(&self) -> io::Result<bool> {
537 process_is_running(self.pid)
538 }
539}
540#[cfg(test)]
541fn spawn_for_test(
542 command: Command,
543 captured_pid: Arc<std::sync::atomic::AtomicU32>,
544) -> io::Result<OwnedChild> {
545 OwnedCommand::from_command(command).spawn_inner(false, true, Some(captured_pid))
546}
547
548#[cfg(windows)]
549fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
550 use windows::Win32::{
551 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
552 System::Threading::{
553 OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
554 PROCESS_SYNCHRONIZE,
555 },
556 };
557
558 let handle = unsafe {
559 OpenProcess(
560 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
561 false,
562 pid.get(),
563 )
564 };
565 let handle = match handle {
566 Ok(handle) => handle,
567 Err(error) => {
568 if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
569 return Ok(false);
570 }
571 return Err(io::Error::other(error.to_string()));
572 }
573 };
574 let wait = unsafe { WaitForSingleObject(handle, 0) };
575 let close = unsafe { CloseHandle(handle) };
576 close.map_err(|error| io::Error::other(error.to_string()))?;
577 match wait.0 {
578 0 => Ok(false),
579 value if value == WAIT_TIMEOUT.0 => Ok(true),
580 _ => Err(io::Error::last_os_error()),
581 }
582}
583
584#[cfg(unix)]
585fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
586 use nix::{errno::Errno, sys::signal::kill, unistd::Pid};
587
588 let pid = i32::try_from(pid.get())
589 .map(Pid::from_raw)
590 .map_err(io::Error::other)?;
591 match kill(pid, None) {
592 Ok(()) | Err(Errno::EPERM) => Ok(true),
593 Err(Errno::ESRCH) => Ok(false),
594 Err(error) => Err(io::Error::from(error)),
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 #[test]
601 fn option_defaults_preserve_existing_behavior() {
602 let command = super::OwnedCommand::new("unused");
603 assert_eq!(command.job_assignment, super::JobAssignmentMode::Strict);
604 assert_eq!(
605 command.unix_containment,
606 super::UnixContainment::ProcessGroup
607 );
608 assert_eq!(command.partial_spawn_cleanup_timeout, None);
609 assert_eq!(
610 super::TerminationOptions::default().grace,
611 std::time::Duration::ZERO
612 );
613 assert_eq!(super::TerminationOptions::default().windows_exit_code, 1);
614 }
615
616 #[test]
617 fn option_builders_keep_every_requested_value() {
618 use super::{JobAssignmentMode, OwnedCommand, TerminationOptions, UnixContainment};
619 use std::time::Duration;
620 let mut command = OwnedCommand::new("unused");
621 command
622 .job_assignment_mode(JobAssignmentMode::BestEffort)
623 .unix_containment(UnixContainment::Session)
624 .partial_spawn_cleanup_timeout(Duration::from_secs(5));
625 assert_eq!(command.job_assignment, JobAssignmentMode::BestEffort);
626 assert_eq!(command.unix_containment, UnixContainment::Session);
627 assert_eq!(
628 command.partial_spawn_cleanup_timeout,
629 Some(Duration::from_secs(5))
630 );
631 let options = TerminationOptions::default()
632 .with_grace(Duration::from_millis(100))
633 .with_windows_exit_code(1067);
634 assert_eq!(options.grace, Duration::from_millis(100));
635 assert_eq!(options.windows_exit_code, 1067);
636 }
637
638 #[test]
639 fn zero_bound_partial_spawn_failure_still_reaps_the_child() {
640 use std::{
641 num::NonZeroU32,
642 process::Command,
643 sync::{
644 atomic::{AtomicU32, Ordering},
645 Arc,
646 },
647 thread,
648 time::{Duration, Instant},
649 };
650 let command = if cfg!(windows) {
651 let mut command = Command::new("powershell.exe");
652 command.args([
653 "-NoLogo",
654 "-NoProfile",
655 "-NonInteractive",
656 "-Command",
657 "Start-Sleep -Seconds 30",
658 ]);
659 command
660 } else {
661 let mut command = Command::new("/bin/sh");
662 command.args(["-c", "exec sleep 30"]);
663 command
664 };
665 let mut command = super::OwnedCommand::from_command(command);
666 command.partial_spawn_cleanup_timeout(Duration::ZERO);
667 let captured = Arc::new(AtomicU32::new(0));
668 let started = Instant::now();
669 assert!(command
670 .spawn_inner(false, true, Some(Arc::clone(&captured)))
671 .is_err());
672 assert!(started.elapsed() < Duration::from_secs(3));
673 let pid = captured.load(Ordering::SeqCst);
674 assert_ne!(pid, 0);
675 let process = super::AdoptedProcess::new(NonZeroU32::new(pid).unwrap());
676 let started = Instant::now();
677 while process.is_running().unwrap() {
678 assert!(started.elapsed() < Duration::from_secs(5));
679 thread::sleep(Duration::from_millis(5));
680 }
681 }
682
683 #[test]
684 fn owned_commands_hide_their_console_window_by_default() {
685 assert!(super::OwnedCommand::new("x").windows_hidden);
686 assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
687 }
688
689 #[cfg(windows)]
691 fn host_job() -> std::os::windows::io::OwnedHandle {
692 use std::os::windows::io::FromRawHandle;
693 use windows::Win32::System::JobObjects::{
694 CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject,
695 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
696 };
697 let job = unsafe { CreateJobObjectW(None, None) }.unwrap();
698 let job_handle = unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(job.0 as _) };
699 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
700 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
701 unsafe {
702 SetInformationJobObject(
703 job,
704 JobObjectExtendedLimitInformation,
705 &info as *const _ as *const _,
706 std::mem::size_of_val(&info) as u32,
707 )
708 }
709 .unwrap();
710 job_handle
711 }
712
713 #[cfg(windows)]
714 fn in_job(pid: u32, job: &std::os::windows::io::OwnedHandle) -> bool {
715 use std::os::windows::io::AsRawHandle;
716 #[link(name = "kernel32")]
717 unsafe extern "system" {
718 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> *mut std::ffi::c_void;
719 fn IsProcessInJob(
720 process: *mut std::ffi::c_void,
721 job: *mut std::ffi::c_void,
722 result: *mut i32,
723 ) -> i32;
724 fn CloseHandle(handle: *mut std::ffi::c_void) -> i32;
725 }
726 unsafe {
727 let process = OpenProcess(0x1000, 0, pid); assert!(!process.is_null());
729 let mut inside = 0;
730 assert_ne!(
731 IsProcessInJob(process, job.as_raw_handle() as _, &mut inside),
732 0
733 );
734 CloseHandle(process);
735 inside != 0
736 }
737 }
738
739 #[cfg(windows)]
740 #[test]
741 fn children_join_the_callers_job_and_die_when_the_caller_closes_it() {
742 use std::os::windows::io::AsHandle;
743 use std::time::{Duration, Instant};
744 let job = host_job();
745 let sleeper = || {
746 let mut command = super::OwnedCommand::new("powershell.exe");
747 command.command_mut().args([
748 "-NoLogo",
749 "-NoProfile",
750 "-NonInteractive",
751 "-Command",
752 "Start-Sleep -Seconds 60",
753 ]);
754 command.windows_job(job.as_handle()).unwrap();
755 command.spawn().unwrap()
756 };
757 let (first, second) = (sleeper(), sleeper());
760 let pids = [first.id(), second.id()];
761 assert!(
762 pids.iter().all(|pid| in_job(*pid, &job)),
763 "children must be in the caller's job"
764 );
765
766 drop(job);
768 let deadline = Instant::now() + Duration::from_secs(5);
769 for pid in pids {
770 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
771 while process.is_running().unwrap_or(false) {
772 assert!(
773 Instant::now() < deadline,
774 "child {pid} survived its caller's job"
775 );
776 std::thread::sleep(Duration::from_millis(20));
777 }
778 }
779 drop((first, second));
780 }
781
782 #[test]
783 fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
784 use std::{
785 process::Command,
786 sync::{
787 atomic::{AtomicU32, Ordering},
788 Arc,
789 },
790 thread,
791 time::{Duration, Instant},
792 };
793
794 let command = if cfg!(windows) {
796 let mut command = Command::new("powershell.exe");
798 command.args([
799 "-NoLogo",
800 "-NoProfile",
801 "-NonInteractive",
802 "-Command",
803 "Start-Sleep -Seconds 60",
804 ]);
805 command
806 } else {
807 let mut command = Command::new("sleep");
809 command.arg("60");
810 command
811 };
812 let spawned_pid = Arc::new(AtomicU32::new(0));
813
814 assert!(super::spawn_for_test(command, Arc::clone(&spawned_pid)).is_err());
815 let pid = spawned_pid.load(Ordering::SeqCst);
816 assert_ne!(pid, 0, "test must fail after the OS process was spawned");
817 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
818 let deadline = Instant::now() + Duration::from_secs(5);
819 while process.is_running().unwrap_or(false) {
820 assert!(
821 Instant::now() < deadline,
822 "partially spawned process leaked"
823 );
824 thread::sleep(Duration::from_millis(20));
825 }
826 }
827}