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.windows_hidden,
551 self.process_priority,
552 );
553 (PlatformChild::from(self.command.spawn()?), true)
554 }
555 Err(error) => return Err(error),
556 }
557 }
558 Some(handles) => {
559 let config = self.windows_spawn_config.take().ok_or_else(|| {
560 io::Error::new(io::ErrorKind::InvalidInput,
561 "allowlisted Windows spawn requires windows_spawn_config for opaque Command settings")
562 })?;
563 crate::spawn_windows::spawn(
564 &self.command,
565 config,
566 handles,
567 detached,
568 self.windows_hidden,
569 self.process_priority,
570 self.env_cleared,
571 self.partial_spawn_cleanup_timeout,
572 in_job_fallback,
573 )?
574 }
575 };
576 #[cfg(not(windows))]
577 let _ = in_job_fallback;
578 #[cfg(not(windows))]
579 let stayed_in_parent_job = false;
580 let pid = child.id();
581 if let Some(captured) = captured_pid {
582 captured.store(pid, std::sync::atomic::Ordering::SeqCst);
583 }
584 let child = Arc::new(Mutex::new(child));
585 let handle = match ProcessHandle::new(&child, spawned_at) {
586 Ok(handle) => handle,
587 Err(error) => {
588 cleanup_direct(child, self.partial_spawn_cleanup_timeout);
589 return Err(error);
590 }
591 };
592 let mut partial = PartialChild {
593 child: Some(OwnedChild {
594 child,
595 handle,
596 pid,
597 detached,
598 terminate_on_drop: !detached,
599 drop_reap_timeout: DEFAULT_DROP_REAP_TIMEOUT,
600 tree_terminated: false,
601 assignment_failures: Vec::new(),
602 suspended: std::sync::atomic::AtomicBool::new(cfg!(windows) && hold_suspended),
603 stayed_in_parent_job,
604 #[cfg(windows)]
605 job: None,
606 }),
607 timeout: self.partial_spawn_cleanup_timeout,
608 };
609 #[cfg(windows)]
610 if !detached {
611 let owned = partial.child.as_mut().expect("partial child is present");
612 crate::process_windows::assign_jobs(
613 &owned.handle,
614 self.caller_job.take(),
615 self.job_assignment,
616 self.windows_breakaway,
617 &mut owned.job,
618 &mut owned.assignment_failures,
619 )?;
620 }
621 #[cfg(windows)]
622 {
623 let owned = partial.child.as_ref().expect("partial child is present");
624 if self.process_priority == ProcessPriority::Efficient {
625 let _ = crate::process_windows::apply_efficiency(&*owned.child());
627 }
628 if !hold_suspended {
629 owned.child().resume(detached)?;
630 }
631 }
632 if fail_after_spawn {
633 return Err(io::Error::other("injected post-spawn wrapping failure"));
634 }
635 let owned = partial.child.take().expect("partial child is present");
636 if !detached {
637 #[cfg(unix)]
639 crate::registry::register(&owned.child, owned.pid);
640 #[cfg(windows)]
641 crate::registry::register(
642 &owned.child,
643 &owned.handle,
644 owned
645 .job
646 .as_ref()
647 .map_or_else(std::sync::Weak::new, std::sync::Arc::downgrade),
648 owned.pid,
649 );
650 }
651 Ok(owned)
652 }
653}
654
655#[derive(Clone, Copy, Debug, Eq, PartialEq)]
661pub struct BreakawayDenied;
662
663impl std::fmt::Display for BreakawayDenied {
664 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
665 f.write_str(
666 "detached launch denied: the caller's job object does not allow breakaway \
667 (ERROR_ACCESS_DENIED); no fallback was attempted",
668 )
669 }
670}
671
672impl std::error::Error for BreakawayDenied {}
673
674#[cfg(windows)]
675fn is_denied_breakaway(error: &io::Error) -> bool {
676 crate::process_windows::is_breakaway_denied(error)
677}
678#[cfg(not(windows))]
679fn is_denied_breakaway(_error: &io::Error) -> bool {
680 false
681}
682
683pub const DEFAULT_DROP_REAP_TIMEOUT: Duration = Duration::from_secs(2);
686
687pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
688 "PATH",
689 "HOME",
690 "USER",
691 "LOGNAME",
692 "LANG",
693 "LC_ALL",
694 "TMPDIR",
695 "TEMP",
696 "TMP",
697 "SystemRoot",
698 "SYSTEMROOT",
699 "USERPROFILE",
700 "APPDATA",
701 "LOCALAPPDATA",
702 "ProgramData",
703 "COMSPEC",
704 "PATHEXT",
705 "XDG_DATA_HOME",
706 "XDG_RUNTIME_DIR",
707];
708
709#[derive(Debug)]
710pub struct OwnedChild {
711 child: Arc<Mutex<PlatformChild>>,
712 handle: ProcessHandle,
713 pid: u32,
715 detached: bool,
716 terminate_on_drop: bool,
717 drop_reap_timeout: Duration,
718 tree_terminated: bool,
719 assignment_failures: Vec<JobAssignmentFailure>,
720 suspended: std::sync::atomic::AtomicBool,
722 stayed_in_parent_job: bool,
724 #[cfg(windows)]
727 job: Option<std::sync::Arc<crate::process_windows::KillOnCloseJob>>,
728}
729#[derive(Clone, Debug, Eq, PartialEq)]
731pub struct TreeTermination {
732 pub status: ExitStatus,
734 pub descendants: crate::SweepReport,
736}
737#[derive(Clone, Copy, Debug, Eq, PartialEq)]
738pub enum ReapOutcome {
739 Exited(ExitStatus),
740 TimedOut,
741}
742#[derive(Clone, Copy, Debug, Eq, PartialEq)]
743pub enum WaitOutcome {
744 Exited(ExitStatus),
745 Terminated(ExitStatus),
746}
747
748impl OwnedChild {
749 pub fn id(&self) -> u32 {
750 self.pid
751 }
752 pub fn stayed_in_parent_job(&self) -> bool {
757 self.stayed_in_parent_job
758 }
759 pub fn child(&self) -> MutexGuard<'_, PlatformChild> {
775 lock_child(&self.child)
776 }
777 pub fn resume(&self) -> io::Result<()> {
782 if self
783 .suspended
784 .swap(false, std::sync::atomic::Ordering::SeqCst)
785 {
786 #[cfg(windows)]
787 if let Err(error) = self.child().resume(self.detached) {
788 self.suspended
789 .store(true, std::sync::atomic::Ordering::SeqCst);
790 return Err(error);
791 }
792 }
793 Ok(())
794 }
795 pub fn is_suspended(&self) -> bool {
797 self.suspended.load(std::sync::atomic::Ordering::SeqCst)
798 }
799 pub fn while_unreaped<R>(&self, act: impl FnOnce(u32) -> R) -> io::Result<Option<R>> {
808 #[cfg(unix)]
809 {
810 let mut child = lock_child(&self.child);
811 if child.leader_state()? == LeaderState::Reaped {
812 return Ok(None);
813 }
814 Ok(Some(act(self.pid)))
815 }
816 #[cfg(windows)]
817 Ok(Some(act(self.pid)))
818 }
819 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
820 self.child().stdin.take()
821 }
822 pub fn close_stdin(&mut self) {
826 drop(self.child().stdin.take());
827 }
828 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
829 self.child().stdout.take()
830 }
831 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
832 self.child().stderr.take()
833 }
834 pub fn job_assignment_failures(&self) -> &[JobAssignmentFailure] {
835 &self.assignment_failures
836 }
837 pub fn process_handle(&self) -> io::Result<ProcessHandle> {
841 self.handle.try_clone()
842 }
843 pub fn creation_time(&self) -> Option<SystemTime> {
844 self.handle.creation_time()
845 }
846 #[cfg(windows)]
847 pub fn creation_time_ticks(&self) -> Option<u64> {
848 self.handle.creation_time_ticks()
849 }
850 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
851 self.handle.try_wait()
853 }
854 pub fn wait(&mut self) -> io::Result<ExitStatus> {
857 drop(self.child().stdin.take());
858 self.handle.wait()
859 }
860 pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
861 self.handle.wait_timeout(timeout)
862 }
863 pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
873 self.wait_or_kill_inner(grace, true)
874 }
875 pub fn wait_then_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
881 self.wait_or_kill_inner(grace, false)
882 }
883 fn wait_or_kill_inner(&mut self, grace: Duration, term_first: bool) -> io::Result<WaitOutcome> {
884 #[cfg(not(unix))]
885 let _ = term_first;
886 #[cfg(unix)]
887 {
888 if term_first && !self.tree_terminated {
889 self.signal(libc::SIGTERM)?;
890 }
891 if self.handle.wait_exited_timeout(grace)? {
893 self.force_kill(1)?;
898 return self.wait().map(WaitOutcome::Exited);
899 }
900 self.terminate_tree().map(WaitOutcome::Terminated)
901 }
902 #[cfg(windows)]
903 {
904 if let Some(status) = self.wait_timeout(grace)? {
905 self.force_kill(1)?;
909 return Ok(WaitOutcome::Exited(status));
910 }
911 self.terminate_tree().map(WaitOutcome::Terminated)
912 }
913 }
914 pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
919 self.force_kill(1)?;
920 self.wait()
921 }
922 pub fn terminate_tree_with_options(
927 &mut self,
928 options: TerminationOptions,
929 ) -> io::Result<ExitStatus> {
930 self.start_termination(options)?;
931 self.wait()
932 }
933 pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
934 self.terminate_tree_bounded_with_options(timeout, TerminationOptions::default())
935 }
936 pub fn terminate_tree_bounded_with_options(
938 &mut self,
939 timeout: Duration,
940 mut options: TerminationOptions,
941 ) -> io::Result<ReapOutcome> {
942 let started = Instant::now();
943 options.grace = options.grace.min(timeout);
944 self.start_termination(options)?;
945 Ok(
946 match self.wait_timeout(timeout.saturating_sub(started.elapsed()))? {
947 Some(status) => ReapOutcome::Exited(status),
948 None => ReapOutcome::TimedOut,
949 },
950 )
951 }
952 pub fn terminate_tree_deep(
976 &mut self,
977 options: TerminationOptions,
978 ) -> io::Result<TreeTermination> {
979 let mut found = self.descendant_identities()?;
980 found.reverse(); #[cfg(unix)]
982 if !self.tree_terminated {
983 signal_shared(&self.child, SignalTarget::Group, libc::SIGTERM)?;
984 await_tree_shared(&self.child, &self.handle, options.grace, true)?;
985 signal_shared(&self.child, SignalTarget::Group, libc::SIGKILL)?;
986 self.tree_terminated = true;
987 }
988 #[cfg(windows)]
989 self.force_kill(options.windows_exit_code)?;
990 let status = self.wait()?;
991 let descendants = crate::sweep(&found, options.grace)?;
992 Ok(TreeTermination {
993 status,
994 descendants,
995 })
996 }
997 fn start_termination(&mut self, options: TerminationOptions) -> io::Result<()> {
998 #[cfg(unix)]
999 if !self.tree_terminated {
1000 self.signal(libc::SIGTERM)?;
1001 self.await_tree(options.grace)?;
1005 }
1006 self.force_kill(options.windows_exit_code)
1007 }
1008 #[cfg(unix)]
1015 fn await_tree(&self, grace: Duration) -> io::Result<()> {
1016 await_tree_shared(&self.child, &self.handle, grace, !self.detached)
1018 }
1019 #[cfg(unix)]
1023 fn signal(&self, signal: i32) -> io::Result<()> {
1024 let target = if self.detached {
1027 SignalTarget::Leader
1028 } else {
1029 SignalTarget::Group
1030 };
1031 signal_shared(&self.child, target, signal).map(|_| ())
1032 }
1033 fn force_kill(&mut self, code: u32) -> io::Result<()> {
1034 if self.tree_terminated {
1035 return Ok(());
1036 }
1037 #[cfg(unix)]
1038 {
1039 let _ = code;
1040 self.signal(libc::SIGKILL)?;
1041 }
1042 #[cfg(windows)]
1043 crate::process_windows::terminate(&self.handle, self.job.as_deref(), code)?;
1044 self.tree_terminated = true;
1045 Ok(())
1046 }
1047}
1048impl OwnedChild {
1049 pub fn set_drop_reap_timeout(&mut self, timeout: Duration) -> &mut Self {
1056 self.drop_reap_timeout = timeout;
1057 self
1058 }
1059 pub fn drop_reap_timeout(&self) -> Duration {
1061 self.drop_reap_timeout
1062 }
1063 fn reap_in_background(&self) {
1065 #[cfg(unix)]
1066 if let Ok(reader) = self.handle.try_clone() {
1067 let _ = thread::Builder::new()
1068 .name("rightkit-drop-reap".into())
1069 .spawn(move || {
1070 let _ = reader.wait();
1071 });
1072 }
1073 }
1074 pub fn descendant_identities(&self) -> io::Result<Vec<crate::AdoptedIdentity>> {
1078 let root = self.while_unreaped(|pid| match NonZeroU32::new(pid) {
1079 Some(pid) => crate::AdoptedIdentity::capture(pid),
1080 None => Ok(None),
1081 })?;
1082 match root {
1083 Some(Ok(Some(root))) => root.descendants(),
1084 Some(Err(error)) => Err(error),
1085 _ => Ok(Vec::new()),
1086 }
1087 }
1088}
1089impl Drop for OwnedChild {
1090 fn drop(&mut self) {
1091 crate::registry::forget(&self.child);
1093 if self.terminate_on_drop {
1094 if self.force_kill(1).is_ok() {
1098 drop(self.child().stdin.take());
1099 if !matches!(
1100 self.handle.wait_timeout(self.drop_reap_timeout),
1101 Ok(Some(_))
1102 ) {
1103 self.reap_in_background();
1104 }
1105 }
1106 } else if self.detached {
1107 #[cfg(unix)]
1108 {
1109 {
1113 let mut child = self.child();
1114 drop(child.stdin.take());
1115 drop(child.stdout.take());
1116 drop(child.stderr.take());
1117 }
1118 if let Ok(reader) = self.handle.try_clone() {
1119 let _ = thread::Builder::new()
1120 .name("rightkit-detached-reap".into())
1121 .spawn(move || {
1122 let _ = reader.wait();
1123 });
1124 }
1125 }
1126 }
1127 }
1128}
1129
1130struct PartialChild {
1131 child: Option<OwnedChild>,
1132 timeout: Option<Duration>,
1133}
1134impl Drop for PartialChild {
1135 fn drop(&mut self) {
1136 if let Some(mut child) = self.child.take() {
1137 child.terminate_on_drop = false;
1139 let _ = child.force_kill(1);
1140 cleanup_direct(Arc::clone(&child.child), self.timeout);
1141 }
1142 }
1143}
1144#[cfg(unix)]
1147const MID_EXIT_RETRY: Duration = Duration::from_millis(300);
1148
1149#[cfg(unix)]
1153pub(crate) fn signal_shared(
1154 shared: &Mutex<crate::unix_child::UnixChild>,
1155 target: SignalTarget,
1156 signal: i32,
1157) -> io::Result<SignalOutcome> {
1158 let started = Instant::now();
1159 loop {
1160 let (outcome, watch) = {
1161 let mut child = lock_child(shared);
1162 (child.signal(target, signal)?, child.exit_watch())
1163 };
1164 if outcome != SignalOutcome::MidExit {
1165 return Ok(outcome);
1166 }
1167 let remaining = MID_EXIT_RETRY.saturating_sub(started.elapsed());
1170 if remaining.is_zero() {
1171 return Err(io::Error::from_raw_os_error(libc::EPERM));
1172 }
1173 watch.pause(remaining);
1174 }
1175}
1176
1177#[cfg(unix)]
1179pub(crate) fn await_tree_shared(
1180 shared: &Mutex<crate::unix_child::UnixChild>,
1181 handle: &ProcessHandle,
1182 grace: Duration,
1183 whole_group: bool,
1184) -> io::Result<()> {
1185 use crate::{exit_watch::POLL_INTERVAL, unix_child::Members};
1186 let Some(deadline) = Instant::now().checked_add(grace) else {
1187 return Ok(());
1188 };
1189 let left = |deadline: Instant| deadline.saturating_duration_since(Instant::now());
1190 if !handle.wait_exited_timeout(left(deadline))? {
1191 return Ok(()); }
1193 if !whole_group {
1194 return Ok(());
1195 }
1196 loop {
1197 match lock_child(shared).group_other_members()? {
1198 Members::Pids(pids) if pids.is_empty() => return Ok(()),
1199 Members::Pids(_) => {}
1200 Members::Unknown => return wait_out(left(deadline)),
1201 }
1202 let remaining = left(deadline);
1203 if remaining.is_zero() {
1204 return Ok(()); }
1206 thread::sleep(POLL_INTERVAL.min(remaining));
1207 }
1208}
1209
1210#[cfg(unix)]
1213fn wait_out(remaining: Duration) -> io::Result<()> {
1214 thread::sleep(remaining);
1215 Ok(())
1216}
1217pub(crate) fn cleanup_direct(child: Arc<Mutex<PlatformChild>>, timeout: Option<Duration>) {
1224 let _ = lock_child(&child).kill();
1225 let Some(timeout) = timeout else {
1226 let _ = lock_child(&child).wait();
1228 return;
1229 };
1230 #[cfg(unix)]
1231 {
1232 let watch = lock_child(&child).exit_watch();
1235 let deadline = Instant::now().checked_add(timeout);
1236 if matches!(
1237 watch.wait_for(deadline, || lock_child(&child).try_wait()),
1238 Ok(Some(_))
1239 ) {
1240 return;
1241 }
1242 }
1243 #[cfg(windows)]
1244 {
1245 let started = Instant::now();
1246 loop {
1247 match lock_child(&child).try_wait() {
1248 Ok(Some(_)) => return,
1249 Err(_) => break,
1250 Ok(None) => {}
1251 }
1252 if started.elapsed() >= timeout {
1253 break;
1254 }
1255 thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
1256 }
1257 }
1258 let _ = thread::Builder::new()
1262 .name("rightkit-partial-reap".into())
1263 .spawn(move || {
1264 let _ = lock_child(&child).wait();
1265 });
1266}
1267
1268#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1269pub struct AdoptedProcess {
1270 pid: NonZeroU32,
1271}
1272impl AdoptedProcess {
1273 pub fn new(pid: NonZeroU32) -> Self {
1274 Self { pid }
1275 }
1276 pub fn id(&self) -> u32 {
1277 self.pid.get()
1278 }
1279 pub fn is_running(&self) -> io::Result<bool> {
1283 process_is_running(self.pid)
1284 }
1285 pub fn identity(&self) -> io::Result<Option<crate::AdoptedIdentity>> {
1290 crate::AdoptedIdentity::capture(self.pid)
1291 }
1292}
1293#[cfg(test)]
1294fn spawn_for_test(
1295 command: Command,
1296 captured_pid: Arc<std::sync::atomic::AtomicU32>,
1297) -> io::Result<OwnedChild> {
1298 OwnedCommand::from_command(command).spawn_inner(false, true, Some(captured_pid))
1299}
1300
1301#[cfg(windows)]
1302fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1303 use windows::Win32::{
1304 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
1305 System::Threading::{
1306 OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
1307 PROCESS_SYNCHRONIZE,
1308 },
1309 };
1310
1311 let handle = unsafe {
1312 OpenProcess(
1313 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
1314 false,
1315 pid.get(),
1316 )
1317 };
1318 let handle = match handle {
1319 Ok(handle) => handle,
1320 Err(error) => {
1321 if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
1322 return Ok(false);
1323 }
1324 return Err(io::Error::other(error.to_string()));
1325 }
1326 };
1327 let wait = unsafe { WaitForSingleObject(handle, 0) };
1328 let close = unsafe { CloseHandle(handle) };
1329 close.map_err(|error| io::Error::other(error.to_string()))?;
1330 match wait.0 {
1331 0 => Ok(false),
1332 value if value == WAIT_TIMEOUT.0 => Ok(true),
1333 _ => Err(io::Error::last_os_error()),
1334 }
1335}
1336
1337#[cfg(unix)]
1338fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1339 let raw_pid = i32::try_from(pid.get()).map_err(io::Error::other)?;
1340 loop {
1341 if unsafe { libc::kill(raw_pid, 0) } == 0 {
1343 return Ok(!crate::foreign::process_is_zombie(pid.get()));
1346 }
1347 let error = io::Error::last_os_error();
1348 match error.raw_os_error() {
1349 Some(libc::EINTR) => continue,
1350 Some(libc::EPERM) => return Ok(!crate::foreign::process_is_zombie(pid.get())),
1351 Some(libc::ESRCH) => return Ok(false),
1352 _ => return Err(error),
1353 }
1354 }
1355}
1356
1357#[cfg(test)]
1358mod tests {
1359 #[test]
1360 fn option_defaults_preserve_existing_behavior() {
1361 let command = super::OwnedCommand::new("unused");
1362 assert_eq!(command.job_assignment, super::JobAssignmentMode::Strict);
1363 assert_eq!(
1364 command.unix_containment,
1365 super::UnixContainment::ProcessGroup
1366 );
1367 assert_eq!(command.partial_spawn_cleanup_timeout, None);
1368 assert_eq!(
1369 super::TerminationOptions::default().grace,
1370 std::time::Duration::ZERO
1371 );
1372 assert_eq!(super::TerminationOptions::default().windows_exit_code, 1);
1373 }
1374
1375 #[cfg(not(windows))]
1376 #[test]
1377 fn new_and_from_command_share_spawn_settings() {
1378 use super::OwnedCommand;
1379 use std::process::Command;
1380 let by_new = OwnedCommand::new("unused");
1382 let by_from = OwnedCommand::from_command(Command::new("unused"));
1383 assert_eq!(format!("{by_new:?}"), format!("{by_from:?}"));
1384 }
1385
1386 #[cfg(windows)]
1390 #[test]
1391 fn only_new_supplies_a_default_windows_spawn_config() {
1392 use super::OwnedCommand;
1393 use std::process::Command;
1394 let by_new = OwnedCommand::new("unused");
1395 let by_from = OwnedCommand::from_command(Command::new("unused"));
1396 assert!(by_new.windows_spawn_config.is_some() && by_new.windows_spawn_config_auto);
1397 assert!(by_from.windows_spawn_config.is_none() && !by_from.windows_spawn_config_auto);
1398 }
1399
1400 #[test]
1401 fn process_priority_defaults_to_normal_and_keeps_the_requested_value() {
1402 use super::{OwnedCommand, ProcessPriority};
1403 let mut command = OwnedCommand::new("unused");
1404 assert_eq!(command.process_priority, ProcessPriority::Normal);
1405 command.process_priority(ProcessPriority::BelowNormal);
1406 assert_eq!(command.process_priority, ProcessPriority::BelowNormal);
1407 command.process_priority(ProcessPriority::Idle);
1408 assert_eq!(command.process_priority, ProcessPriority::Idle);
1409 }
1410
1411 #[cfg(unix)]
1414 #[test]
1415 fn process_priority_maps_to_nice_floors() {
1416 use super::ProcessPriority;
1417 assert_eq!(ProcessPriority::Normal.nice_floor(), None);
1418 assert_eq!(ProcessPriority::BelowNormal.nice_floor(), Some(10));
1419 assert_eq!(ProcessPriority::Idle.nice_floor(), Some(19));
1420 assert_eq!(ProcessPriority::Efficient.nice_floor(), Some(10));
1421 }
1422
1423 #[cfg(windows)]
1425 #[test]
1426 fn below_normal_child_has_below_normal_priority_class() {
1427 use super::{OwnedCommand, ProcessPriority};
1428 use std::process::Stdio;
1429 use windows::Win32::{
1430 Foundation::CloseHandle,
1431 System::Threading::{
1432 GetPriorityClass, OpenProcess, BELOW_NORMAL_PRIORITY_CLASS,
1433 PROCESS_QUERY_LIMITED_INFORMATION,
1434 },
1435 };
1436 let mut command = OwnedCommand::new("ping");
1437 command
1438 .command_mut()
1439 .args(["-n", "6", "127.0.0.1"])
1440 .stdin(Stdio::null())
1441 .stdout(Stdio::null())
1442 .stderr(Stdio::null());
1443 command.process_priority(ProcessPriority::BelowNormal);
1444 let mut child = command.spawn().expect("spawn ping");
1445 let class = unsafe {
1447 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, child.id())
1448 .expect("open the child");
1449 let class = GetPriorityClass(handle);
1450 let _ = CloseHandle(handle);
1451 class
1452 };
1453 let _ = child.terminate_tree();
1454 assert_eq!(class, BELOW_NORMAL_PRIORITY_CLASS.0);
1455 }
1456
1457 #[cfg(unix)]
1459 #[test]
1460 fn lowered_priority_child_runs_at_or_above_the_nice_floor() {
1461 use super::{OwnedCommand, ProcessPriority};
1462 use std::{io::Read, process::Stdio};
1463 for (priority, floor) in [
1464 (ProcessPriority::BelowNormal, 10),
1465 (ProcessPriority::Idle, 19),
1466 ] {
1467 let mut command = OwnedCommand::new("/bin/sh");
1468 command
1469 .command_mut()
1470 .args(["-c", "ps -o ni= -p $$"])
1471 .stdin(Stdio::null())
1472 .stdout(Stdio::piped())
1473 .stderr(Stdio::null());
1474 command.process_priority(priority);
1475 let mut child = command.spawn().expect("spawn sh");
1476 let mut text = String::new();
1477 child
1478 .take_stdout()
1479 .expect("piped stdout")
1480 .read_to_string(&mut text)
1481 .expect("read ps output");
1482 let _ = child.wait();
1483 let nice: i32 = text.trim().parse().expect("ps printed a nice value");
1484 assert!(nice >= floor, "nice {nice} below floor {floor}");
1485 }
1486 }
1487
1488 #[test]
1489 fn breakaway_is_off_by_default_explicit_by_default_opt_in_and_silent_is_separate() {
1490 use super::BreakawayMode;
1491 let mut command = super::OwnedCommand::new("unused");
1492 assert_eq!(command.windows_breakaway, BreakawayMode::None);
1493 command.windows_allow_breakaway(true);
1494 assert_eq!(command.windows_breakaway, BreakawayMode::Explicit);
1495 command.windows_allow_silent_breakaway(true);
1496 assert_eq!(command.windows_breakaway, BreakawayMode::Silent);
1497 command.windows_allow_silent_breakaway(false);
1498 assert_eq!(command.windows_breakaway, BreakawayMode::None);
1499 }
1500
1501 #[test]
1502 fn kill_on_parent_exit_is_opt_in() {
1503 let mut command = super::OwnedCommand::new("unused");
1504 assert!(!command.kill_on_parent_exit);
1505 command.kill_on_parent_exit(true);
1506 assert!(command.kill_on_parent_exit);
1507 }
1508
1509 #[test]
1510 fn breakaway_denied_is_a_typed_recoverable_error() {
1511 let error =
1512 std::io::Error::new(std::io::ErrorKind::PermissionDenied, super::BreakawayDenied);
1513 assert!(error
1514 .get_ref()
1515 .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1516 .is_some());
1517 assert!(super::BreakawayDenied.to_string().contains("breakaway"));
1518 }
1519
1520 #[test]
1521 fn a_strict_detached_spawn_passes_other_failures_through_untyped() {
1522 let command = super::OwnedCommand::new("rightkit-no-such-program-for-strict-detach");
1523 let Err(error) = command.spawn_uncontained_detached_strict() else {
1524 panic!("a missing program must not spawn");
1525 };
1526 assert!(error
1527 .get_ref()
1528 .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1529 .is_none());
1530 }
1531
1532 #[test]
1533 fn a_strict_detached_spawn_still_rejects_kill_on_parent_exit() {
1534 let mut command = super::OwnedCommand::new("unused");
1535 command.kill_on_parent_exit(true);
1536 let Err(error) = command.spawn_uncontained_detached_strict() else {
1537 panic!("a detached spawn must not accept the parent-death tie");
1538 };
1539 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1540 }
1541
1542 #[test]
1543 fn a_detached_spawn_rejects_kill_on_parent_exit() {
1544 let mut command = super::OwnedCommand::new("unused");
1546 command.kill_on_parent_exit(true);
1547 let Err(error) = command.spawn_uncontained_detached() else {
1548 panic!("a detached spawn must not accept the parent-death tie");
1549 };
1550 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1551 }
1552
1553 #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
1556 #[test]
1557 fn a_host_without_a_parent_death_primitive_refuses_before_starting_anything() {
1558 let marker =
1559 std::env::temp_dir().join(format!("rightkit-process-untied-{}", std::process::id()));
1560 let _ = std::fs::remove_file(&marker);
1561 let mut command = super::OwnedCommand::new("/usr/bin/touch");
1562 command.command_mut().arg(&marker);
1563 command.kill_on_parent_exit(true);
1564 let Err(error) = command.spawn() else {
1565 panic!("kill_on_parent_exit must not run the child untied");
1566 };
1567 assert_eq!(error.kind(), std::io::ErrorKind::Unsupported);
1568 std::thread::sleep(std::time::Duration::from_millis(200));
1569 assert!(!marker.exists(), "the program ran");
1570 }
1571
1572 #[test]
1573 fn option_builders_keep_every_requested_value() {
1574 use super::{JobAssignmentMode, OwnedCommand, TerminationOptions, UnixContainment};
1575 use std::time::Duration;
1576 let mut command = OwnedCommand::new("unused");
1577 command
1578 .job_assignment_mode(JobAssignmentMode::BestEffort)
1579 .unix_containment(UnixContainment::Session)
1580 .partial_spawn_cleanup_timeout(Duration::from_secs(5));
1581 assert_eq!(command.job_assignment, JobAssignmentMode::BestEffort);
1582 assert_eq!(command.unix_containment, UnixContainment::Session);
1583 assert_eq!(
1584 command.partial_spawn_cleanup_timeout,
1585 Some(Duration::from_secs(5))
1586 );
1587 let options = TerminationOptions::default()
1588 .with_grace(Duration::from_millis(100))
1589 .with_windows_exit_code(1067);
1590 assert_eq!(options.grace, Duration::from_millis(100));
1591 assert_eq!(options.windows_exit_code, 1067);
1592 }
1593
1594 #[test]
1595 fn zero_bound_partial_spawn_failure_still_reaps_the_child() {
1596 use std::{
1597 num::NonZeroU32,
1598 process::Command,
1599 sync::{
1600 atomic::{AtomicU32, Ordering},
1601 Arc,
1602 },
1603 thread,
1604 time::{Duration, Instant},
1605 };
1606 let command = if cfg!(windows) {
1607 let mut command = Command::new("powershell.exe");
1608 command.args([
1609 "-NoLogo",
1610 "-NoProfile",
1611 "-NonInteractive",
1612 "-Command",
1613 "Start-Sleep -Seconds 30",
1614 ]);
1615 command
1616 } else {
1617 let mut command = Command::new("/bin/sh");
1618 command.args(["-c", "exec sleep 30"]);
1619 command
1620 };
1621 let mut command = super::OwnedCommand::from_command(command);
1622 command.partial_spawn_cleanup_timeout(Duration::ZERO);
1623 let captured = Arc::new(AtomicU32::new(0));
1624 let started = Instant::now();
1625 assert!(command
1626 .spawn_inner(false, true, Some(Arc::clone(&captured)))
1627 .is_err());
1628 assert!(started.elapsed() < Duration::from_secs(3));
1629 let pid = captured.load(Ordering::SeqCst);
1630 assert_ne!(pid, 0);
1631 let process = super::AdoptedProcess::new(NonZeroU32::new(pid).unwrap());
1632 let started = Instant::now();
1633 while process.is_running().unwrap() {
1634 assert!(started.elapsed() < Duration::from_secs(5));
1635 thread::sleep(Duration::from_millis(5));
1636 }
1637 }
1638
1639 #[test]
1640 fn owned_commands_hide_their_console_window_by_default() {
1641 assert!(super::OwnedCommand::new("x").windows_hidden);
1642 assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
1643 }
1644
1645 #[cfg(windows)]
1647 fn host_job() -> std::os::windows::io::OwnedHandle {
1648 use std::os::windows::io::FromRawHandle;
1649 use windows::Win32::System::JobObjects::{
1650 CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject,
1651 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
1652 };
1653 let job = unsafe { CreateJobObjectW(None, None) }.unwrap();
1654 let job_handle = unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(job.0 as _) };
1655 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
1656 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
1657 unsafe {
1658 SetInformationJobObject(
1659 job,
1660 JobObjectExtendedLimitInformation,
1661 &info as *const _ as *const _,
1662 std::mem::size_of_val(&info) as u32,
1663 )
1664 }
1665 .unwrap();
1666 job_handle
1667 }
1668
1669 #[cfg(windows)]
1670 fn in_job(pid: u32, job: &std::os::windows::io::OwnedHandle) -> bool {
1671 use std::os::windows::io::AsRawHandle;
1672 #[link(name = "kernel32")]
1673 unsafe extern "system" {
1674 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> *mut std::ffi::c_void;
1675 fn IsProcessInJob(
1676 process: *mut std::ffi::c_void,
1677 job: *mut std::ffi::c_void,
1678 result: *mut i32,
1679 ) -> i32;
1680 fn CloseHandle(handle: *mut std::ffi::c_void) -> i32;
1681 }
1682 unsafe {
1683 let process = OpenProcess(0x1000, 0, pid); assert!(!process.is_null());
1685 let mut inside = 0;
1686 assert_ne!(
1687 IsProcessInJob(process, job.as_raw_handle() as _, &mut inside),
1688 0
1689 );
1690 CloseHandle(process);
1691 inside != 0
1692 }
1693 }
1694
1695 #[cfg(windows)]
1696 #[test]
1697 fn children_join_the_callers_job_and_die_when_the_caller_closes_it() {
1698 use std::os::windows::io::AsHandle;
1699 use std::time::{Duration, Instant};
1700 let job = host_job();
1701 let sleeper = || {
1702 let mut command = super::OwnedCommand::new("powershell.exe");
1703 command.command_mut().args([
1704 "-NoLogo",
1705 "-NoProfile",
1706 "-NonInteractive",
1707 "-Command",
1708 "Start-Sleep -Seconds 60",
1709 ]);
1710 command.windows_job(job.as_handle()).unwrap();
1711 command.spawn().unwrap()
1712 };
1713 let (first, second) = (sleeper(), sleeper());
1716 let pids = [first.id(), second.id()];
1717 assert!(
1718 pids.iter().all(|pid| in_job(*pid, &job)),
1719 "children must be in the caller's job"
1720 );
1721
1722 drop(job);
1724 let deadline = Instant::now() + Duration::from_secs(5);
1725 for pid in pids {
1726 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1727 while process.is_running().unwrap_or(false) {
1728 assert!(
1729 Instant::now() < deadline,
1730 "child {pid} survived its caller's job"
1731 );
1732 std::thread::sleep(Duration::from_millis(20));
1733 }
1734 }
1735 drop((first, second));
1736 }
1737
1738 #[test]
1739 fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
1740 use std::{
1741 process::Command,
1742 sync::{
1743 atomic::{AtomicU32, Ordering},
1744 Arc,
1745 },
1746 thread,
1747 time::{Duration, Instant},
1748 };
1749
1750 let command = if cfg!(windows) {
1752 let mut command = Command::new("powershell.exe");
1754 command.args([
1755 "-NoLogo",
1756 "-NoProfile",
1757 "-NonInteractive",
1758 "-Command",
1759 "Start-Sleep -Seconds 60",
1760 ]);
1761 command
1762 } else {
1763 let mut command = Command::new("sleep");
1765 command.arg("60");
1766 command
1767 };
1768 let spawned_pid = Arc::new(AtomicU32::new(0));
1769
1770 assert!(super::spawn_for_test(command, Arc::clone(&spawned_pid)).is_err());
1771 let pid = spawned_pid.load(Ordering::SeqCst);
1772 assert_ne!(pid, 0, "test must fail after the OS process was spawned");
1773 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1774 let deadline = Instant::now() + Duration::from_secs(5);
1775 while process.is_running().unwrap_or(false) {
1776 assert!(
1777 Instant::now() < deadline,
1778 "partially spawned process leaked"
1779 );
1780 thread::sleep(Duration::from_millis(20));
1781 }
1782 }
1783}