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(Clone, Copy, Debug, Default, Eq, PartialEq)]
99pub(crate) enum BreakawayMode {
100 #[default]
102 None,
103 Explicit,
105 Silent,
107}
108
109#[derive(Debug)]
110pub struct JobAssignmentFailure {
111 pub stage: &'static str,
112 pub error: io::Error,
113}
114impl JobAssignmentFailure {
115 pub fn raw_os_error(&self) -> Option<i32> {
116 self.error.raw_os_error()
117 }
118}
119
120#[cfg(windows)]
122pub type RawHandleOrFd = std::os::windows::io::RawHandle;
123#[cfg(unix)]
124pub type RawHandleOrFd = std::os::fd::RawFd;
125#[cfg(windows)]
126type InheritedValue = usize;
127#[cfg(unix)]
128type InheritedValue = RawHandleOrFd;
129
130#[derive(Debug)]
131pub struct OwnedCommand {
132 command: Command,
133 windows_hidden: bool,
134 windows_breakaway: BreakawayMode,
135 caller_job: CallerJob,
136 job_assignment: JobAssignmentMode,
137 unix_containment: UnixContainment,
138 process_priority: ProcessPriority,
139 kill_on_parent_exit: bool,
140 hold_suspended: bool,
142 partial_spawn_cleanup_timeout: Option<Duration>,
144 inherited_handles: Option<Vec<InheritedValue>>,
145 #[cfg(windows)]
146 windows_spawn_config: Option<crate::WindowsSpawnConfig>,
147 #[cfg(windows)]
148 windows_spawn_config_auto: bool,
149 env_cleared: bool,
150}
151#[cfg(windows)]
152type CallerJob = Option<std::os::windows::io::OwnedHandle>;
153#[cfg(not(windows))]
154type CallerJob = Option<std::convert::Infallible>;
155
156impl OwnedCommand {
157 pub fn new(program: impl AsRef<OsStr>) -> Self {
158 let command = Self::from_command(Command::new(program));
159 #[cfg(windows)]
160 let command = {
161 let mut command = command;
162 command.windows_spawn_config = Some(crate::WindowsSpawnConfig::default());
165 command.windows_spawn_config_auto = true;
166 command
167 };
168 command
169 }
170 pub fn from_command(command: Command) -> Self {
176 Self {
177 command,
178 windows_hidden: true,
179 windows_breakaway: BreakawayMode::None,
180 caller_job: None,
181 job_assignment: JobAssignmentMode::Strict,
182 unix_containment: UnixContainment::ProcessGroup,
183 process_priority: ProcessPriority::Normal,
184 kill_on_parent_exit: false,
185 hold_suspended: false,
186 partial_spawn_cleanup_timeout: None,
187 inherited_handles: None,
188 #[cfg(windows)]
189 windows_spawn_config: None,
190 #[cfg(windows)]
191 windows_spawn_config_auto: false,
192 env_cleared: false,
193 }
194 }
195 pub fn command_mut(&mut self) -> &mut Command {
196 #[cfg(windows)]
197 if self.windows_spawn_config_auto {
198 self.windows_spawn_config = None;
200 }
201 &mut self.command
202 }
203 pub fn inherit_handles_only(
205 &mut self,
206 handles: impl IntoIterator<Item = RawHandleOrFd>,
207 ) -> &mut Self {
208 #[cfg(windows)]
209 let handles = handles.into_iter().map(|handle| handle as usize).collect();
210 #[cfg(unix)]
211 let handles = handles.into_iter().collect();
212 self.inherited_handles = Some(handles);
213 self
214 }
215 #[cfg(windows)]
219 pub fn windows_spawn_config(&mut self, config: crate::WindowsSpawnConfig) -> &mut Self {
220 self.windows_spawn_config = Some(config);
221 self.windows_spawn_config_auto = false;
222 self
223 }
224 pub fn env_allowlist<I, S>(&mut self, keys: I) -> &mut Self
226 where
227 I: IntoIterator<Item = S>,
228 S: AsRef<OsStr>,
229 {
230 let kept: Vec<_> = keys
231 .into_iter()
232 .filter_map(|k| std::env::var_os(k.as_ref()).map(|v| (k.as_ref().to_os_string(), v)))
233 .collect();
234 self.command.env_clear();
235 self.env_cleared = true;
236 for (k, v) in kept {
237 self.command.env(k, v);
238 }
239 self
240 }
241 pub fn env_allowlist_with_defaults<I, S>(&mut self, extra: I) -> &mut Self
245 where
246 I: IntoIterator<Item = S>,
247 S: AsRef<OsStr>,
248 {
249 let keys: Vec<std::ffi::OsString> = DEFAULT_ENV_ALLOWLIST
250 .iter()
251 .map(std::ffi::OsString::from)
252 .chain(extra.into_iter().map(|k| k.as_ref().to_os_string()))
253 .collect();
254 self.env_allowlist(keys)
255 }
256 pub fn env_is_cleared(&self) -> bool {
261 self.env_cleared
262 }
263 pub fn env_strip<I, S>(&mut self, keys: I) -> &mut Self
264 where
265 I: IntoIterator<Item = S>,
266 S: AsRef<OsStr>,
267 {
268 for k in keys {
269 self.command.env_remove(k);
270 }
271 self
272 }
273 pub fn windows_hide(&mut self) -> &mut Self {
275 self.windows_hidden = true;
276 self
277 }
278 pub fn windows_visible(&mut self) -> &mut Self {
283 self.windows_hidden = false;
284 self
285 }
286 pub fn windows_allow_breakaway(&mut self, allow: bool) -> &mut Self {
293 self.windows_breakaway = if allow {
294 BreakawayMode::Explicit
295 } else {
296 BreakawayMode::None
297 };
298 self
299 }
300 pub fn windows_allow_silent_breakaway(&mut self, allow: bool) -> &mut Self {
305 self.windows_breakaway = if allow {
306 BreakawayMode::Silent
307 } else {
308 BreakawayMode::None
309 };
310 self
311 }
312 #[cfg(windows)]
314 pub fn windows_job(
315 &mut self,
316 job: std::os::windows::io::BorrowedHandle<'_>,
317 ) -> io::Result<&mut Self> {
318 self.caller_job = Some(job.try_clone_to_owned()?);
319 Ok(self)
320 }
321 pub fn job_assignment_mode(&mut self, mode: JobAssignmentMode) -> &mut Self {
323 self.job_assignment = mode;
324 self
325 }
326 pub fn unix_containment(&mut self, mode: UnixContainment) -> &mut Self {
328 self.unix_containment = mode;
329 self
330 }
331 pub fn process_priority(&mut self, priority: ProcessPriority) -> &mut Self {
335 self.process_priority = priority;
336 self
337 }
338 pub fn kill_on_parent_exit(&mut self, enabled: bool) -> &mut Self {
360 self.kill_on_parent_exit = enabled;
361 self
362 }
363 pub fn partial_spawn_cleanup_timeout(&mut self, timeout: Duration) -> &mut Self {
365 self.partial_spawn_cleanup_timeout = Some(timeout);
366 self
367 }
368 pub fn spawn(self) -> io::Result<OwnedChild> {
369 self.spawn_inner(false, false, None)
370 }
371 pub fn spawn_suspended(mut self) -> io::Result<OwnedChild> {
381 self.hold_suspended = true;
382 self.spawn_inner(false, false, None)
383 }
384 pub fn spawn_uncontained_detached(self) -> io::Result<OwnedChild> {
399 self.spawn_inner_with(true, false, None, true)
400 }
401 pub fn spawn_uncontained_detached_strict(self) -> io::Result<OwnedChild> {
407 match self.spawn_inner_with(true, false, None, false) {
408 Err(error) if cfg!(windows) && is_denied_breakaway(&error) => Err(io::Error::new(
409 io::ErrorKind::PermissionDenied,
410 BreakawayDenied,
411 )),
412 other => other,
413 }
414 }
415 fn spawn_inner(
416 self,
417 detached: bool,
418 fail_after_spawn: bool,
419 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
420 ) -> io::Result<OwnedChild> {
421 self.spawn_inner_with(detached, fail_after_spawn, captured_pid, false)
422 }
423 fn spawn_inner_with(
424 mut self,
425 detached: bool,
426 fail_after_spawn: bool,
427 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
428 in_job_fallback: bool,
429 ) -> io::Result<OwnedChild> {
430 if detached && self.caller_job.is_some() {
431 return Err(io::Error::new(
432 io::ErrorKind::InvalidInput,
433 "detached launch cannot join a caller job",
434 ));
435 }
436 if detached && self.kill_on_parent_exit {
437 return Err(io::Error::new(
438 io::ErrorKind::InvalidInput,
439 "kill_on_parent_exit cannot combine with a detached launch",
440 ));
441 }
442 #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
443 if self.kill_on_parent_exit {
444 return Err(io::Error::new(
445 io::ErrorKind::Unsupported,
446 "kill_on_parent_exit is supported on Linux and macOS only",
447 ));
448 }
449 #[cfg(unix)]
450 {
451 use std::os::unix::process::CommandExt;
452 let session = detached || self.unix_containment == UnixContainment::Session;
453 let nice_floor = self.process_priority.nice_floor();
454 #[cfg(target_os = "macos")]
455 let utility_qos = self.process_priority == ProcessPriority::Efficient;
456 let inheritance = self
457 .inherited_handles
458 .take()
459 .map(crate::process_unix::Inheritance::new)
460 .transpose()?;
461 #[cfg(target_os = "linux")]
463 let supervisor = self
464 .kill_on_parent_exit
465 .then(|| unsafe { libc::getpid() });
467 unsafe {
469 self.command.pre_exec(move || {
470 let result = if session {
471 libc::setsid()
472 } else {
473 libc::setpgid(0, 0)
474 };
475 if result < 0 {
476 return Err(io::Error::last_os_error());
477 }
478 if let Some(floor) = nice_floor {
479 crate::process_unix::raise_nice_to(floor)?;
480 }
481 #[cfg(target_os = "macos")]
482 if utility_qos {
483 crate::process_unix::set_utility_qos();
484 }
485 if let Some(inheritance) = &inheritance {
486 inheritance.apply()?;
487 }
488 #[cfg(target_os = "linux")]
489 if let Some(supervisor) = supervisor {
490 crate::process_unix::die_with_parent(supervisor)?;
491 }
492 Ok(())
493 });
494 }
495 }
496 #[cfg(windows)]
497 crate::process_windows::configure(
498 &mut self.command,
499 detached,
500 self.windows_hidden,
501 self.process_priority,
502 );
503 #[cfg(unix)]
504 let _ = (
505 self.windows_hidden,
506 self.windows_breakaway,
507 self.job_assignment,
508 );
509 #[cfg(windows)]
510 let _ = (self.unix_containment, self.kill_on_parent_exit);
511 let spawned_at = SystemTime::now();
512 let hold_suspended = self.hold_suspended && !detached;
513 #[cfg(unix)]
514 let _ = hold_suspended;
515 #[cfg(unix)]
516 let child = {
517 let mut child = crate::unix_child::UnixChild::new(self.command.spawn()?);
518 child.require_identity()?;
521 #[cfg(target_os = "macos")]
524 if self.kill_on_parent_exit {
525 if let Err(error) = crate::process_unix::arm_parent_death_watch(child.id()) {
526 let _ = child.signal(SignalTarget::Group, libc::SIGKILL);
527 let _ = child.kill();
528 let _ = child.wait();
529 return Err(error);
530 }
531 }
532 child
533 };
534 #[cfg(windows)]
535 let (child, stayed_in_parent_job) = match self.inherited_handles.as_deref() {
536 None => {
537 let _spawning = crate::spawn_windows::spawn_guard();
540 match self.command.spawn() {
541 Ok(child) => (PlatformChild::from(child), false),
542 Err(error)
544 if detached
545 && in_job_fallback
546 && crate::process_windows::is_breakaway_denied(&error) =>
547 {
548 crate::process_windows::configure_in_job_detached(
549 &mut self.command,
550 self.process_priority,
551 );
552 (PlatformChild::from(self.command.spawn()?), true)
553 }
554 Err(error) => return Err(error),
555 }
556 }
557 Some(handles) => {
558 let config = self.windows_spawn_config.take().ok_or_else(|| {
559 io::Error::new(io::ErrorKind::InvalidInput,
560 "allowlisted Windows spawn requires windows_spawn_config for opaque Command settings")
561 })?;
562 crate::spawn_windows::spawn(
563 &self.command,
564 config,
565 handles,
566 detached,
567 self.windows_hidden,
568 self.process_priority,
569 self.env_cleared,
570 self.partial_spawn_cleanup_timeout,
571 in_job_fallback,
572 )?
573 }
574 };
575 #[cfg(not(windows))]
576 let _ = in_job_fallback;
577 #[cfg(not(windows))]
578 let stayed_in_parent_job = false;
579 let pid = child.id();
580 if let Some(captured) = captured_pid {
581 captured.store(pid, std::sync::atomic::Ordering::SeqCst);
582 }
583 let child = Arc::new(Mutex::new(child));
584 let handle = match ProcessHandle::new(&child, spawned_at) {
585 Ok(handle) => handle,
586 Err(error) => {
587 cleanup_direct(child, self.partial_spawn_cleanup_timeout);
588 return Err(error);
589 }
590 };
591 let mut partial = PartialChild {
592 child: Some(OwnedChild {
593 child,
594 handle,
595 pid,
596 detached,
597 terminate_on_drop: !detached,
598 drop_reap_timeout: DEFAULT_DROP_REAP_TIMEOUT,
599 tree_terminated: false,
600 assignment_failures: Vec::new(),
601 suspended: std::sync::atomic::AtomicBool::new(cfg!(windows) && hold_suspended),
602 stayed_in_parent_job,
603 #[cfg(windows)]
604 job: None,
605 }),
606 timeout: self.partial_spawn_cleanup_timeout,
607 };
608 #[cfg(windows)]
609 if !detached {
610 let owned = partial.child.as_mut().expect("partial child is present");
611 crate::process_windows::assign_jobs(
612 &owned.handle,
613 self.caller_job.take(),
614 self.job_assignment,
615 self.windows_breakaway,
616 &mut owned.job,
617 &mut owned.assignment_failures,
618 )?;
619 }
620 #[cfg(windows)]
621 {
622 let owned = partial.child.as_ref().expect("partial child is present");
623 if self.process_priority == ProcessPriority::Efficient {
624 let _ = crate::process_windows::apply_efficiency(&*owned.child());
626 }
627 if !hold_suspended {
628 owned.child().resume(detached)?;
629 }
630 }
631 if fail_after_spawn {
632 return Err(io::Error::other("injected post-spawn wrapping failure"));
633 }
634 let owned = partial.child.take().expect("partial child is present");
635 if !detached {
636 #[cfg(unix)]
638 crate::registry::register(&owned.child, owned.pid);
639 #[cfg(windows)]
640 crate::registry::register(
641 &owned.child,
642 &owned.handle,
643 owned
644 .job
645 .as_ref()
646 .map_or_else(std::sync::Weak::new, std::sync::Arc::downgrade),
647 owned.pid,
648 );
649 }
650 Ok(owned)
651 }
652}
653
654#[derive(Clone, Copy, Debug, Eq, PartialEq)]
660pub struct BreakawayDenied;
661
662impl std::fmt::Display for BreakawayDenied {
663 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
664 f.write_str(
665 "detached launch denied: the caller's job object does not allow breakaway \
666 (ERROR_ACCESS_DENIED); no fallback was attempted",
667 )
668 }
669}
670
671impl std::error::Error for BreakawayDenied {}
672
673#[cfg(windows)]
674fn is_denied_breakaway(error: &io::Error) -> bool {
675 crate::process_windows::is_breakaway_denied(error)
676}
677#[cfg(not(windows))]
678fn is_denied_breakaway(_error: &io::Error) -> bool {
679 false
680}
681
682pub const DEFAULT_DROP_REAP_TIMEOUT: Duration = Duration::from_secs(2);
685
686pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
687 "PATH",
688 "HOME",
689 "USER",
690 "LOGNAME",
691 "LANG",
692 "LC_ALL",
693 "TMPDIR",
694 "TEMP",
695 "TMP",
696 "SystemRoot",
697 "SYSTEMROOT",
698 "USERPROFILE",
699 "APPDATA",
700 "LOCALAPPDATA",
701 "ProgramData",
702 "COMSPEC",
703 "PATHEXT",
704 "XDG_DATA_HOME",
705 "XDG_RUNTIME_DIR",
706];
707
708#[derive(Debug)]
709pub struct OwnedChild {
710 child: Arc<Mutex<PlatformChild>>,
711 handle: ProcessHandle,
712 pid: u32,
714 detached: bool,
715 terminate_on_drop: bool,
716 drop_reap_timeout: Duration,
717 tree_terminated: bool,
718 assignment_failures: Vec<JobAssignmentFailure>,
719 suspended: std::sync::atomic::AtomicBool,
721 stayed_in_parent_job: bool,
723 #[cfg(windows)]
726 job: Option<std::sync::Arc<crate::process_windows::KillOnCloseJob>>,
727}
728#[derive(Clone, Debug, Eq, PartialEq)]
730pub struct TreeTermination {
731 pub status: ExitStatus,
733 pub descendants: crate::SweepReport,
735}
736#[derive(Clone, Copy, Debug, Eq, PartialEq)]
737pub enum ReapOutcome {
738 Exited(ExitStatus),
739 TimedOut,
740}
741#[derive(Clone, Copy, Debug, Eq, PartialEq)]
742pub enum WaitOutcome {
743 Exited(ExitStatus),
744 Terminated(ExitStatus),
745}
746
747impl OwnedChild {
748 pub fn id(&self) -> u32 {
749 self.pid
750 }
751 pub fn stayed_in_parent_job(&self) -> bool {
756 self.stayed_in_parent_job
757 }
758 pub fn child(&self) -> MutexGuard<'_, PlatformChild> {
774 lock_child(&self.child)
775 }
776 pub fn resume(&self) -> io::Result<()> {
781 if self
782 .suspended
783 .swap(false, std::sync::atomic::Ordering::SeqCst)
784 {
785 #[cfg(windows)]
786 if let Err(error) = self.child().resume(self.detached) {
787 self.suspended
788 .store(true, std::sync::atomic::Ordering::SeqCst);
789 return Err(error);
790 }
791 }
792 Ok(())
793 }
794 pub fn is_suspended(&self) -> bool {
796 self.suspended.load(std::sync::atomic::Ordering::SeqCst)
797 }
798 pub fn while_unreaped<R>(&self, act: impl FnOnce(u32) -> R) -> io::Result<Option<R>> {
807 #[cfg(unix)]
808 {
809 let mut child = lock_child(&self.child);
810 if child.leader_state()? == LeaderState::Reaped {
811 return Ok(None);
812 }
813 Ok(Some(act(self.pid)))
814 }
815 #[cfg(windows)]
816 Ok(Some(act(self.pid)))
817 }
818 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
819 self.child().stdin.take()
820 }
821 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
822 self.child().stdout.take()
823 }
824 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
825 self.child().stderr.take()
826 }
827 pub fn job_assignment_failures(&self) -> &[JobAssignmentFailure] {
828 &self.assignment_failures
829 }
830 pub fn process_handle(&self) -> io::Result<ProcessHandle> {
834 self.handle.try_clone()
835 }
836 pub fn creation_time(&self) -> Option<SystemTime> {
837 self.handle.creation_time()
838 }
839 #[cfg(windows)]
840 pub fn creation_time_ticks(&self) -> Option<u64> {
841 self.handle.creation_time_ticks()
842 }
843 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
844 self.handle.try_wait()
846 }
847 pub fn wait(&mut self) -> io::Result<ExitStatus> {
850 drop(self.child().stdin.take());
851 self.handle.wait()
852 }
853 pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
854 self.handle.wait_timeout(timeout)
855 }
856 pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
866 self.wait_or_kill_inner(grace, true)
867 }
868 pub fn wait_then_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
874 self.wait_or_kill_inner(grace, false)
875 }
876 fn wait_or_kill_inner(&mut self, grace: Duration, term_first: bool) -> io::Result<WaitOutcome> {
877 #[cfg(not(unix))]
878 let _ = term_first;
879 #[cfg(unix)]
880 {
881 if term_first && !self.tree_terminated {
882 self.signal(libc::SIGTERM)?;
883 }
884 if self.handle.wait_exited_timeout(grace)? {
886 self.force_kill(1)?;
891 return self.wait().map(WaitOutcome::Exited);
892 }
893 self.terminate_tree().map(WaitOutcome::Terminated)
894 }
895 #[cfg(windows)]
896 {
897 if let Some(status) = self.wait_timeout(grace)? {
898 self.force_kill(1)?;
902 return Ok(WaitOutcome::Exited(status));
903 }
904 self.terminate_tree().map(WaitOutcome::Terminated)
905 }
906 }
907 pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
912 self.force_kill(1)?;
913 self.wait()
914 }
915 pub fn terminate_tree_with_options(
920 &mut self,
921 options: TerminationOptions,
922 ) -> io::Result<ExitStatus> {
923 self.start_termination(options)?;
924 self.wait()
925 }
926 pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
927 self.terminate_tree_bounded_with_options(timeout, TerminationOptions::default())
928 }
929 pub fn terminate_tree_bounded_with_options(
931 &mut self,
932 timeout: Duration,
933 mut options: TerminationOptions,
934 ) -> io::Result<ReapOutcome> {
935 let started = Instant::now();
936 options.grace = options.grace.min(timeout);
937 self.start_termination(options)?;
938 Ok(
939 match self.wait_timeout(timeout.saturating_sub(started.elapsed()))? {
940 Some(status) => ReapOutcome::Exited(status),
941 None => ReapOutcome::TimedOut,
942 },
943 )
944 }
945 pub fn terminate_tree_deep(
969 &mut self,
970 options: TerminationOptions,
971 ) -> io::Result<TreeTermination> {
972 let mut found = self.descendant_identities()?;
973 found.reverse(); #[cfg(unix)]
975 if !self.tree_terminated {
976 signal_shared(&self.child, SignalTarget::Group, libc::SIGTERM)?;
977 await_tree_shared(&self.child, &self.handle, options.grace, true)?;
978 signal_shared(&self.child, SignalTarget::Group, libc::SIGKILL)?;
979 self.tree_terminated = true;
980 }
981 #[cfg(windows)]
982 self.force_kill(options.windows_exit_code)?;
983 let status = self.wait()?;
984 let descendants = crate::sweep(&found, options.grace)?;
985 Ok(TreeTermination {
986 status,
987 descendants,
988 })
989 }
990 fn start_termination(&mut self, options: TerminationOptions) -> io::Result<()> {
991 #[cfg(unix)]
992 if !self.tree_terminated {
993 self.signal(libc::SIGTERM)?;
994 self.await_tree(options.grace)?;
998 }
999 self.force_kill(options.windows_exit_code)
1000 }
1001 #[cfg(unix)]
1008 fn await_tree(&self, grace: Duration) -> io::Result<()> {
1009 await_tree_shared(&self.child, &self.handle, grace, !self.detached)
1011 }
1012 #[cfg(unix)]
1016 fn signal(&self, signal: i32) -> io::Result<()> {
1017 let target = if self.detached {
1020 SignalTarget::Leader
1021 } else {
1022 SignalTarget::Group
1023 };
1024 signal_shared(&self.child, target, signal).map(|_| ())
1025 }
1026 fn force_kill(&mut self, code: u32) -> io::Result<()> {
1027 if self.tree_terminated {
1028 return Ok(());
1029 }
1030 #[cfg(unix)]
1031 {
1032 let _ = code;
1033 self.signal(libc::SIGKILL)?;
1034 }
1035 #[cfg(windows)]
1036 crate::process_windows::terminate(&self.handle, self.job.as_deref(), code)?;
1037 self.tree_terminated = true;
1038 Ok(())
1039 }
1040}
1041impl OwnedChild {
1042 pub fn set_drop_reap_timeout(&mut self, timeout: Duration) -> &mut Self {
1049 self.drop_reap_timeout = timeout;
1050 self
1051 }
1052 pub fn drop_reap_timeout(&self) -> Duration {
1054 self.drop_reap_timeout
1055 }
1056 fn reap_in_background(&self) {
1058 #[cfg(unix)]
1059 if let Ok(reader) = self.handle.try_clone() {
1060 let _ = thread::Builder::new()
1061 .name("rightkit-drop-reap".into())
1062 .spawn(move || {
1063 let _ = reader.wait();
1064 });
1065 }
1066 }
1067 pub fn descendant_identities(&self) -> io::Result<Vec<crate::AdoptedIdentity>> {
1071 let root = self.while_unreaped(|pid| match NonZeroU32::new(pid) {
1072 Some(pid) => crate::AdoptedIdentity::capture(pid),
1073 None => Ok(None),
1074 })?;
1075 match root {
1076 Some(Ok(Some(root))) => root.descendants(),
1077 Some(Err(error)) => Err(error),
1078 _ => Ok(Vec::new()),
1079 }
1080 }
1081}
1082impl Drop for OwnedChild {
1083 fn drop(&mut self) {
1084 crate::registry::forget(&self.child);
1086 if self.terminate_on_drop {
1087 if self.force_kill(1).is_ok() {
1091 drop(self.child().stdin.take());
1092 if !matches!(
1093 self.handle.wait_timeout(self.drop_reap_timeout),
1094 Ok(Some(_))
1095 ) {
1096 self.reap_in_background();
1097 }
1098 }
1099 } else if self.detached {
1100 #[cfg(unix)]
1101 {
1102 {
1106 let mut child = self.child();
1107 drop(child.stdin.take());
1108 drop(child.stdout.take());
1109 drop(child.stderr.take());
1110 }
1111 if let Ok(reader) = self.handle.try_clone() {
1112 let _ = thread::Builder::new()
1113 .name("rightkit-detached-reap".into())
1114 .spawn(move || {
1115 let _ = reader.wait();
1116 });
1117 }
1118 }
1119 }
1120 }
1121}
1122
1123struct PartialChild {
1124 child: Option<OwnedChild>,
1125 timeout: Option<Duration>,
1126}
1127impl Drop for PartialChild {
1128 fn drop(&mut self) {
1129 if let Some(mut child) = self.child.take() {
1130 child.terminate_on_drop = false;
1132 let _ = child.force_kill(1);
1133 cleanup_direct(Arc::clone(&child.child), self.timeout);
1134 }
1135 }
1136}
1137#[cfg(unix)]
1140const MID_EXIT_RETRY: Duration = Duration::from_millis(300);
1141
1142#[cfg(unix)]
1146pub(crate) fn signal_shared(
1147 shared: &Mutex<crate::unix_child::UnixChild>,
1148 target: SignalTarget,
1149 signal: i32,
1150) -> io::Result<SignalOutcome> {
1151 let started = Instant::now();
1152 loop {
1153 let (outcome, watch) = {
1154 let mut child = lock_child(shared);
1155 (child.signal(target, signal)?, child.exit_watch())
1156 };
1157 if outcome != SignalOutcome::MidExit {
1158 return Ok(outcome);
1159 }
1160 let remaining = MID_EXIT_RETRY.saturating_sub(started.elapsed());
1163 if remaining.is_zero() {
1164 return Err(io::Error::from_raw_os_error(libc::EPERM));
1165 }
1166 watch.pause(remaining);
1167 }
1168}
1169
1170#[cfg(unix)]
1172pub(crate) fn await_tree_shared(
1173 shared: &Mutex<crate::unix_child::UnixChild>,
1174 handle: &ProcessHandle,
1175 grace: Duration,
1176 whole_group: bool,
1177) -> io::Result<()> {
1178 use crate::{exit_watch::POLL_INTERVAL, unix_child::Members};
1179 let Some(deadline) = Instant::now().checked_add(grace) else {
1180 return Ok(());
1181 };
1182 let left = |deadline: Instant| deadline.saturating_duration_since(Instant::now());
1183 if !handle.wait_exited_timeout(left(deadline))? {
1184 return Ok(()); }
1186 if !whole_group {
1187 return Ok(());
1188 }
1189 loop {
1190 match lock_child(shared).group_other_members()? {
1191 Members::Pids(pids) if pids.is_empty() => return Ok(()),
1192 Members::Pids(_) => {}
1193 Members::Unknown => return wait_out(left(deadline)),
1194 }
1195 let remaining = left(deadline);
1196 if remaining.is_zero() {
1197 return Ok(()); }
1199 thread::sleep(POLL_INTERVAL.min(remaining));
1200 }
1201}
1202
1203#[cfg(unix)]
1206fn wait_out(remaining: Duration) -> io::Result<()> {
1207 thread::sleep(remaining);
1208 Ok(())
1209}
1210pub(crate) fn cleanup_direct(child: Arc<Mutex<PlatformChild>>, timeout: Option<Duration>) {
1217 let _ = lock_child(&child).kill();
1218 let Some(timeout) = timeout else {
1219 let _ = lock_child(&child).wait();
1221 return;
1222 };
1223 #[cfg(unix)]
1224 {
1225 let watch = lock_child(&child).exit_watch();
1228 let deadline = Instant::now().checked_add(timeout);
1229 if matches!(
1230 watch.wait_for(deadline, || lock_child(&child).try_wait()),
1231 Ok(Some(_))
1232 ) {
1233 return;
1234 }
1235 }
1236 #[cfg(windows)]
1237 {
1238 let started = Instant::now();
1239 loop {
1240 match lock_child(&child).try_wait() {
1241 Ok(Some(_)) => return,
1242 Err(_) => break,
1243 Ok(None) => {}
1244 }
1245 if started.elapsed() >= timeout {
1246 break;
1247 }
1248 thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
1249 }
1250 }
1251 let _ = thread::Builder::new()
1255 .name("rightkit-partial-reap".into())
1256 .spawn(move || {
1257 let _ = lock_child(&child).wait();
1258 });
1259}
1260
1261#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1262pub struct AdoptedProcess {
1263 pid: NonZeroU32,
1264}
1265impl AdoptedProcess {
1266 pub fn new(pid: NonZeroU32) -> Self {
1267 Self { pid }
1268 }
1269 pub fn id(&self) -> u32 {
1270 self.pid.get()
1271 }
1272 pub fn is_running(&self) -> io::Result<bool> {
1273 process_is_running(self.pid)
1274 }
1275 pub fn identity(&self) -> io::Result<Option<crate::AdoptedIdentity>> {
1280 crate::AdoptedIdentity::capture(self.pid)
1281 }
1282}
1283#[cfg(test)]
1284fn spawn_for_test(
1285 command: Command,
1286 captured_pid: Arc<std::sync::atomic::AtomicU32>,
1287) -> io::Result<OwnedChild> {
1288 OwnedCommand::from_command(command).spawn_inner(false, true, Some(captured_pid))
1289}
1290
1291#[cfg(windows)]
1292fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1293 use windows::Win32::{
1294 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
1295 System::Threading::{
1296 OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
1297 PROCESS_SYNCHRONIZE,
1298 },
1299 };
1300
1301 let handle = unsafe {
1302 OpenProcess(
1303 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
1304 false,
1305 pid.get(),
1306 )
1307 };
1308 let handle = match handle {
1309 Ok(handle) => handle,
1310 Err(error) => {
1311 if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
1312 return Ok(false);
1313 }
1314 return Err(io::Error::other(error.to_string()));
1315 }
1316 };
1317 let wait = unsafe { WaitForSingleObject(handle, 0) };
1318 let close = unsafe { CloseHandle(handle) };
1319 close.map_err(|error| io::Error::other(error.to_string()))?;
1320 match wait.0 {
1321 0 => Ok(false),
1322 value if value == WAIT_TIMEOUT.0 => Ok(true),
1323 _ => Err(io::Error::last_os_error()),
1324 }
1325}
1326
1327#[cfg(unix)]
1328fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1329 let pid = i32::try_from(pid.get()).map_err(io::Error::other)?;
1330 loop {
1331 if unsafe { libc::kill(pid, 0) } == 0 {
1333 return Ok(true);
1334 }
1335 let error = io::Error::last_os_error();
1336 match error.raw_os_error() {
1337 Some(libc::EINTR) => continue,
1338 Some(libc::EPERM) => return Ok(true),
1339 Some(libc::ESRCH) => return Ok(false),
1340 _ => return Err(error),
1341 }
1342 }
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347 #[test]
1348 fn option_defaults_preserve_existing_behavior() {
1349 let command = super::OwnedCommand::new("unused");
1350 assert_eq!(command.job_assignment, super::JobAssignmentMode::Strict);
1351 assert_eq!(
1352 command.unix_containment,
1353 super::UnixContainment::ProcessGroup
1354 );
1355 assert_eq!(command.partial_spawn_cleanup_timeout, None);
1356 assert_eq!(
1357 super::TerminationOptions::default().grace,
1358 std::time::Duration::ZERO
1359 );
1360 assert_eq!(super::TerminationOptions::default().windows_exit_code, 1);
1361 }
1362
1363 #[cfg(not(windows))]
1364 #[test]
1365 fn new_and_from_command_share_spawn_settings() {
1366 use super::OwnedCommand;
1367 use std::process::Command;
1368 let by_new = OwnedCommand::new("unused");
1370 let by_from = OwnedCommand::from_command(Command::new("unused"));
1371 assert_eq!(format!("{by_new:?}"), format!("{by_from:?}"));
1372 }
1373
1374 #[cfg(windows)]
1378 #[test]
1379 fn only_new_supplies_a_default_windows_spawn_config() {
1380 use super::OwnedCommand;
1381 use std::process::Command;
1382 let by_new = OwnedCommand::new("unused");
1383 let by_from = OwnedCommand::from_command(Command::new("unused"));
1384 assert!(by_new.windows_spawn_config.is_some() && by_new.windows_spawn_config_auto);
1385 assert!(by_from.windows_spawn_config.is_none() && !by_from.windows_spawn_config_auto);
1386 }
1387
1388 #[test]
1389 fn process_priority_defaults_to_normal_and_keeps_the_requested_value() {
1390 use super::{OwnedCommand, ProcessPriority};
1391 let mut command = OwnedCommand::new("unused");
1392 assert_eq!(command.process_priority, ProcessPriority::Normal);
1393 command.process_priority(ProcessPriority::BelowNormal);
1394 assert_eq!(command.process_priority, ProcessPriority::BelowNormal);
1395 command.process_priority(ProcessPriority::Idle);
1396 assert_eq!(command.process_priority, ProcessPriority::Idle);
1397 }
1398
1399 #[cfg(unix)]
1402 #[test]
1403 fn process_priority_maps_to_nice_floors() {
1404 use super::ProcessPriority;
1405 assert_eq!(ProcessPriority::Normal.nice_floor(), None);
1406 assert_eq!(ProcessPriority::BelowNormal.nice_floor(), Some(10));
1407 assert_eq!(ProcessPriority::Idle.nice_floor(), Some(19));
1408 assert_eq!(ProcessPriority::Efficient.nice_floor(), Some(10));
1409 }
1410
1411 #[cfg(windows)]
1413 #[test]
1414 fn below_normal_child_has_below_normal_priority_class() {
1415 use super::{OwnedCommand, ProcessPriority};
1416 use std::process::Stdio;
1417 use windows::Win32::{
1418 Foundation::CloseHandle,
1419 System::Threading::{
1420 GetPriorityClass, OpenProcess, BELOW_NORMAL_PRIORITY_CLASS,
1421 PROCESS_QUERY_LIMITED_INFORMATION,
1422 },
1423 };
1424 let mut command = OwnedCommand::new("ping");
1425 command
1426 .command_mut()
1427 .args(["-n", "6", "127.0.0.1"])
1428 .stdin(Stdio::null())
1429 .stdout(Stdio::null())
1430 .stderr(Stdio::null());
1431 command.process_priority(ProcessPriority::BelowNormal);
1432 let mut child = command.spawn().expect("spawn ping");
1433 let class = unsafe {
1435 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, child.id())
1436 .expect("open the child");
1437 let class = GetPriorityClass(handle);
1438 let _ = CloseHandle(handle);
1439 class
1440 };
1441 let _ = child.terminate_tree();
1442 assert_eq!(class, BELOW_NORMAL_PRIORITY_CLASS.0);
1443 }
1444
1445 #[cfg(unix)]
1447 #[test]
1448 fn lowered_priority_child_runs_at_or_above_the_nice_floor() {
1449 use super::{OwnedCommand, ProcessPriority};
1450 use std::{io::Read, process::Stdio};
1451 for (priority, floor) in [
1452 (ProcessPriority::BelowNormal, 10),
1453 (ProcessPriority::Idle, 19),
1454 ] {
1455 let mut command = OwnedCommand::new("/bin/sh");
1456 command
1457 .command_mut()
1458 .args(["-c", "ps -o ni= -p $$"])
1459 .stdin(Stdio::null())
1460 .stdout(Stdio::piped())
1461 .stderr(Stdio::null());
1462 command.process_priority(priority);
1463 let mut child = command.spawn().expect("spawn sh");
1464 let mut text = String::new();
1465 child
1466 .take_stdout()
1467 .expect("piped stdout")
1468 .read_to_string(&mut text)
1469 .expect("read ps output");
1470 let _ = child.wait();
1471 let nice: i32 = text.trim().parse().expect("ps printed a nice value");
1472 assert!(nice >= floor, "nice {nice} below floor {floor}");
1473 }
1474 }
1475
1476 #[test]
1477 fn breakaway_is_off_by_default_explicit_by_default_opt_in_and_silent_is_separate() {
1478 use super::BreakawayMode;
1479 let mut command = super::OwnedCommand::new("unused");
1480 assert_eq!(command.windows_breakaway, BreakawayMode::None);
1481 command.windows_allow_breakaway(true);
1482 assert_eq!(command.windows_breakaway, BreakawayMode::Explicit);
1483 command.windows_allow_silent_breakaway(true);
1484 assert_eq!(command.windows_breakaway, BreakawayMode::Silent);
1485 command.windows_allow_silent_breakaway(false);
1486 assert_eq!(command.windows_breakaway, BreakawayMode::None);
1487 }
1488
1489 #[test]
1490 fn kill_on_parent_exit_is_opt_in() {
1491 let mut command = super::OwnedCommand::new("unused");
1492 assert!(!command.kill_on_parent_exit);
1493 command.kill_on_parent_exit(true);
1494 assert!(command.kill_on_parent_exit);
1495 }
1496
1497 #[test]
1498 fn breakaway_denied_is_a_typed_recoverable_error() {
1499 let error =
1500 std::io::Error::new(std::io::ErrorKind::PermissionDenied, super::BreakawayDenied);
1501 assert!(error
1502 .get_ref()
1503 .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1504 .is_some());
1505 assert!(super::BreakawayDenied.to_string().contains("breakaway"));
1506 }
1507
1508 #[test]
1509 fn a_strict_detached_spawn_passes_other_failures_through_untyped() {
1510 let command = super::OwnedCommand::new("rightkit-no-such-program-for-strict-detach");
1511 let Err(error) = command.spawn_uncontained_detached_strict() else {
1512 panic!("a missing program must not spawn");
1513 };
1514 assert!(error
1515 .get_ref()
1516 .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1517 .is_none());
1518 }
1519
1520 #[test]
1521 fn a_strict_detached_spawn_still_rejects_kill_on_parent_exit() {
1522 let mut command = super::OwnedCommand::new("unused");
1523 command.kill_on_parent_exit(true);
1524 let Err(error) = command.spawn_uncontained_detached_strict() else {
1525 panic!("a detached spawn must not accept the parent-death tie");
1526 };
1527 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1528 }
1529
1530 #[test]
1531 fn a_detached_spawn_rejects_kill_on_parent_exit() {
1532 let mut command = super::OwnedCommand::new("unused");
1534 command.kill_on_parent_exit(true);
1535 let Err(error) = command.spawn_uncontained_detached() else {
1536 panic!("a detached spawn must not accept the parent-death tie");
1537 };
1538 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1539 }
1540
1541 #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
1544 #[test]
1545 fn a_host_without_a_parent_death_primitive_refuses_before_starting_anything() {
1546 let marker =
1547 std::env::temp_dir().join(format!("rightkit-process-untied-{}", std::process::id()));
1548 let _ = std::fs::remove_file(&marker);
1549 let mut command = super::OwnedCommand::new("/usr/bin/touch");
1550 command.command_mut().arg(&marker);
1551 command.kill_on_parent_exit(true);
1552 let Err(error) = command.spawn() else {
1553 panic!("kill_on_parent_exit must not run the child untied");
1554 };
1555 assert_eq!(error.kind(), std::io::ErrorKind::Unsupported);
1556 std::thread::sleep(std::time::Duration::from_millis(200));
1557 assert!(!marker.exists(), "the program ran");
1558 }
1559
1560 #[test]
1561 fn option_builders_keep_every_requested_value() {
1562 use super::{JobAssignmentMode, OwnedCommand, TerminationOptions, UnixContainment};
1563 use std::time::Duration;
1564 let mut command = OwnedCommand::new("unused");
1565 command
1566 .job_assignment_mode(JobAssignmentMode::BestEffort)
1567 .unix_containment(UnixContainment::Session)
1568 .partial_spawn_cleanup_timeout(Duration::from_secs(5));
1569 assert_eq!(command.job_assignment, JobAssignmentMode::BestEffort);
1570 assert_eq!(command.unix_containment, UnixContainment::Session);
1571 assert_eq!(
1572 command.partial_spawn_cleanup_timeout,
1573 Some(Duration::from_secs(5))
1574 );
1575 let options = TerminationOptions::default()
1576 .with_grace(Duration::from_millis(100))
1577 .with_windows_exit_code(1067);
1578 assert_eq!(options.grace, Duration::from_millis(100));
1579 assert_eq!(options.windows_exit_code, 1067);
1580 }
1581
1582 #[test]
1583 fn zero_bound_partial_spawn_failure_still_reaps_the_child() {
1584 use std::{
1585 num::NonZeroU32,
1586 process::Command,
1587 sync::{
1588 atomic::{AtomicU32, Ordering},
1589 Arc,
1590 },
1591 thread,
1592 time::{Duration, Instant},
1593 };
1594 let command = if cfg!(windows) {
1595 let mut command = Command::new("powershell.exe");
1596 command.args([
1597 "-NoLogo",
1598 "-NoProfile",
1599 "-NonInteractive",
1600 "-Command",
1601 "Start-Sleep -Seconds 30",
1602 ]);
1603 command
1604 } else {
1605 let mut command = Command::new("/bin/sh");
1606 command.args(["-c", "exec sleep 30"]);
1607 command
1608 };
1609 let mut command = super::OwnedCommand::from_command(command);
1610 command.partial_spawn_cleanup_timeout(Duration::ZERO);
1611 let captured = Arc::new(AtomicU32::new(0));
1612 let started = Instant::now();
1613 assert!(command
1614 .spawn_inner(false, true, Some(Arc::clone(&captured)))
1615 .is_err());
1616 assert!(started.elapsed() < Duration::from_secs(3));
1617 let pid = captured.load(Ordering::SeqCst);
1618 assert_ne!(pid, 0);
1619 let process = super::AdoptedProcess::new(NonZeroU32::new(pid).unwrap());
1620 let started = Instant::now();
1621 while process.is_running().unwrap() {
1622 assert!(started.elapsed() < Duration::from_secs(5));
1623 thread::sleep(Duration::from_millis(5));
1624 }
1625 }
1626
1627 #[test]
1628 fn owned_commands_hide_their_console_window_by_default() {
1629 assert!(super::OwnedCommand::new("x").windows_hidden);
1630 assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
1631 }
1632
1633 #[cfg(windows)]
1635 fn host_job() -> std::os::windows::io::OwnedHandle {
1636 use std::os::windows::io::FromRawHandle;
1637 use windows::Win32::System::JobObjects::{
1638 CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject,
1639 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
1640 };
1641 let job = unsafe { CreateJobObjectW(None, None) }.unwrap();
1642 let job_handle = unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(job.0 as _) };
1643 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
1644 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
1645 unsafe {
1646 SetInformationJobObject(
1647 job,
1648 JobObjectExtendedLimitInformation,
1649 &info as *const _ as *const _,
1650 std::mem::size_of_val(&info) as u32,
1651 )
1652 }
1653 .unwrap();
1654 job_handle
1655 }
1656
1657 #[cfg(windows)]
1658 fn in_job(pid: u32, job: &std::os::windows::io::OwnedHandle) -> bool {
1659 use std::os::windows::io::AsRawHandle;
1660 #[link(name = "kernel32")]
1661 unsafe extern "system" {
1662 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> *mut std::ffi::c_void;
1663 fn IsProcessInJob(
1664 process: *mut std::ffi::c_void,
1665 job: *mut std::ffi::c_void,
1666 result: *mut i32,
1667 ) -> i32;
1668 fn CloseHandle(handle: *mut std::ffi::c_void) -> i32;
1669 }
1670 unsafe {
1671 let process = OpenProcess(0x1000, 0, pid); assert!(!process.is_null());
1673 let mut inside = 0;
1674 assert_ne!(
1675 IsProcessInJob(process, job.as_raw_handle() as _, &mut inside),
1676 0
1677 );
1678 CloseHandle(process);
1679 inside != 0
1680 }
1681 }
1682
1683 #[cfg(windows)]
1684 #[test]
1685 fn children_join_the_callers_job_and_die_when_the_caller_closes_it() {
1686 use std::os::windows::io::AsHandle;
1687 use std::time::{Duration, Instant};
1688 let job = host_job();
1689 let sleeper = || {
1690 let mut command = super::OwnedCommand::new("powershell.exe");
1691 command.command_mut().args([
1692 "-NoLogo",
1693 "-NoProfile",
1694 "-NonInteractive",
1695 "-Command",
1696 "Start-Sleep -Seconds 60",
1697 ]);
1698 command.windows_job(job.as_handle()).unwrap();
1699 command.spawn().unwrap()
1700 };
1701 let (first, second) = (sleeper(), sleeper());
1704 let pids = [first.id(), second.id()];
1705 assert!(
1706 pids.iter().all(|pid| in_job(*pid, &job)),
1707 "children must be in the caller's job"
1708 );
1709
1710 drop(job);
1712 let deadline = Instant::now() + Duration::from_secs(5);
1713 for pid in pids {
1714 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1715 while process.is_running().unwrap_or(false) {
1716 assert!(
1717 Instant::now() < deadline,
1718 "child {pid} survived its caller's job"
1719 );
1720 std::thread::sleep(Duration::from_millis(20));
1721 }
1722 }
1723 drop((first, second));
1724 }
1725
1726 #[test]
1727 fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
1728 use std::{
1729 process::Command,
1730 sync::{
1731 atomic::{AtomicU32, Ordering},
1732 Arc,
1733 },
1734 thread,
1735 time::{Duration, Instant},
1736 };
1737
1738 let command = if cfg!(windows) {
1740 let mut command = Command::new("powershell.exe");
1742 command.args([
1743 "-NoLogo",
1744 "-NoProfile",
1745 "-NonInteractive",
1746 "-Command",
1747 "Start-Sleep -Seconds 60",
1748 ]);
1749 command
1750 } else {
1751 let mut command = Command::new("sleep");
1753 command.arg("60");
1754 command
1755 };
1756 let spawned_pid = Arc::new(AtomicU32::new(0));
1757
1758 assert!(super::spawn_for_test(command, Arc::clone(&spawned_pid)).is_err());
1759 let pid = spawned_pid.load(Ordering::SeqCst);
1760 assert_ne!(pid, 0, "test must fail after the OS process was spawned");
1761 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1762 let deadline = Instant::now() + Duration::from_secs(5);
1763 while process.is_running().unwrap_or(false) {
1764 assert!(
1765 Instant::now() < deadline,
1766 "partially spawned process leaked"
1767 );
1768 thread::sleep(Duration::from_millis(20));
1769 }
1770 }
1771}