1use crate::process_handle::{lock_child, ProcessHandle};
2#[cfg(unix)]
3use crate::unix_child::{LeaderState, SignalOutcome, SignalTarget};
4use std::{
5 ffi::OsStr,
6 io,
7 num::NonZeroU32,
8 process::{ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus},
9 sync::{Arc, Mutex, MutexGuard},
10 thread,
11 time::{Duration, Instant, SystemTime},
12};
13#[cfg(unix)]
14type PlatformChild = crate::unix_child::UnixChild;
15#[cfg(windows)]
16type PlatformChild = crate::spawn_windows::WindowsChild;
17
18#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
19pub enum JobAssignmentMode {
20 #[default]
21 Strict,
22 BestEffort,
23}
24
25#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
26pub enum UnixContainment {
27 #[default]
28 ProcessGroup,
29 Session,
30}
31
32#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
49#[non_exhaustive]
50pub enum ProcessPriority {
51 #[default]
52 Normal,
53 BelowNormal,
54 Idle,
55 Efficient,
56}
57
58impl ProcessPriority {
59 #[cfg(unix)]
61 pub(crate) fn nice_floor(self) -> Option<libc::c_int> {
62 match self {
63 Self::Normal => None,
64 Self::BelowNormal | Self::Efficient => Some(10),
65 Self::Idle => Some(19),
66 }
67 }
68}
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub struct TerminationOptions {
72 pub grace: Duration,
74 pub windows_exit_code: u32,
76}
77impl Default for TerminationOptions {
78 fn default() -> Self {
79 Self {
80 grace: Duration::ZERO,
81 windows_exit_code: 1,
82 }
83 }
84}
85impl TerminationOptions {
86 pub fn with_grace(mut self, grace: Duration) -> Self {
87 self.grace = grace;
88 self
89 }
90 pub fn with_windows_exit_code(mut self, code: u32) -> Self {
91 self.windows_exit_code = code;
92 self
93 }
94}
95
96#[derive(Debug)]
98pub struct JobAssignmentFailure {
99 pub stage: &'static str,
100 pub error: io::Error,
101}
102impl JobAssignmentFailure {
103 pub fn raw_os_error(&self) -> Option<i32> {
104 self.error.raw_os_error()
105 }
106}
107
108#[cfg(windows)]
110pub type RawHandleOrFd = std::os::windows::io::RawHandle;
111#[cfg(unix)]
112pub type RawHandleOrFd = std::os::fd::RawFd;
113#[cfg(windows)]
114type InheritedValue = usize;
115#[cfg(unix)]
116type InheritedValue = RawHandleOrFd;
117
118#[derive(Debug)]
119pub struct OwnedCommand {
120 command: Command,
121 windows_hidden: bool,
122 caller_job: CallerJob,
123 job_assignment: JobAssignmentMode,
124 unix_containment: UnixContainment,
125 process_priority: ProcessPriority,
126 kill_on_parent_exit: bool,
127 hold_suspended: bool,
129 partial_spawn_cleanup_timeout: Option<Duration>,
131 inherited_handles: Option<Vec<InheritedValue>>,
132 #[cfg(windows)]
133 windows_spawn_config: Option<crate::WindowsSpawnConfig>,
134 #[cfg(windows)]
135 windows_spawn_config_auto: bool,
136 env_cleared: bool,
137}
138#[cfg(windows)]
139type CallerJob = Option<std::os::windows::io::OwnedHandle>;
140#[cfg(not(windows))]
141type CallerJob = Option<std::convert::Infallible>;
142
143impl OwnedCommand {
144 pub fn new(program: impl AsRef<OsStr>) -> Self {
145 let command = Self::from_command(Command::new(program));
146 #[cfg(windows)]
147 let command = {
148 let mut command = command;
149 command.windows_spawn_config = Some(crate::WindowsSpawnConfig::default());
152 command.windows_spawn_config_auto = true;
153 command
154 };
155 command
156 }
157 pub fn from_command(command: Command) -> Self {
163 Self {
164 command,
165 windows_hidden: true,
166 caller_job: None,
167 job_assignment: JobAssignmentMode::Strict,
168 unix_containment: UnixContainment::ProcessGroup,
169 process_priority: ProcessPriority::Normal,
170 kill_on_parent_exit: false,
171 hold_suspended: false,
172 partial_spawn_cleanup_timeout: None,
173 inherited_handles: None,
174 #[cfg(windows)]
175 windows_spawn_config: None,
176 #[cfg(windows)]
177 windows_spawn_config_auto: false,
178 env_cleared: false,
179 }
180 }
181 pub fn command_mut(&mut self) -> &mut Command {
182 #[cfg(windows)]
183 if self.windows_spawn_config_auto {
184 self.windows_spawn_config = None;
186 }
187 &mut self.command
188 }
189 pub fn inherit_handles_only(
191 &mut self,
192 handles: impl IntoIterator<Item = RawHandleOrFd>,
193 ) -> &mut Self {
194 #[cfg(windows)]
195 let handles = handles.into_iter().map(|handle| handle as usize).collect();
196 #[cfg(unix)]
197 let handles = handles.into_iter().collect();
198 self.inherited_handles = Some(handles);
199 self
200 }
201 #[cfg(windows)]
205 pub fn windows_spawn_config(&mut self, config: crate::WindowsSpawnConfig) -> &mut Self {
206 self.windows_spawn_config = Some(config);
207 self.windows_spawn_config_auto = false;
208 self
209 }
210 pub fn env_allowlist<I, S>(&mut self, keys: I) -> &mut Self
212 where
213 I: IntoIterator<Item = S>,
214 S: AsRef<OsStr>,
215 {
216 let kept: Vec<_> = keys
217 .into_iter()
218 .filter_map(|k| std::env::var_os(k.as_ref()).map(|v| (k.as_ref().to_os_string(), v)))
219 .collect();
220 self.command.env_clear();
221 self.env_cleared = true;
222 for (k, v) in kept {
223 self.command.env(k, v);
224 }
225 self
226 }
227 pub fn env_allowlist_with_defaults<I, S>(&mut self, extra: I) -> &mut Self
231 where
232 I: IntoIterator<Item = S>,
233 S: AsRef<OsStr>,
234 {
235 let keys: Vec<std::ffi::OsString> = DEFAULT_ENV_ALLOWLIST
236 .iter()
237 .map(std::ffi::OsString::from)
238 .chain(extra.into_iter().map(|k| k.as_ref().to_os_string()))
239 .collect();
240 self.env_allowlist(keys)
241 }
242 pub fn env_is_cleared(&self) -> bool {
247 self.env_cleared
248 }
249 pub fn env_strip<I, S>(&mut self, keys: I) -> &mut Self
250 where
251 I: IntoIterator<Item = S>,
252 S: AsRef<OsStr>,
253 {
254 for k in keys {
255 self.command.env_remove(k);
256 }
257 self
258 }
259 pub fn windows_hide(&mut self) -> &mut Self {
261 self.windows_hidden = true;
262 self
263 }
264 #[cfg(windows)]
266 pub fn windows_job(
267 &mut self,
268 job: std::os::windows::io::BorrowedHandle<'_>,
269 ) -> io::Result<&mut Self> {
270 self.caller_job = Some(job.try_clone_to_owned()?);
271 Ok(self)
272 }
273 pub fn job_assignment_mode(&mut self, mode: JobAssignmentMode) -> &mut Self {
275 self.job_assignment = mode;
276 self
277 }
278 pub fn unix_containment(&mut self, mode: UnixContainment) -> &mut Self {
280 self.unix_containment = mode;
281 self
282 }
283 pub fn process_priority(&mut self, priority: ProcessPriority) -> &mut Self {
287 self.process_priority = priority;
288 self
289 }
290 pub fn kill_on_parent_exit(&mut self, enabled: bool) -> &mut Self {
312 self.kill_on_parent_exit = enabled;
313 self
314 }
315 pub fn partial_spawn_cleanup_timeout(&mut self, timeout: Duration) -> &mut Self {
317 self.partial_spawn_cleanup_timeout = Some(timeout);
318 self
319 }
320 pub fn spawn(self) -> io::Result<OwnedChild> {
321 self.spawn_inner(false, false, None)
322 }
323 pub fn spawn_suspended(mut self) -> io::Result<OwnedChild> {
333 self.hold_suspended = true;
334 self.spawn_inner(false, false, None)
335 }
336 pub fn spawn_uncontained_detached(self) -> io::Result<OwnedChild> {
351 self.spawn_inner_with(true, false, None, true)
352 }
353 pub fn spawn_uncontained_detached_strict(self) -> io::Result<OwnedChild> {
359 match self.spawn_inner_with(true, false, None, false) {
360 Err(error) if cfg!(windows) && is_denied_breakaway(&error) => Err(io::Error::new(
361 io::ErrorKind::PermissionDenied,
362 BreakawayDenied,
363 )),
364 other => other,
365 }
366 }
367 fn spawn_inner(
368 self,
369 detached: bool,
370 fail_after_spawn: bool,
371 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
372 ) -> io::Result<OwnedChild> {
373 self.spawn_inner_with(detached, fail_after_spawn, captured_pid, false)
374 }
375 fn spawn_inner_with(
376 mut self,
377 detached: bool,
378 fail_after_spawn: bool,
379 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
380 in_job_fallback: bool,
381 ) -> io::Result<OwnedChild> {
382 if detached && self.caller_job.is_some() {
383 return Err(io::Error::new(
384 io::ErrorKind::InvalidInput,
385 "detached launch cannot join a caller job",
386 ));
387 }
388 if detached && self.kill_on_parent_exit {
389 return Err(io::Error::new(
390 io::ErrorKind::InvalidInput,
391 "kill_on_parent_exit cannot combine with a detached launch",
392 ));
393 }
394 #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
395 if self.kill_on_parent_exit {
396 return Err(io::Error::new(
397 io::ErrorKind::Unsupported,
398 "kill_on_parent_exit is supported on Linux and macOS only",
399 ));
400 }
401 #[cfg(unix)]
402 {
403 use std::os::unix::process::CommandExt;
404 let session = detached || self.unix_containment == UnixContainment::Session;
405 let nice_floor = self.process_priority.nice_floor();
406 #[cfg(target_os = "macos")]
407 let utility_qos = self.process_priority == ProcessPriority::Efficient;
408 let inheritance = self
409 .inherited_handles
410 .take()
411 .map(crate::process_unix::Inheritance::new)
412 .transpose()?;
413 #[cfg(target_os = "linux")]
415 let supervisor = self
416 .kill_on_parent_exit
417 .then(|| unsafe { libc::getpid() });
419 unsafe {
421 self.command.pre_exec(move || {
422 let result = if session {
423 libc::setsid()
424 } else {
425 libc::setpgid(0, 0)
426 };
427 if result < 0 {
428 return Err(io::Error::last_os_error());
429 }
430 if let Some(floor) = nice_floor {
431 crate::process_unix::raise_nice_to(floor)?;
432 }
433 #[cfg(target_os = "macos")]
434 if utility_qos {
435 crate::process_unix::set_utility_qos();
436 }
437 if let Some(inheritance) = &inheritance {
438 inheritance.apply()?;
439 }
440 #[cfg(target_os = "linux")]
441 if let Some(supervisor) = supervisor {
442 crate::process_unix::die_with_parent(supervisor)?;
443 }
444 Ok(())
445 });
446 }
447 }
448 #[cfg(windows)]
449 crate::process_windows::configure(
450 &mut self.command,
451 detached,
452 self.windows_hidden,
453 self.process_priority,
454 );
455 #[cfg(unix)]
456 let _ = (self.windows_hidden, self.job_assignment);
457 #[cfg(windows)]
458 let _ = (self.unix_containment, self.kill_on_parent_exit);
459 let spawned_at = SystemTime::now();
460 let hold_suspended = self.hold_suspended && !detached;
461 #[cfg(unix)]
462 let _ = hold_suspended;
463 #[cfg(unix)]
464 let child = {
465 let mut child = crate::unix_child::UnixChild::new(self.command.spawn()?);
466 child.require_identity()?;
469 #[cfg(target_os = "macos")]
472 if self.kill_on_parent_exit {
473 if let Err(error) = crate::process_unix::arm_parent_death_watch(child.id()) {
474 let _ = child.signal(SignalTarget::Group, libc::SIGKILL);
475 let _ = child.kill();
476 let _ = child.wait();
477 return Err(error);
478 }
479 }
480 child
481 };
482 #[cfg(windows)]
483 let (child, stayed_in_parent_job) = match self.inherited_handles.as_deref() {
484 None => {
485 let _spawning = crate::spawn_windows::spawn_guard();
488 match self.command.spawn() {
489 Ok(child) => (PlatformChild::from(child), false),
490 Err(error)
492 if detached
493 && in_job_fallback
494 && crate::process_windows::is_breakaway_denied(&error) =>
495 {
496 crate::process_windows::configure_in_job_detached(
497 &mut self.command,
498 self.process_priority,
499 );
500 (PlatformChild::from(self.command.spawn()?), true)
501 }
502 Err(error) => return Err(error),
503 }
504 }
505 Some(handles) => {
506 let config = self.windows_spawn_config.take().ok_or_else(|| {
507 io::Error::new(io::ErrorKind::InvalidInput,
508 "allowlisted Windows spawn requires windows_spawn_config for opaque Command settings")
509 })?;
510 crate::spawn_windows::spawn(
511 &self.command,
512 config,
513 handles,
514 detached,
515 self.windows_hidden,
516 self.process_priority,
517 self.env_cleared,
518 self.partial_spawn_cleanup_timeout,
519 in_job_fallback,
520 )?
521 }
522 };
523 #[cfg(not(windows))]
524 let _ = in_job_fallback;
525 #[cfg(not(windows))]
526 let stayed_in_parent_job = false;
527 let pid = child.id();
528 if let Some(captured) = captured_pid {
529 captured.store(pid, std::sync::atomic::Ordering::SeqCst);
530 }
531 let child = Arc::new(Mutex::new(child));
532 let handle = match ProcessHandle::new(&child, spawned_at) {
533 Ok(handle) => handle,
534 Err(error) => {
535 cleanup_direct(child, self.partial_spawn_cleanup_timeout);
536 return Err(error);
537 }
538 };
539 let mut partial = PartialChild {
540 child: Some(OwnedChild {
541 child,
542 handle,
543 pid,
544 detached,
545 terminate_on_drop: !detached,
546 drop_reap_timeout: DEFAULT_DROP_REAP_TIMEOUT,
547 tree_terminated: false,
548 assignment_failures: Vec::new(),
549 suspended: std::sync::atomic::AtomicBool::new(cfg!(windows) && hold_suspended),
550 stayed_in_parent_job,
551 #[cfg(windows)]
552 job: None,
553 }),
554 timeout: self.partial_spawn_cleanup_timeout,
555 };
556 #[cfg(windows)]
557 if !detached {
558 let owned = partial.child.as_mut().expect("partial child is present");
559 crate::process_windows::assign_jobs(
560 &owned.handle,
561 self.caller_job.take(),
562 self.job_assignment,
563 &mut owned.job,
564 &mut owned.assignment_failures,
565 )?;
566 }
567 #[cfg(windows)]
568 {
569 let owned = partial.child.as_ref().expect("partial child is present");
570 if self.process_priority == ProcessPriority::Efficient {
571 let _ = crate::process_windows::apply_efficiency(&*owned.child());
573 }
574 if !hold_suspended {
575 owned.child().resume(detached)?;
576 }
577 }
578 if fail_after_spawn {
579 return Err(io::Error::other("injected post-spawn wrapping failure"));
580 }
581 let owned = partial.child.take().expect("partial child is present");
582 if !detached {
583 #[cfg(unix)]
585 crate::registry::register(&owned.child, owned.pid);
586 #[cfg(windows)]
587 crate::registry::register(
588 &owned.child,
589 &owned.handle,
590 owned
591 .job
592 .as_ref()
593 .map_or_else(std::sync::Weak::new, std::sync::Arc::downgrade),
594 owned.pid,
595 );
596 }
597 Ok(owned)
598 }
599}
600
601#[derive(Clone, Copy, Debug, Eq, PartialEq)]
607pub struct BreakawayDenied;
608
609impl std::fmt::Display for BreakawayDenied {
610 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611 f.write_str(
612 "detached launch denied: the caller's job object does not allow breakaway \
613 (ERROR_ACCESS_DENIED); no fallback was attempted",
614 )
615 }
616}
617
618impl std::error::Error for BreakawayDenied {}
619
620#[cfg(windows)]
621fn is_denied_breakaway(error: &io::Error) -> bool {
622 crate::process_windows::is_breakaway_denied(error)
623}
624#[cfg(not(windows))]
625fn is_denied_breakaway(_error: &io::Error) -> bool {
626 false
627}
628
629pub const DEFAULT_DROP_REAP_TIMEOUT: Duration = Duration::from_secs(2);
632
633pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
634 "PATH",
635 "HOME",
636 "USER",
637 "LOGNAME",
638 "LANG",
639 "LC_ALL",
640 "TMPDIR",
641 "TEMP",
642 "TMP",
643 "SystemRoot",
644 "SYSTEMROOT",
645 "USERPROFILE",
646 "APPDATA",
647 "LOCALAPPDATA",
648 "ProgramData",
649 "COMSPEC",
650 "PATHEXT",
651 "XDG_DATA_HOME",
652 "XDG_RUNTIME_DIR",
653];
654
655#[derive(Debug)]
656pub struct OwnedChild {
657 child: Arc<Mutex<PlatformChild>>,
658 handle: ProcessHandle,
659 pid: u32,
661 detached: bool,
662 terminate_on_drop: bool,
663 drop_reap_timeout: Duration,
664 tree_terminated: bool,
665 assignment_failures: Vec<JobAssignmentFailure>,
666 suspended: std::sync::atomic::AtomicBool,
668 stayed_in_parent_job: bool,
670 #[cfg(windows)]
673 job: Option<std::sync::Arc<crate::process_windows::KillOnCloseJob>>,
674}
675#[derive(Clone, Debug, Eq, PartialEq)]
677pub struct TreeTermination {
678 pub status: ExitStatus,
680 pub descendants: crate::SweepReport,
682}
683#[derive(Clone, Copy, Debug, Eq, PartialEq)]
684pub enum ReapOutcome {
685 Exited(ExitStatus),
686 TimedOut,
687}
688#[derive(Clone, Copy, Debug, Eq, PartialEq)]
689pub enum WaitOutcome {
690 Exited(ExitStatus),
691 Terminated(ExitStatus),
692}
693
694impl OwnedChild {
695 pub fn id(&self) -> u32 {
696 self.pid
697 }
698 pub fn stayed_in_parent_job(&self) -> bool {
703 self.stayed_in_parent_job
704 }
705 pub fn child(&self) -> MutexGuard<'_, PlatformChild> {
721 lock_child(&self.child)
722 }
723 pub fn resume(&self) -> io::Result<()> {
728 if self
729 .suspended
730 .swap(false, std::sync::atomic::Ordering::SeqCst)
731 {
732 #[cfg(windows)]
733 if let Err(error) = self.child().resume(self.detached) {
734 self.suspended
735 .store(true, std::sync::atomic::Ordering::SeqCst);
736 return Err(error);
737 }
738 }
739 Ok(())
740 }
741 pub fn is_suspended(&self) -> bool {
743 self.suspended.load(std::sync::atomic::Ordering::SeqCst)
744 }
745 pub fn while_unreaped<R>(&self, act: impl FnOnce(u32) -> R) -> io::Result<Option<R>> {
754 #[cfg(unix)]
755 {
756 let mut child = lock_child(&self.child);
757 if child.leader_state()? == LeaderState::Reaped {
758 return Ok(None);
759 }
760 Ok(Some(act(self.pid)))
761 }
762 #[cfg(windows)]
763 Ok(Some(act(self.pid)))
764 }
765 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
766 self.child().stdin.take()
767 }
768 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
769 self.child().stdout.take()
770 }
771 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
772 self.child().stderr.take()
773 }
774 pub fn job_assignment_failures(&self) -> &[JobAssignmentFailure] {
775 &self.assignment_failures
776 }
777 pub fn process_handle(&self) -> io::Result<ProcessHandle> {
781 self.handle.try_clone()
782 }
783 pub fn creation_time(&self) -> Option<SystemTime> {
784 self.handle.creation_time()
785 }
786 #[cfg(windows)]
787 pub fn creation_time_ticks(&self) -> Option<u64> {
788 self.handle.creation_time_ticks()
789 }
790 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
791 self.handle.try_wait()
793 }
794 pub fn wait(&mut self) -> io::Result<ExitStatus> {
797 drop(self.child().stdin.take());
798 self.handle.wait()
799 }
800 pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
801 self.handle.wait_timeout(timeout)
802 }
803 pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
813 self.wait_or_kill_inner(grace, true)
814 }
815 pub fn wait_then_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
821 self.wait_or_kill_inner(grace, false)
822 }
823 fn wait_or_kill_inner(&mut self, grace: Duration, term_first: bool) -> io::Result<WaitOutcome> {
824 #[cfg(not(unix))]
825 let _ = term_first;
826 #[cfg(unix)]
827 {
828 if term_first && !self.tree_terminated {
829 self.signal(libc::SIGTERM)?;
830 }
831 if self.handle.wait_exited_timeout(grace)? {
833 self.force_kill(1)?;
838 return self.wait().map(WaitOutcome::Exited);
839 }
840 self.terminate_tree().map(WaitOutcome::Terminated)
841 }
842 #[cfg(windows)]
843 {
844 if let Some(status) = self.wait_timeout(grace)? {
845 self.force_kill(1)?;
849 return Ok(WaitOutcome::Exited(status));
850 }
851 self.terminate_tree().map(WaitOutcome::Terminated)
852 }
853 }
854 pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
859 self.force_kill(1)?;
860 self.wait()
861 }
862 pub fn terminate_tree_with_options(
867 &mut self,
868 options: TerminationOptions,
869 ) -> io::Result<ExitStatus> {
870 self.start_termination(options)?;
871 self.wait()
872 }
873 pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
874 self.terminate_tree_bounded_with_options(timeout, TerminationOptions::default())
875 }
876 pub fn terminate_tree_bounded_with_options(
878 &mut self,
879 timeout: Duration,
880 mut options: TerminationOptions,
881 ) -> io::Result<ReapOutcome> {
882 let started = Instant::now();
883 options.grace = options.grace.min(timeout);
884 self.start_termination(options)?;
885 Ok(
886 match self.wait_timeout(timeout.saturating_sub(started.elapsed()))? {
887 Some(status) => ReapOutcome::Exited(status),
888 None => ReapOutcome::TimedOut,
889 },
890 )
891 }
892 pub fn terminate_tree_deep(
916 &mut self,
917 options: TerminationOptions,
918 ) -> io::Result<TreeTermination> {
919 let mut found = self.descendant_identities()?;
920 found.reverse(); #[cfg(unix)]
922 if !self.tree_terminated {
923 signal_shared(&self.child, SignalTarget::Group, libc::SIGTERM)?;
924 await_tree_shared(&self.child, &self.handle, options.grace, true)?;
925 signal_shared(&self.child, SignalTarget::Group, libc::SIGKILL)?;
926 self.tree_terminated = true;
927 }
928 #[cfg(windows)]
929 self.force_kill(options.windows_exit_code)?;
930 let status = self.wait()?;
931 let descendants = crate::sweep(&found, options.grace)?;
932 Ok(TreeTermination {
933 status,
934 descendants,
935 })
936 }
937 fn start_termination(&mut self, options: TerminationOptions) -> io::Result<()> {
938 #[cfg(unix)]
939 if !self.tree_terminated {
940 self.signal(libc::SIGTERM)?;
941 self.await_tree(options.grace)?;
945 }
946 self.force_kill(options.windows_exit_code)
947 }
948 #[cfg(unix)]
955 fn await_tree(&self, grace: Duration) -> io::Result<()> {
956 await_tree_shared(&self.child, &self.handle, grace, !self.detached)
958 }
959 #[cfg(unix)]
963 fn signal(&self, signal: i32) -> io::Result<()> {
964 let target = if self.detached {
967 SignalTarget::Leader
968 } else {
969 SignalTarget::Group
970 };
971 signal_shared(&self.child, target, signal).map(|_| ())
972 }
973 fn force_kill(&mut self, code: u32) -> io::Result<()> {
974 if self.tree_terminated {
975 return Ok(());
976 }
977 #[cfg(unix)]
978 {
979 let _ = code;
980 self.signal(libc::SIGKILL)?;
981 }
982 #[cfg(windows)]
983 crate::process_windows::terminate(&self.handle, self.job.as_deref(), code)?;
984 self.tree_terminated = true;
985 Ok(())
986 }
987}
988impl OwnedChild {
989 pub fn set_drop_reap_timeout(&mut self, timeout: Duration) -> &mut Self {
996 self.drop_reap_timeout = timeout;
997 self
998 }
999 pub fn drop_reap_timeout(&self) -> Duration {
1001 self.drop_reap_timeout
1002 }
1003 fn reap_in_background(&self) {
1005 #[cfg(unix)]
1006 if let Ok(reader) = self.handle.try_clone() {
1007 let _ = thread::Builder::new()
1008 .name("rightkit-drop-reap".into())
1009 .spawn(move || {
1010 let _ = reader.wait();
1011 });
1012 }
1013 }
1014 pub fn descendant_identities(&self) -> io::Result<Vec<crate::AdoptedIdentity>> {
1018 let root = self.while_unreaped(|pid| match NonZeroU32::new(pid) {
1019 Some(pid) => crate::AdoptedIdentity::capture(pid),
1020 None => Ok(None),
1021 })?;
1022 match root {
1023 Some(Ok(Some(root))) => root.descendants(),
1024 Some(Err(error)) => Err(error),
1025 _ => Ok(Vec::new()),
1026 }
1027 }
1028}
1029impl Drop for OwnedChild {
1030 fn drop(&mut self) {
1031 crate::registry::forget(&self.child);
1033 if self.terminate_on_drop {
1034 if self.force_kill(1).is_ok() {
1038 drop(self.child().stdin.take());
1039 if !matches!(
1040 self.handle.wait_timeout(self.drop_reap_timeout),
1041 Ok(Some(_))
1042 ) {
1043 self.reap_in_background();
1044 }
1045 }
1046 } else if self.detached {
1047 #[cfg(unix)]
1048 {
1049 {
1053 let mut child = self.child();
1054 drop(child.stdin.take());
1055 drop(child.stdout.take());
1056 drop(child.stderr.take());
1057 }
1058 if let Ok(reader) = self.handle.try_clone() {
1059 let _ = thread::Builder::new()
1060 .name("rightkit-detached-reap".into())
1061 .spawn(move || {
1062 let _ = reader.wait();
1063 });
1064 }
1065 }
1066 }
1067 }
1068}
1069
1070struct PartialChild {
1071 child: Option<OwnedChild>,
1072 timeout: Option<Duration>,
1073}
1074impl Drop for PartialChild {
1075 fn drop(&mut self) {
1076 if let Some(mut child) = self.child.take() {
1077 child.terminate_on_drop = false;
1079 let _ = child.force_kill(1);
1080 cleanup_direct(Arc::clone(&child.child), self.timeout);
1081 }
1082 }
1083}
1084#[cfg(unix)]
1087const MID_EXIT_RETRY: Duration = Duration::from_millis(300);
1088
1089#[cfg(unix)]
1093pub(crate) fn signal_shared(
1094 shared: &Mutex<crate::unix_child::UnixChild>,
1095 target: SignalTarget,
1096 signal: i32,
1097) -> io::Result<SignalOutcome> {
1098 let started = Instant::now();
1099 loop {
1100 let (outcome, watch) = {
1101 let mut child = lock_child(shared);
1102 (child.signal(target, signal)?, child.exit_watch())
1103 };
1104 if outcome != SignalOutcome::MidExit {
1105 return Ok(outcome);
1106 }
1107 let remaining = MID_EXIT_RETRY.saturating_sub(started.elapsed());
1110 if remaining.is_zero() {
1111 return Err(io::Error::from_raw_os_error(libc::EPERM));
1112 }
1113 watch.pause(remaining);
1114 }
1115}
1116
1117#[cfg(unix)]
1119pub(crate) fn await_tree_shared(
1120 shared: &Mutex<crate::unix_child::UnixChild>,
1121 handle: &ProcessHandle,
1122 grace: Duration,
1123 whole_group: bool,
1124) -> io::Result<()> {
1125 use crate::{exit_watch::POLL_INTERVAL, unix_child::Members};
1126 let Some(deadline) = Instant::now().checked_add(grace) else {
1127 return Ok(());
1128 };
1129 let left = |deadline: Instant| deadline.saturating_duration_since(Instant::now());
1130 if !handle.wait_exited_timeout(left(deadline))? {
1131 return Ok(()); }
1133 if !whole_group {
1134 return Ok(());
1135 }
1136 loop {
1137 match lock_child(shared).group_other_members()? {
1138 Members::Pids(pids) if pids.is_empty() => return Ok(()),
1139 Members::Pids(_) => {}
1140 Members::Unknown => return wait_out(left(deadline)),
1141 }
1142 let remaining = left(deadline);
1143 if remaining.is_zero() {
1144 return Ok(()); }
1146 thread::sleep(POLL_INTERVAL.min(remaining));
1147 }
1148}
1149
1150#[cfg(unix)]
1153fn wait_out(remaining: Duration) -> io::Result<()> {
1154 thread::sleep(remaining);
1155 Ok(())
1156}
1157pub(crate) fn cleanup_direct(child: Arc<Mutex<PlatformChild>>, timeout: Option<Duration>) {
1164 let _ = lock_child(&child).kill();
1165 let Some(timeout) = timeout else {
1166 let _ = lock_child(&child).wait();
1168 return;
1169 };
1170 #[cfg(unix)]
1171 {
1172 let watch = lock_child(&child).exit_watch();
1175 let deadline = Instant::now().checked_add(timeout);
1176 if matches!(
1177 watch.wait_for(deadline, || lock_child(&child).try_wait()),
1178 Ok(Some(_))
1179 ) {
1180 return;
1181 }
1182 }
1183 #[cfg(windows)]
1184 {
1185 let started = Instant::now();
1186 loop {
1187 match lock_child(&child).try_wait() {
1188 Ok(Some(_)) => return,
1189 Err(_) => break,
1190 Ok(None) => {}
1191 }
1192 if started.elapsed() >= timeout {
1193 break;
1194 }
1195 thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
1196 }
1197 }
1198 let _ = thread::Builder::new()
1202 .name("rightkit-partial-reap".into())
1203 .spawn(move || {
1204 let _ = lock_child(&child).wait();
1205 });
1206}
1207
1208#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1209pub struct AdoptedProcess {
1210 pid: NonZeroU32,
1211}
1212impl AdoptedProcess {
1213 pub fn new(pid: NonZeroU32) -> Self {
1214 Self { pid }
1215 }
1216 pub fn id(&self) -> u32 {
1217 self.pid.get()
1218 }
1219 pub fn is_running(&self) -> io::Result<bool> {
1220 process_is_running(self.pid)
1221 }
1222 pub fn identity(&self) -> io::Result<Option<crate::AdoptedIdentity>> {
1227 crate::AdoptedIdentity::capture(self.pid)
1228 }
1229}
1230#[cfg(test)]
1231fn spawn_for_test(
1232 command: Command,
1233 captured_pid: Arc<std::sync::atomic::AtomicU32>,
1234) -> io::Result<OwnedChild> {
1235 OwnedCommand::from_command(command).spawn_inner(false, true, Some(captured_pid))
1236}
1237
1238#[cfg(windows)]
1239fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1240 use windows::Win32::{
1241 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
1242 System::Threading::{
1243 OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
1244 PROCESS_SYNCHRONIZE,
1245 },
1246 };
1247
1248 let handle = unsafe {
1249 OpenProcess(
1250 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
1251 false,
1252 pid.get(),
1253 )
1254 };
1255 let handle = match handle {
1256 Ok(handle) => handle,
1257 Err(error) => {
1258 if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
1259 return Ok(false);
1260 }
1261 return Err(io::Error::other(error.to_string()));
1262 }
1263 };
1264 let wait = unsafe { WaitForSingleObject(handle, 0) };
1265 let close = unsafe { CloseHandle(handle) };
1266 close.map_err(|error| io::Error::other(error.to_string()))?;
1267 match wait.0 {
1268 0 => Ok(false),
1269 value if value == WAIT_TIMEOUT.0 => Ok(true),
1270 _ => Err(io::Error::last_os_error()),
1271 }
1272}
1273
1274#[cfg(unix)]
1275fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1276 let pid = i32::try_from(pid.get()).map_err(io::Error::other)?;
1277 loop {
1278 if unsafe { libc::kill(pid, 0) } == 0 {
1280 return Ok(true);
1281 }
1282 let error = io::Error::last_os_error();
1283 match error.raw_os_error() {
1284 Some(libc::EINTR) => continue,
1285 Some(libc::EPERM) => return Ok(true),
1286 Some(libc::ESRCH) => return Ok(false),
1287 _ => return Err(error),
1288 }
1289 }
1290}
1291
1292#[cfg(test)]
1293mod tests {
1294 #[test]
1295 fn option_defaults_preserve_existing_behavior() {
1296 let command = super::OwnedCommand::new("unused");
1297 assert_eq!(command.job_assignment, super::JobAssignmentMode::Strict);
1298 assert_eq!(
1299 command.unix_containment,
1300 super::UnixContainment::ProcessGroup
1301 );
1302 assert_eq!(command.partial_spawn_cleanup_timeout, None);
1303 assert_eq!(
1304 super::TerminationOptions::default().grace,
1305 std::time::Duration::ZERO
1306 );
1307 assert_eq!(super::TerminationOptions::default().windows_exit_code, 1);
1308 }
1309
1310 #[cfg(not(windows))]
1311 #[test]
1312 fn new_and_from_command_share_spawn_settings() {
1313 use super::OwnedCommand;
1314 use std::process::Command;
1315 let by_new = OwnedCommand::new("unused");
1317 let by_from = OwnedCommand::from_command(Command::new("unused"));
1318 assert_eq!(format!("{by_new:?}"), format!("{by_from:?}"));
1319 }
1320
1321 #[cfg(windows)]
1325 #[test]
1326 fn only_new_supplies_a_default_windows_spawn_config() {
1327 use super::OwnedCommand;
1328 use std::process::Command;
1329 let by_new = OwnedCommand::new("unused");
1330 let by_from = OwnedCommand::from_command(Command::new("unused"));
1331 assert!(by_new.windows_spawn_config.is_some() && by_new.windows_spawn_config_auto);
1332 assert!(by_from.windows_spawn_config.is_none() && !by_from.windows_spawn_config_auto);
1333 }
1334
1335 #[test]
1336 fn process_priority_defaults_to_normal_and_keeps_the_requested_value() {
1337 use super::{OwnedCommand, ProcessPriority};
1338 let mut command = OwnedCommand::new("unused");
1339 assert_eq!(command.process_priority, ProcessPriority::Normal);
1340 command.process_priority(ProcessPriority::BelowNormal);
1341 assert_eq!(command.process_priority, ProcessPriority::BelowNormal);
1342 command.process_priority(ProcessPriority::Idle);
1343 assert_eq!(command.process_priority, ProcessPriority::Idle);
1344 }
1345
1346 #[cfg(unix)]
1349 #[test]
1350 fn process_priority_maps_to_nice_floors() {
1351 use super::ProcessPriority;
1352 assert_eq!(ProcessPriority::Normal.nice_floor(), None);
1353 assert_eq!(ProcessPriority::BelowNormal.nice_floor(), Some(10));
1354 assert_eq!(ProcessPriority::Idle.nice_floor(), Some(19));
1355 assert_eq!(ProcessPriority::Efficient.nice_floor(), Some(10));
1356 }
1357
1358 #[cfg(windows)]
1360 #[test]
1361 fn below_normal_child_has_below_normal_priority_class() {
1362 use super::{OwnedCommand, ProcessPriority};
1363 use std::process::Stdio;
1364 use windows::Win32::{
1365 Foundation::CloseHandle,
1366 System::Threading::{
1367 GetPriorityClass, OpenProcess, BELOW_NORMAL_PRIORITY_CLASS,
1368 PROCESS_QUERY_LIMITED_INFORMATION,
1369 },
1370 };
1371 let mut command = OwnedCommand::new("ping");
1372 command
1373 .command_mut()
1374 .args(["-n", "6", "127.0.0.1"])
1375 .stdin(Stdio::null())
1376 .stdout(Stdio::null())
1377 .stderr(Stdio::null());
1378 command.process_priority(ProcessPriority::BelowNormal);
1379 let mut child = command.spawn().expect("spawn ping");
1380 let class = unsafe {
1382 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, child.id())
1383 .expect("open the child");
1384 let class = GetPriorityClass(handle);
1385 let _ = CloseHandle(handle);
1386 class
1387 };
1388 let _ = child.terminate_tree();
1389 assert_eq!(class, BELOW_NORMAL_PRIORITY_CLASS.0);
1390 }
1391
1392 #[cfg(unix)]
1394 #[test]
1395 fn lowered_priority_child_runs_at_or_above_the_nice_floor() {
1396 use super::{OwnedCommand, ProcessPriority};
1397 use std::{io::Read, process::Stdio};
1398 for (priority, floor) in [
1399 (ProcessPriority::BelowNormal, 10),
1400 (ProcessPriority::Idle, 19),
1401 ] {
1402 let mut command = OwnedCommand::new("/bin/sh");
1403 command
1404 .command_mut()
1405 .args(["-c", "ps -o ni= -p $$"])
1406 .stdin(Stdio::null())
1407 .stdout(Stdio::piped())
1408 .stderr(Stdio::null());
1409 command.process_priority(priority);
1410 let mut child = command.spawn().expect("spawn sh");
1411 let mut text = String::new();
1412 child
1413 .take_stdout()
1414 .expect("piped stdout")
1415 .read_to_string(&mut text)
1416 .expect("read ps output");
1417 let _ = child.wait();
1418 let nice: i32 = text.trim().parse().expect("ps printed a nice value");
1419 assert!(nice >= floor, "nice {nice} below floor {floor}");
1420 }
1421 }
1422
1423 #[test]
1424 fn kill_on_parent_exit_is_opt_in() {
1425 let mut command = super::OwnedCommand::new("unused");
1426 assert!(!command.kill_on_parent_exit);
1427 command.kill_on_parent_exit(true);
1428 assert!(command.kill_on_parent_exit);
1429 }
1430
1431 #[test]
1432 fn breakaway_denied_is_a_typed_recoverable_error() {
1433 let error =
1434 std::io::Error::new(std::io::ErrorKind::PermissionDenied, super::BreakawayDenied);
1435 assert!(error
1436 .get_ref()
1437 .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1438 .is_some());
1439 assert!(super::BreakawayDenied.to_string().contains("breakaway"));
1440 }
1441
1442 #[test]
1443 fn a_strict_detached_spawn_passes_other_failures_through_untyped() {
1444 let command = super::OwnedCommand::new("rightkit-no-such-program-for-strict-detach");
1445 let Err(error) = command.spawn_uncontained_detached_strict() else {
1446 panic!("a missing program must not spawn");
1447 };
1448 assert!(error
1449 .get_ref()
1450 .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1451 .is_none());
1452 }
1453
1454 #[test]
1455 fn a_strict_detached_spawn_still_rejects_kill_on_parent_exit() {
1456 let mut command = super::OwnedCommand::new("unused");
1457 command.kill_on_parent_exit(true);
1458 let Err(error) = command.spawn_uncontained_detached_strict() else {
1459 panic!("a detached spawn must not accept the parent-death tie");
1460 };
1461 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1462 }
1463
1464 #[test]
1465 fn a_detached_spawn_rejects_kill_on_parent_exit() {
1466 let mut command = super::OwnedCommand::new("unused");
1468 command.kill_on_parent_exit(true);
1469 let Err(error) = command.spawn_uncontained_detached() else {
1470 panic!("a detached spawn must not accept the parent-death tie");
1471 };
1472 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1473 }
1474
1475 #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
1478 #[test]
1479 fn a_host_without_a_parent_death_primitive_refuses_before_starting_anything() {
1480 let marker =
1481 std::env::temp_dir().join(format!("rightkit-process-untied-{}", std::process::id()));
1482 let _ = std::fs::remove_file(&marker);
1483 let mut command = super::OwnedCommand::new("/usr/bin/touch");
1484 command.command_mut().arg(&marker);
1485 command.kill_on_parent_exit(true);
1486 let Err(error) = command.spawn() else {
1487 panic!("kill_on_parent_exit must not run the child untied");
1488 };
1489 assert_eq!(error.kind(), std::io::ErrorKind::Unsupported);
1490 std::thread::sleep(std::time::Duration::from_millis(200));
1491 assert!(!marker.exists(), "the program ran");
1492 }
1493
1494 #[test]
1495 fn option_builders_keep_every_requested_value() {
1496 use super::{JobAssignmentMode, OwnedCommand, TerminationOptions, UnixContainment};
1497 use std::time::Duration;
1498 let mut command = OwnedCommand::new("unused");
1499 command
1500 .job_assignment_mode(JobAssignmentMode::BestEffort)
1501 .unix_containment(UnixContainment::Session)
1502 .partial_spawn_cleanup_timeout(Duration::from_secs(5));
1503 assert_eq!(command.job_assignment, JobAssignmentMode::BestEffort);
1504 assert_eq!(command.unix_containment, UnixContainment::Session);
1505 assert_eq!(
1506 command.partial_spawn_cleanup_timeout,
1507 Some(Duration::from_secs(5))
1508 );
1509 let options = TerminationOptions::default()
1510 .with_grace(Duration::from_millis(100))
1511 .with_windows_exit_code(1067);
1512 assert_eq!(options.grace, Duration::from_millis(100));
1513 assert_eq!(options.windows_exit_code, 1067);
1514 }
1515
1516 #[test]
1517 fn zero_bound_partial_spawn_failure_still_reaps_the_child() {
1518 use std::{
1519 num::NonZeroU32,
1520 process::Command,
1521 sync::{
1522 atomic::{AtomicU32, Ordering},
1523 Arc,
1524 },
1525 thread,
1526 time::{Duration, Instant},
1527 };
1528 let command = if cfg!(windows) {
1529 let mut command = Command::new("powershell.exe");
1530 command.args([
1531 "-NoLogo",
1532 "-NoProfile",
1533 "-NonInteractive",
1534 "-Command",
1535 "Start-Sleep -Seconds 30",
1536 ]);
1537 command
1538 } else {
1539 let mut command = Command::new("/bin/sh");
1540 command.args(["-c", "exec sleep 30"]);
1541 command
1542 };
1543 let mut command = super::OwnedCommand::from_command(command);
1544 command.partial_spawn_cleanup_timeout(Duration::ZERO);
1545 let captured = Arc::new(AtomicU32::new(0));
1546 let started = Instant::now();
1547 assert!(command
1548 .spawn_inner(false, true, Some(Arc::clone(&captured)))
1549 .is_err());
1550 assert!(started.elapsed() < Duration::from_secs(3));
1551 let pid = captured.load(Ordering::SeqCst);
1552 assert_ne!(pid, 0);
1553 let process = super::AdoptedProcess::new(NonZeroU32::new(pid).unwrap());
1554 let started = Instant::now();
1555 while process.is_running().unwrap() {
1556 assert!(started.elapsed() < Duration::from_secs(5));
1557 thread::sleep(Duration::from_millis(5));
1558 }
1559 }
1560
1561 #[test]
1562 fn owned_commands_hide_their_console_window_by_default() {
1563 assert!(super::OwnedCommand::new("x").windows_hidden);
1564 assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
1565 }
1566
1567 #[cfg(windows)]
1569 fn host_job() -> std::os::windows::io::OwnedHandle {
1570 use std::os::windows::io::FromRawHandle;
1571 use windows::Win32::System::JobObjects::{
1572 CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject,
1573 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
1574 };
1575 let job = unsafe { CreateJobObjectW(None, None) }.unwrap();
1576 let job_handle = unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(job.0 as _) };
1577 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
1578 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
1579 unsafe {
1580 SetInformationJobObject(
1581 job,
1582 JobObjectExtendedLimitInformation,
1583 &info as *const _ as *const _,
1584 std::mem::size_of_val(&info) as u32,
1585 )
1586 }
1587 .unwrap();
1588 job_handle
1589 }
1590
1591 #[cfg(windows)]
1592 fn in_job(pid: u32, job: &std::os::windows::io::OwnedHandle) -> bool {
1593 use std::os::windows::io::AsRawHandle;
1594 #[link(name = "kernel32")]
1595 unsafe extern "system" {
1596 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> *mut std::ffi::c_void;
1597 fn IsProcessInJob(
1598 process: *mut std::ffi::c_void,
1599 job: *mut std::ffi::c_void,
1600 result: *mut i32,
1601 ) -> i32;
1602 fn CloseHandle(handle: *mut std::ffi::c_void) -> i32;
1603 }
1604 unsafe {
1605 let process = OpenProcess(0x1000, 0, pid); assert!(!process.is_null());
1607 let mut inside = 0;
1608 assert_ne!(
1609 IsProcessInJob(process, job.as_raw_handle() as _, &mut inside),
1610 0
1611 );
1612 CloseHandle(process);
1613 inside != 0
1614 }
1615 }
1616
1617 #[cfg(windows)]
1618 #[test]
1619 fn children_join_the_callers_job_and_die_when_the_caller_closes_it() {
1620 use std::os::windows::io::AsHandle;
1621 use std::time::{Duration, Instant};
1622 let job = host_job();
1623 let sleeper = || {
1624 let mut command = super::OwnedCommand::new("powershell.exe");
1625 command.command_mut().args([
1626 "-NoLogo",
1627 "-NoProfile",
1628 "-NonInteractive",
1629 "-Command",
1630 "Start-Sleep -Seconds 60",
1631 ]);
1632 command.windows_job(job.as_handle()).unwrap();
1633 command.spawn().unwrap()
1634 };
1635 let (first, second) = (sleeper(), sleeper());
1638 let pids = [first.id(), second.id()];
1639 assert!(
1640 pids.iter().all(|pid| in_job(*pid, &job)),
1641 "children must be in the caller's job"
1642 );
1643
1644 drop(job);
1646 let deadline = Instant::now() + Duration::from_secs(5);
1647 for pid in pids {
1648 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1649 while process.is_running().unwrap_or(false) {
1650 assert!(
1651 Instant::now() < deadline,
1652 "child {pid} survived its caller's job"
1653 );
1654 std::thread::sleep(Duration::from_millis(20));
1655 }
1656 }
1657 drop((first, second));
1658 }
1659
1660 #[test]
1661 fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
1662 use std::{
1663 process::Command,
1664 sync::{
1665 atomic::{AtomicU32, Ordering},
1666 Arc,
1667 },
1668 thread,
1669 time::{Duration, Instant},
1670 };
1671
1672 let command = if cfg!(windows) {
1674 let mut command = Command::new("powershell.exe");
1676 command.args([
1677 "-NoLogo",
1678 "-NoProfile",
1679 "-NonInteractive",
1680 "-Command",
1681 "Start-Sleep -Seconds 60",
1682 ]);
1683 command
1684 } else {
1685 let mut command = Command::new("sleep");
1687 command.arg("60");
1688 command
1689 };
1690 let spawned_pid = Arc::new(AtomicU32::new(0));
1691
1692 assert!(super::spawn_for_test(command, Arc::clone(&spawned_pid)).is_err());
1693 let pid = spawned_pid.load(Ordering::SeqCst);
1694 assert_ne!(pid, 0, "test must fail after the OS process was spawned");
1695 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1696 let deadline = Instant::now() + Duration::from_secs(5);
1697 while process.is_running().unwrap_or(false) {
1698 assert!(
1699 Instant::now() < deadline,
1700 "partially spawned process leaked"
1701 );
1702 thread::sleep(Duration::from_millis(20));
1703 }
1704 }
1705}