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, Stdio},
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 run_stdio: Option<RunStdio>,
152 env_cleared: bool,
153}
154
155#[derive(Clone, Copy, Debug)]
158struct RunStdio {
159 stdin_piped: bool,
160}
161#[cfg(windows)]
162type CallerJob = Option<std::os::windows::io::OwnedHandle>;
163#[cfg(not(windows))]
164type CallerJob = Option<std::convert::Infallible>;
165
166impl OwnedCommand {
167 pub fn new(program: impl AsRef<OsStr>) -> Self {
168 let command = Self::from_command(Command::new(program));
169 #[cfg(windows)]
170 let command = {
171 let mut command = command;
172 command.windows_spawn_config = Some(crate::WindowsSpawnConfig::default());
175 command.windows_spawn_config_auto = true;
176 command
177 };
178 command
179 }
180 pub fn from_command(command: Command) -> Self {
186 Self {
187 command,
188 windows_hidden: true,
189 windows_breakaway: BreakawayMode::None,
190 caller_job: None,
191 job_assignment: JobAssignmentMode::Strict,
192 unix_containment: UnixContainment::ProcessGroup,
193 process_priority: ProcessPriority::Normal,
194 kill_on_parent_exit: false,
195 hold_suspended: false,
196 partial_spawn_cleanup_timeout: None,
197 inherited_handles: None,
198 #[cfg(windows)]
199 windows_spawn_config: None,
200 #[cfg(windows)]
201 windows_spawn_config_auto: false,
202 run_stdio: None,
203 env_cleared: false,
204 }
205 }
206 pub(crate) fn pipe_stdio(&mut self, stdin_piped: bool) -> &mut Self {
213 self.run_stdio = Some(RunStdio { stdin_piped });
214 self
215 }
216 pub fn command_mut(&mut self) -> &mut Command {
217 #[cfg(windows)]
218 if self.windows_spawn_config_auto {
219 self.windows_spawn_config = None;
221 }
222 &mut self.command
223 }
224 pub fn inherit_handles_only(
226 &mut self,
227 handles: impl IntoIterator<Item = RawHandleOrFd>,
228 ) -> &mut Self {
229 #[cfg(windows)]
230 let handles = handles.into_iter().map(|handle| handle as usize).collect();
231 #[cfg(unix)]
232 let handles = handles.into_iter().collect();
233 self.inherited_handles = Some(handles);
234 self
235 }
236 #[cfg(windows)]
242 pub fn windows_spawn_config(&mut self, config: crate::WindowsSpawnConfig) -> &mut Self {
243 self.windows_spawn_config = Some(config);
244 self.windows_spawn_config_auto = false;
245 self
246 }
247 pub fn env_allowlist<I, S>(&mut self, keys: I) -> &mut Self
249 where
250 I: IntoIterator<Item = S>,
251 S: AsRef<OsStr>,
252 {
253 let kept: Vec<_> = keys
254 .into_iter()
255 .filter_map(|k| std::env::var_os(k.as_ref()).map(|v| (k.as_ref().to_os_string(), v)))
256 .collect();
257 self.command.env_clear();
258 self.env_cleared = true;
259 for (k, v) in kept {
260 self.command.env(k, v);
261 }
262 self
263 }
264 pub fn env_allowlist_with_defaults<I, S>(&mut self, extra: I) -> &mut Self
268 where
269 I: IntoIterator<Item = S>,
270 S: AsRef<OsStr>,
271 {
272 let keys: Vec<std::ffi::OsString> = DEFAULT_ENV_ALLOWLIST
273 .iter()
274 .map(std::ffi::OsString::from)
275 .chain(extra.into_iter().map(|k| k.as_ref().to_os_string()))
276 .collect();
277 self.env_allowlist(keys)
278 }
279 pub fn env_is_cleared(&self) -> bool {
284 self.env_cleared
285 }
286 pub fn env_strip<I, S>(&mut self, keys: I) -> &mut Self
287 where
288 I: IntoIterator<Item = S>,
289 S: AsRef<OsStr>,
290 {
291 for k in keys {
292 self.command.env_remove(k);
293 }
294 self
295 }
296 pub fn windows_hide(&mut self) -> &mut Self {
298 self.windows_hidden = true;
299 self
300 }
301 pub fn windows_visible(&mut self) -> &mut Self {
306 self.windows_hidden = false;
307 self
308 }
309 pub fn windows_allow_breakaway(&mut self, allow: bool) -> &mut Self {
316 self.windows_breakaway = if allow {
317 BreakawayMode::Explicit
318 } else {
319 BreakawayMode::None
320 };
321 self
322 }
323 pub fn windows_allow_silent_breakaway(&mut self, allow: bool) -> &mut Self {
328 self.windows_breakaway = if allow {
329 BreakawayMode::Silent
330 } else {
331 BreakawayMode::None
332 };
333 self
334 }
335 #[cfg(windows)]
337 pub fn windows_job(
338 &mut self,
339 job: std::os::windows::io::BorrowedHandle<'_>,
340 ) -> io::Result<&mut Self> {
341 self.caller_job = Some(job.try_clone_to_owned()?);
342 Ok(self)
343 }
344 pub fn job_assignment_mode(&mut self, mode: JobAssignmentMode) -> &mut Self {
346 self.job_assignment = mode;
347 self
348 }
349 pub fn unix_containment(&mut self, mode: UnixContainment) -> &mut Self {
351 self.unix_containment = mode;
352 self
353 }
354 pub fn process_priority(&mut self, priority: ProcessPriority) -> &mut Self {
358 self.process_priority = priority;
359 self
360 }
361 pub fn kill_on_parent_exit(&mut self, enabled: bool) -> &mut Self {
383 self.kill_on_parent_exit = enabled;
384 self
385 }
386 pub fn partial_spawn_cleanup_timeout(&mut self, timeout: Duration) -> &mut Self {
388 self.partial_spawn_cleanup_timeout = Some(timeout);
389 self
390 }
391 pub fn spawn(self) -> io::Result<OwnedChild> {
392 self.spawn_inner(false, false, None)
393 }
394 pub fn spawn_suspended(mut self) -> io::Result<OwnedChild> {
404 self.hold_suspended = true;
405 self.spawn_inner(false, false, None)
406 }
407 pub fn spawn_uncontained_detached(self) -> io::Result<OwnedChild> {
422 self.spawn_inner_with(true, false, None, true)
423 }
424 pub fn spawn_uncontained_detached_strict(self) -> io::Result<OwnedChild> {
430 match self.spawn_inner_with(true, false, None, false) {
431 Err(error) if cfg!(windows) && is_denied_breakaway(&error) => Err(io::Error::new(
432 io::ErrorKind::PermissionDenied,
433 BreakawayDenied,
434 )),
435 other => other,
436 }
437 }
438 fn spawn_inner(
439 self,
440 detached: bool,
441 fail_after_spawn: bool,
442 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
443 ) -> io::Result<OwnedChild> {
444 self.spawn_inner_with(detached, fail_after_spawn, captured_pid, false)
445 }
446 fn spawn_inner_with(
447 mut self,
448 detached: bool,
449 fail_after_spawn: bool,
450 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
451 in_job_fallback: bool,
452 ) -> io::Result<OwnedChild> {
453 if detached && self.caller_job.is_some() {
454 return Err(io::Error::new(
455 io::ErrorKind::InvalidInput,
456 "detached launch cannot join a caller job",
457 ));
458 }
459 if detached && self.kill_on_parent_exit {
460 return Err(io::Error::new(
461 io::ErrorKind::InvalidInput,
462 "kill_on_parent_exit cannot combine with a detached launch",
463 ));
464 }
465 if let Some(RunStdio { stdin_piped }) = self.run_stdio {
466 self.command
469 .stdin(if stdin_piped {
470 Stdio::piped()
471 } else {
472 Stdio::null()
473 })
474 .stdout(Stdio::piped())
475 .stderr(Stdio::piped());
476 #[cfg(windows)]
477 if let Some(config) = self.windows_spawn_config.as_mut() {
478 config.stdin = if stdin_piped {
479 crate::WindowsStdio::Piped
480 } else {
481 crate::WindowsStdio::Null
482 };
483 config.stdout = crate::WindowsStdio::Piped;
484 config.stderr = crate::WindowsStdio::Piped;
485 }
486 }
487 #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
488 if self.kill_on_parent_exit {
489 return Err(io::Error::new(
490 io::ErrorKind::Unsupported,
491 "kill_on_parent_exit is supported on Linux and macOS only",
492 ));
493 }
494 #[cfg(unix)]
495 {
496 use std::os::unix::process::CommandExt;
497 let session = detached || self.unix_containment == UnixContainment::Session;
498 let nice_floor = self.process_priority.nice_floor();
499 #[cfg(target_os = "macos")]
500 let utility_qos = self.process_priority == ProcessPriority::Efficient;
501 let inheritance = self
502 .inherited_handles
503 .take()
504 .map(crate::process_unix::Inheritance::new)
505 .transpose()?;
506 #[cfg(target_os = "linux")]
508 let supervisor = self
509 .kill_on_parent_exit
510 .then(|| unsafe { libc::getpid() });
512 unsafe {
514 self.command.pre_exec(move || {
515 let result = if session {
516 libc::setsid()
517 } else {
518 libc::setpgid(0, 0)
519 };
520 if result < 0 {
521 return Err(io::Error::last_os_error());
522 }
523 if let Some(floor) = nice_floor {
524 crate::process_unix::raise_nice_to(floor)?;
525 }
526 #[cfg(target_os = "macos")]
527 if utility_qos {
528 crate::process_unix::set_utility_qos();
529 }
530 if let Some(inheritance) = &inheritance {
531 inheritance.apply()?;
532 }
533 #[cfg(target_os = "linux")]
534 if let Some(supervisor) = supervisor {
535 crate::process_unix::die_with_parent(supervisor)?;
536 }
537 Ok(())
538 });
539 }
540 }
541 #[cfg(windows)]
542 crate::process_windows::configure(
543 &mut self.command,
544 detached,
545 self.windows_hidden,
546 self.process_priority,
547 );
548 #[cfg(unix)]
549 let _ = (
550 self.windows_hidden,
551 self.windows_breakaway,
552 self.job_assignment,
553 );
554 #[cfg(windows)]
555 let _ = (self.unix_containment, self.kill_on_parent_exit);
556 let spawned_at = SystemTime::now();
557 let hold_suspended = self.hold_suspended && !detached;
558 #[cfg(unix)]
559 let _ = hold_suspended;
560 #[cfg(unix)]
561 let child = {
562 let mut child = crate::unix_child::UnixChild::new(self.command.spawn()?);
563 child.require_identity()?;
566 #[cfg(target_os = "macos")]
569 if self.kill_on_parent_exit {
570 if let Err(error) = crate::process_unix::arm_parent_death_watch(child.id()) {
571 let _ = child.signal(SignalTarget::Group, libc::SIGKILL);
572 let _ = child.kill();
573 let _ = child.wait();
574 return Err(error);
575 }
576 }
577 child
578 };
579 #[cfg(windows)]
580 let (child, stayed_in_parent_job) = match self.inherited_handles.as_deref() {
581 None => {
582 let _spawning = crate::spawn_windows::spawn_guard();
585 match self.command.spawn() {
586 Ok(child) => (PlatformChild::from(child), false),
587 Err(error)
589 if detached
590 && in_job_fallback
591 && crate::process_windows::is_breakaway_denied(&error) =>
592 {
593 crate::process_windows::configure_in_job_detached(
594 &mut self.command,
595 self.windows_hidden,
596 self.process_priority,
597 );
598 (PlatformChild::from(self.command.spawn()?), true)
599 }
600 Err(error) => return Err(error),
601 }
602 }
603 Some(handles) => {
604 let config = self.windows_spawn_config.take().ok_or_else(|| {
605 io::Error::new(io::ErrorKind::InvalidInput,
606 "allowlisted Windows spawn requires windows_spawn_config for opaque Command settings")
607 })?;
608 crate::spawn_windows::spawn(
609 &self.command,
610 config,
611 handles,
612 detached,
613 self.windows_hidden,
614 self.process_priority,
615 self.env_cleared,
616 self.partial_spawn_cleanup_timeout,
617 in_job_fallback,
618 )?
619 }
620 };
621 #[cfg(not(windows))]
622 let _ = in_job_fallback;
623 #[cfg(not(windows))]
624 let stayed_in_parent_job = false;
625 let pid = child.id();
626 if let Some(captured) = captured_pid {
627 captured.store(pid, std::sync::atomic::Ordering::SeqCst);
628 }
629 let child = Arc::new(Mutex::new(child));
630 let handle = match ProcessHandle::new(&child, spawned_at) {
631 Ok(handle) => handle,
632 Err(error) => {
633 cleanup_direct(child, self.partial_spawn_cleanup_timeout);
634 return Err(error);
635 }
636 };
637 let mut partial = PartialChild {
638 child: Some(OwnedChild {
639 child,
640 handle,
641 pid,
642 detached,
643 terminate_on_drop: !detached,
644 drop_reap_timeout: DEFAULT_DROP_REAP_TIMEOUT,
645 tree_terminated: false,
646 assignment_failures: Vec::new(),
647 suspended: std::sync::atomic::AtomicBool::new(cfg!(windows) && hold_suspended),
648 stayed_in_parent_job,
649 #[cfg(windows)]
650 job: None,
651 }),
652 timeout: self.partial_spawn_cleanup_timeout,
653 };
654 #[cfg(windows)]
655 if !detached {
656 let owned = partial.child.as_mut().expect("partial child is present");
657 crate::process_windows::assign_jobs(
658 &owned.handle,
659 self.caller_job.take(),
660 self.job_assignment,
661 self.windows_breakaway,
662 &mut owned.job,
663 &mut owned.assignment_failures,
664 )?;
665 }
666 #[cfg(windows)]
667 {
668 let owned = partial.child.as_ref().expect("partial child is present");
669 if self.process_priority == ProcessPriority::Efficient {
670 let _ = crate::process_windows::apply_efficiency(&*owned.child());
672 }
673 if !hold_suspended {
674 owned.child().resume(detached)?;
675 }
676 }
677 if fail_after_spawn {
678 return Err(io::Error::other("injected post-spawn wrapping failure"));
679 }
680 let owned = partial.child.take().expect("partial child is present");
681 if !detached {
682 #[cfg(unix)]
684 crate::registry::register(&owned.child, owned.pid);
685 #[cfg(windows)]
686 crate::registry::register(
687 &owned.child,
688 &owned.handle,
689 owned
690 .job
691 .as_ref()
692 .map_or_else(std::sync::Weak::new, std::sync::Arc::downgrade),
693 owned.pid,
694 );
695 }
696 Ok(owned)
697 }
698}
699
700#[derive(Clone, Copy, Debug, Eq, PartialEq)]
706pub struct BreakawayDenied;
707
708impl std::fmt::Display for BreakawayDenied {
709 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
710 f.write_str(
711 "detached launch denied: the caller's job object does not allow breakaway \
712 (ERROR_ACCESS_DENIED); no fallback was attempted",
713 )
714 }
715}
716
717impl std::error::Error for BreakawayDenied {}
718
719#[cfg(windows)]
720fn is_denied_breakaway(error: &io::Error) -> bool {
721 crate::process_windows::is_breakaway_denied(error)
722}
723#[cfg(not(windows))]
724fn is_denied_breakaway(_error: &io::Error) -> bool {
725 false
726}
727
728pub const DEFAULT_DROP_REAP_TIMEOUT: Duration = Duration::from_secs(2);
731
732pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
733 "PATH",
734 "HOME",
735 "USER",
736 "LOGNAME",
737 "LANG",
738 "LC_ALL",
739 "TMPDIR",
740 "TEMP",
741 "TMP",
742 "SystemRoot",
743 "SYSTEMROOT",
744 "USERPROFILE",
745 "APPDATA",
746 "LOCALAPPDATA",
747 "ProgramData",
748 "COMSPEC",
749 "PATHEXT",
750 "XDG_DATA_HOME",
751 "XDG_RUNTIME_DIR",
752];
753
754#[derive(Debug)]
755pub struct OwnedChild {
756 child: Arc<Mutex<PlatformChild>>,
757 handle: ProcessHandle,
758 pid: u32,
760 detached: bool,
761 terminate_on_drop: bool,
762 drop_reap_timeout: Duration,
763 tree_terminated: bool,
764 assignment_failures: Vec<JobAssignmentFailure>,
765 suspended: std::sync::atomic::AtomicBool,
767 stayed_in_parent_job: bool,
769 #[cfg(windows)]
772 job: Option<std::sync::Arc<crate::process_windows::KillOnCloseJob>>,
773}
774#[derive(Clone, Debug, Eq, PartialEq)]
776pub struct TreeTermination {
777 pub status: ExitStatus,
779 pub descendants: crate::SweepReport,
781}
782#[derive(Clone, Copy, Debug, Eq, PartialEq)]
783pub enum ReapOutcome {
784 Exited(ExitStatus),
785 TimedOut,
786}
787#[derive(Clone, Copy, Debug, Eq, PartialEq)]
788pub enum WaitOutcome {
789 Exited(ExitStatus),
790 Terminated(ExitStatus),
791}
792
793impl OwnedChild {
794 pub fn id(&self) -> u32 {
795 self.pid
796 }
797 pub fn stayed_in_parent_job(&self) -> bool {
802 self.stayed_in_parent_job
803 }
804 pub fn child(&self) -> MutexGuard<'_, PlatformChild> {
820 lock_child(&self.child)
821 }
822 pub fn resume(&self) -> io::Result<()> {
827 if self
828 .suspended
829 .swap(false, std::sync::atomic::Ordering::SeqCst)
830 {
831 #[cfg(windows)]
832 if let Err(error) = self.child().resume(self.detached) {
833 self.suspended
834 .store(true, std::sync::atomic::Ordering::SeqCst);
835 return Err(error);
836 }
837 }
838 Ok(())
839 }
840 pub fn is_suspended(&self) -> bool {
842 self.suspended.load(std::sync::atomic::Ordering::SeqCst)
843 }
844 pub fn while_unreaped<R>(&self, act: impl FnOnce(u32) -> R) -> io::Result<Option<R>> {
853 #[cfg(unix)]
854 {
855 let mut child = lock_child(&self.child);
856 if child.leader_state()? == LeaderState::Reaped {
857 return Ok(None);
858 }
859 Ok(Some(act(self.pid)))
860 }
861 #[cfg(windows)]
862 Ok(Some(act(self.pid)))
863 }
864 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
865 self.child().stdin.take()
866 }
867 pub fn close_stdin(&mut self) {
871 drop(self.child().stdin.take());
872 }
873 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
874 self.child().stdout.take()
875 }
876 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
877 self.child().stderr.take()
878 }
879 pub fn job_assignment_failures(&self) -> &[JobAssignmentFailure] {
880 &self.assignment_failures
881 }
882 pub fn process_handle(&self) -> io::Result<ProcessHandle> {
886 self.handle.try_clone()
887 }
888 pub fn creation_time(&self) -> Option<SystemTime> {
889 self.handle.creation_time()
890 }
891 #[cfg(windows)]
892 pub fn creation_time_ticks(&self) -> Option<u64> {
893 self.handle.creation_time_ticks()
894 }
895 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
896 self.handle.try_wait()
898 }
899 pub fn wait(&mut self) -> io::Result<ExitStatus> {
902 drop(self.child().stdin.take());
903 self.handle.wait()
904 }
905 pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
906 self.handle.wait_timeout(timeout)
907 }
908 pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
918 self.wait_or_kill_inner(grace, true)
919 }
920 pub fn wait_then_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
926 self.wait_or_kill_inner(grace, false)
927 }
928 fn wait_or_kill_inner(&mut self, grace: Duration, term_first: bool) -> io::Result<WaitOutcome> {
929 #[cfg(not(unix))]
930 let _ = term_first;
931 #[cfg(unix)]
932 {
933 if term_first && !self.tree_terminated {
934 self.signal(libc::SIGTERM)?;
935 }
936 if self.handle.wait_exited_timeout(grace)? {
938 self.force_kill(1)?;
943 return self.wait().map(WaitOutcome::Exited);
944 }
945 self.terminate_tree().map(WaitOutcome::Terminated)
946 }
947 #[cfg(windows)]
948 {
949 if let Some(status) = self.wait_timeout(grace)? {
950 self.force_kill(1)?;
954 return Ok(WaitOutcome::Exited(status));
955 }
956 self.terminate_tree().map(WaitOutcome::Terminated)
957 }
958 }
959 pub(crate) fn wait_exit_then_sweep(
965 &mut self,
966 timeout: Duration,
967 ) -> io::Result<Option<ExitStatus>> {
968 if self.handle.wait_exited_timeout(timeout)? {
969 self.force_kill(1)?;
970 return self.wait().map(Some);
971 }
972 Ok(None)
973 }
974 pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
979 self.force_kill(1)?;
980 self.wait()
981 }
982 pub fn terminate_tree_with_options(
987 &mut self,
988 options: TerminationOptions,
989 ) -> io::Result<ExitStatus> {
990 self.start_termination(options)?;
991 self.wait()
992 }
993 pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
994 self.terminate_tree_bounded_with_options(timeout, TerminationOptions::default())
995 }
996 pub fn terminate_tree_bounded_with_options(
998 &mut self,
999 timeout: Duration,
1000 mut options: TerminationOptions,
1001 ) -> io::Result<ReapOutcome> {
1002 let started = Instant::now();
1003 options.grace = options.grace.min(timeout);
1004 self.start_termination(options)?;
1005 Ok(
1006 match self.wait_timeout(timeout.saturating_sub(started.elapsed()))? {
1007 Some(status) => ReapOutcome::Exited(status),
1008 None => ReapOutcome::TimedOut,
1009 },
1010 )
1011 }
1012 pub fn terminate_tree_deep(
1036 &mut self,
1037 options: TerminationOptions,
1038 ) -> io::Result<TreeTermination> {
1039 let mut found = self.descendant_identities()?;
1040 found.reverse(); #[cfg(unix)]
1042 if !self.tree_terminated {
1043 signal_shared(&self.child, SignalTarget::Group, libc::SIGTERM)?;
1044 await_tree_shared(&self.child, &self.handle, options.grace, true)?;
1045 signal_shared(&self.child, SignalTarget::Group, libc::SIGKILL)?;
1046 self.tree_terminated = true;
1047 }
1048 #[cfg(windows)]
1049 self.force_kill(options.windows_exit_code)?;
1050 let status = self.wait()?;
1051 let descendants = crate::sweep(&found, options.grace)?;
1052 Ok(TreeTermination {
1053 status,
1054 descendants,
1055 })
1056 }
1057 fn start_termination(&mut self, options: TerminationOptions) -> io::Result<()> {
1058 #[cfg(unix)]
1059 if !self.tree_terminated {
1060 self.signal(libc::SIGTERM)?;
1061 self.await_tree(options.grace)?;
1065 }
1066 self.force_kill(options.windows_exit_code)
1067 }
1068 #[cfg(unix)]
1075 fn await_tree(&self, grace: Duration) -> io::Result<()> {
1076 await_tree_shared(&self.child, &self.handle, grace, !self.detached)
1078 }
1079 #[cfg(unix)]
1083 fn signal(&self, signal: i32) -> io::Result<()> {
1084 let target = if self.detached {
1087 SignalTarget::Leader
1088 } else {
1089 SignalTarget::Group
1090 };
1091 signal_shared(&self.child, target, signal).map(|_| ())
1092 }
1093 fn force_kill(&mut self, code: u32) -> io::Result<()> {
1094 if self.tree_terminated {
1095 return Ok(());
1096 }
1097 #[cfg(unix)]
1098 {
1099 let _ = code;
1100 self.signal(libc::SIGKILL)?;
1101 }
1102 #[cfg(windows)]
1103 crate::process_windows::terminate(&self.handle, self.job.as_deref(), code)?;
1104 self.tree_terminated = true;
1105 Ok(())
1106 }
1107}
1108impl OwnedChild {
1109 pub fn set_drop_reap_timeout(&mut self, timeout: Duration) -> &mut Self {
1116 self.drop_reap_timeout = timeout;
1117 self
1118 }
1119 pub fn drop_reap_timeout(&self) -> Duration {
1121 self.drop_reap_timeout
1122 }
1123 fn reap_in_background(&self) {
1125 #[cfg(unix)]
1126 if let Ok(reader) = self.handle.try_clone() {
1127 let _ = thread::Builder::new()
1128 .name("rightkit-drop-reap".into())
1129 .spawn(move || {
1130 let _ = reader.wait();
1131 });
1132 }
1133 }
1134 pub fn descendant_identities(&self) -> io::Result<Vec<crate::AdoptedIdentity>> {
1138 let root = self.while_unreaped(|pid| match NonZeroU32::new(pid) {
1139 Some(pid) => crate::AdoptedIdentity::capture(pid),
1140 None => Ok(None),
1141 })?;
1142 match root {
1143 Some(Ok(Some(root))) => root.descendants(),
1144 Some(Err(error)) => Err(error),
1145 _ => Ok(Vec::new()),
1146 }
1147 }
1148}
1149impl Drop for OwnedChild {
1150 fn drop(&mut self) {
1151 crate::registry::forget(&self.child);
1153 if self.terminate_on_drop {
1154 if self.force_kill(1).is_ok() {
1158 drop(self.child().stdin.take());
1159 if !matches!(
1160 self.handle.wait_timeout(self.drop_reap_timeout),
1161 Ok(Some(_))
1162 ) {
1163 self.reap_in_background();
1164 }
1165 }
1166 } else if self.detached {
1167 #[cfg(unix)]
1168 {
1169 {
1173 let mut child = self.child();
1174 drop(child.stdin.take());
1175 drop(child.stdout.take());
1176 drop(child.stderr.take());
1177 }
1178 if let Ok(reader) = self.handle.try_clone() {
1179 let _ = thread::Builder::new()
1180 .name("rightkit-detached-reap".into())
1181 .spawn(move || {
1182 let _ = reader.wait();
1183 });
1184 }
1185 }
1186 }
1187 }
1188}
1189
1190struct PartialChild {
1191 child: Option<OwnedChild>,
1192 timeout: Option<Duration>,
1193}
1194impl Drop for PartialChild {
1195 fn drop(&mut self) {
1196 if let Some(mut child) = self.child.take() {
1197 child.terminate_on_drop = false;
1199 let _ = child.force_kill(1);
1200 cleanup_direct(Arc::clone(&child.child), self.timeout);
1201 }
1202 }
1203}
1204#[cfg(unix)]
1207const MID_EXIT_RETRY: Duration = Duration::from_millis(300);
1208
1209#[cfg(unix)]
1213pub(crate) fn signal_shared(
1214 shared: &Mutex<crate::unix_child::UnixChild>,
1215 target: SignalTarget,
1216 signal: i32,
1217) -> io::Result<SignalOutcome> {
1218 let started = Instant::now();
1219 loop {
1220 let (outcome, watch) = {
1221 let mut child = lock_child(shared);
1222 (child.signal(target, signal)?, child.exit_watch())
1223 };
1224 if outcome != SignalOutcome::MidExit {
1225 return Ok(outcome);
1226 }
1227 let remaining = MID_EXIT_RETRY.saturating_sub(started.elapsed());
1230 if remaining.is_zero() {
1231 return Err(io::Error::from_raw_os_error(libc::EPERM));
1232 }
1233 watch.pause(remaining);
1234 }
1235}
1236
1237#[cfg(unix)]
1239pub(crate) fn await_tree_shared(
1240 shared: &Mutex<crate::unix_child::UnixChild>,
1241 handle: &ProcessHandle,
1242 grace: Duration,
1243 whole_group: bool,
1244) -> io::Result<()> {
1245 use crate::{exit_watch::POLL_INTERVAL, unix_child::Members};
1246 let Some(deadline) = Instant::now().checked_add(grace) else {
1247 return Ok(());
1248 };
1249 let left = |deadline: Instant| deadline.saturating_duration_since(Instant::now());
1250 if !handle.wait_exited_timeout(left(deadline))? {
1251 return Ok(()); }
1253 if !whole_group {
1254 return Ok(());
1255 }
1256 loop {
1257 match lock_child(shared).group_other_members()? {
1258 Members::Pids(pids) if pids.is_empty() => return Ok(()),
1259 Members::Pids(_) => {}
1260 Members::Unknown => return wait_out(left(deadline)),
1261 }
1262 let remaining = left(deadline);
1263 if remaining.is_zero() {
1264 return Ok(()); }
1266 thread::sleep(POLL_INTERVAL.min(remaining));
1267 }
1268}
1269
1270#[cfg(unix)]
1273fn wait_out(remaining: Duration) -> io::Result<()> {
1274 thread::sleep(remaining);
1275 Ok(())
1276}
1277pub(crate) fn cleanup_direct(child: Arc<Mutex<PlatformChild>>, timeout: Option<Duration>) {
1284 let _ = lock_child(&child).kill();
1285 let Some(timeout) = timeout else {
1286 let _ = lock_child(&child).wait();
1288 return;
1289 };
1290 #[cfg(unix)]
1291 {
1292 let watch = lock_child(&child).exit_watch();
1295 let deadline = Instant::now().checked_add(timeout);
1296 if matches!(
1297 watch.wait_for(deadline, || lock_child(&child).try_wait()),
1298 Ok(Some(_))
1299 ) {
1300 return;
1301 }
1302 }
1303 #[cfg(windows)]
1304 {
1305 let started = Instant::now();
1306 loop {
1307 match lock_child(&child).try_wait() {
1308 Ok(Some(_)) => return,
1309 Err(_) => break,
1310 Ok(None) => {}
1311 }
1312 if started.elapsed() >= timeout {
1313 break;
1314 }
1315 thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
1316 }
1317 }
1318 let _ = thread::Builder::new()
1322 .name("rightkit-partial-reap".into())
1323 .spawn(move || {
1324 let _ = lock_child(&child).wait();
1325 });
1326}
1327
1328#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1329pub struct AdoptedProcess {
1330 pid: NonZeroU32,
1331}
1332impl AdoptedProcess {
1333 pub fn new(pid: NonZeroU32) -> Self {
1334 Self { pid }
1335 }
1336 pub fn id(&self) -> u32 {
1337 self.pid.get()
1338 }
1339 pub fn is_running(&self) -> io::Result<bool> {
1343 process_is_running(self.pid)
1344 }
1345 pub fn identity(&self) -> io::Result<Option<crate::AdoptedIdentity>> {
1350 crate::AdoptedIdentity::capture(self.pid)
1351 }
1352}
1353#[cfg(test)]
1354fn spawn_for_test(
1355 command: Command,
1356 captured_pid: Arc<std::sync::atomic::AtomicU32>,
1357) -> io::Result<OwnedChild> {
1358 OwnedCommand::from_command(command).spawn_inner(false, true, Some(captured_pid))
1359}
1360
1361#[cfg(windows)]
1362fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1363 use windows::Win32::{
1364 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
1365 System::Threading::{
1366 OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
1367 PROCESS_SYNCHRONIZE,
1368 },
1369 };
1370
1371 let handle = unsafe {
1372 OpenProcess(
1373 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
1374 false,
1375 pid.get(),
1376 )
1377 };
1378 let handle = match handle {
1379 Ok(handle) => handle,
1380 Err(error) => {
1381 if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
1382 return Ok(false);
1383 }
1384 return Err(io::Error::other(error.to_string()));
1385 }
1386 };
1387 let wait = unsafe { WaitForSingleObject(handle, 0) };
1388 let close = unsafe { CloseHandle(handle) };
1389 close.map_err(|error| io::Error::other(error.to_string()))?;
1390 match wait.0 {
1391 0 => Ok(false),
1392 value if value == WAIT_TIMEOUT.0 => Ok(true),
1393 _ => Err(io::Error::last_os_error()),
1394 }
1395}
1396
1397#[cfg(unix)]
1398fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1399 let raw_pid = i32::try_from(pid.get()).map_err(io::Error::other)?;
1400 loop {
1401 if unsafe { libc::kill(raw_pid, 0) } == 0 {
1403 return Ok(!crate::foreign::process_is_zombie(pid.get()));
1406 }
1407 let error = io::Error::last_os_error();
1408 match error.raw_os_error() {
1409 Some(libc::EINTR) => continue,
1410 Some(libc::EPERM) => return Ok(!crate::foreign::process_is_zombie(pid.get())),
1411 Some(libc::ESRCH) => return Ok(false),
1412 _ => return Err(error),
1413 }
1414 }
1415}
1416
1417#[cfg(test)]
1418mod tests {
1419 #[test]
1420 fn option_defaults_preserve_existing_behavior() {
1421 let command = super::OwnedCommand::new("unused");
1422 assert_eq!(command.job_assignment, super::JobAssignmentMode::Strict);
1423 assert_eq!(
1424 command.unix_containment,
1425 super::UnixContainment::ProcessGroup
1426 );
1427 assert_eq!(command.partial_spawn_cleanup_timeout, None);
1428 assert_eq!(
1429 super::TerminationOptions::default().grace,
1430 std::time::Duration::ZERO
1431 );
1432 assert_eq!(super::TerminationOptions::default().windows_exit_code, 1);
1433 }
1434
1435 #[cfg(not(windows))]
1436 #[test]
1437 fn new_and_from_command_share_spawn_settings() {
1438 use super::OwnedCommand;
1439 use std::process::Command;
1440 let by_new = OwnedCommand::new("unused");
1442 let by_from = OwnedCommand::from_command(Command::new("unused"));
1443 assert_eq!(format!("{by_new:?}"), format!("{by_from:?}"));
1444 }
1445
1446 #[cfg(windows)]
1450 #[test]
1451 fn only_new_supplies_a_default_windows_spawn_config() {
1452 use super::OwnedCommand;
1453 use std::process::Command;
1454 let by_new = OwnedCommand::new("unused");
1455 let by_from = OwnedCommand::from_command(Command::new("unused"));
1456 assert!(by_new.windows_spawn_config.is_some() && by_new.windows_spawn_config_auto);
1457 assert!(by_from.windows_spawn_config.is_none() && !by_from.windows_spawn_config_auto);
1458 }
1459
1460 #[test]
1461 fn process_priority_defaults_to_normal_and_keeps_the_requested_value() {
1462 use super::{OwnedCommand, ProcessPriority};
1463 let mut command = OwnedCommand::new("unused");
1464 assert_eq!(command.process_priority, ProcessPriority::Normal);
1465 command.process_priority(ProcessPriority::BelowNormal);
1466 assert_eq!(command.process_priority, ProcessPriority::BelowNormal);
1467 command.process_priority(ProcessPriority::Idle);
1468 assert_eq!(command.process_priority, ProcessPriority::Idle);
1469 }
1470
1471 #[cfg(unix)]
1474 #[test]
1475 fn process_priority_maps_to_nice_floors() {
1476 use super::ProcessPriority;
1477 assert_eq!(ProcessPriority::Normal.nice_floor(), None);
1478 assert_eq!(ProcessPriority::BelowNormal.nice_floor(), Some(10));
1479 assert_eq!(ProcessPriority::Idle.nice_floor(), Some(19));
1480 assert_eq!(ProcessPriority::Efficient.nice_floor(), Some(10));
1481 }
1482
1483 #[cfg(windows)]
1485 #[test]
1486 fn below_normal_child_has_below_normal_priority_class() {
1487 use super::{OwnedCommand, ProcessPriority};
1488 use std::process::Stdio;
1489 use windows::Win32::{
1490 Foundation::CloseHandle,
1491 System::Threading::{
1492 GetPriorityClass, OpenProcess, BELOW_NORMAL_PRIORITY_CLASS,
1493 PROCESS_QUERY_LIMITED_INFORMATION,
1494 },
1495 };
1496 let mut command = OwnedCommand::new("ping");
1497 command
1498 .command_mut()
1499 .args(["-n", "6", "127.0.0.1"])
1500 .stdin(Stdio::null())
1501 .stdout(Stdio::null())
1502 .stderr(Stdio::null());
1503 command.process_priority(ProcessPriority::BelowNormal);
1504 let mut child = command.spawn().expect("spawn ping");
1505 let class = unsafe {
1507 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, child.id())
1508 .expect("open the child");
1509 let class = GetPriorityClass(handle);
1510 let _ = CloseHandle(handle);
1511 class
1512 };
1513 let _ = child.terminate_tree();
1514 assert_eq!(class, BELOW_NORMAL_PRIORITY_CLASS.0);
1515 }
1516
1517 #[cfg(unix)]
1519 #[test]
1520 fn lowered_priority_child_runs_at_or_above_the_nice_floor() {
1521 use super::{OwnedCommand, ProcessPriority};
1522 use std::{io::Read, process::Stdio};
1523 for (priority, floor) in [
1524 (ProcessPriority::BelowNormal, 10),
1525 (ProcessPriority::Idle, 19),
1526 ] {
1527 let mut command = OwnedCommand::new("/bin/sh");
1528 command
1529 .command_mut()
1530 .args(["-c", "ps -o ni= -p $$"])
1531 .stdin(Stdio::null())
1532 .stdout(Stdio::piped())
1533 .stderr(Stdio::null());
1534 command.process_priority(priority);
1535 let mut child = command.spawn().expect("spawn sh");
1536 let mut text = String::new();
1537 child
1538 .take_stdout()
1539 .expect("piped stdout")
1540 .read_to_string(&mut text)
1541 .expect("read ps output");
1542 let _ = child.wait();
1543 let nice: i32 = text.trim().parse().expect("ps printed a nice value");
1544 assert!(nice >= floor, "nice {nice} below floor {floor}");
1545 }
1546 }
1547
1548 #[test]
1549 fn breakaway_is_off_by_default_explicit_by_default_opt_in_and_silent_is_separate() {
1550 use super::BreakawayMode;
1551 let mut command = super::OwnedCommand::new("unused");
1552 assert_eq!(command.windows_breakaway, BreakawayMode::None);
1553 command.windows_allow_breakaway(true);
1554 assert_eq!(command.windows_breakaway, BreakawayMode::Explicit);
1555 command.windows_allow_silent_breakaway(true);
1556 assert_eq!(command.windows_breakaway, BreakawayMode::Silent);
1557 command.windows_allow_silent_breakaway(false);
1558 assert_eq!(command.windows_breakaway, BreakawayMode::None);
1559 }
1560
1561 #[test]
1562 fn kill_on_parent_exit_is_opt_in() {
1563 let mut command = super::OwnedCommand::new("unused");
1564 assert!(!command.kill_on_parent_exit);
1565 command.kill_on_parent_exit(true);
1566 assert!(command.kill_on_parent_exit);
1567 }
1568
1569 #[test]
1570 fn breakaway_denied_is_a_typed_recoverable_error() {
1571 let error =
1572 std::io::Error::new(std::io::ErrorKind::PermissionDenied, super::BreakawayDenied);
1573 assert!(error
1574 .get_ref()
1575 .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1576 .is_some());
1577 assert!(super::BreakawayDenied.to_string().contains("breakaway"));
1578 }
1579
1580 #[test]
1581 fn a_strict_detached_spawn_passes_other_failures_through_untyped() {
1582 let command = super::OwnedCommand::new("rightkit-no-such-program-for-strict-detach");
1583 let Err(error) = command.spawn_uncontained_detached_strict() else {
1584 panic!("a missing program must not spawn");
1585 };
1586 assert!(error
1587 .get_ref()
1588 .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1589 .is_none());
1590 }
1591
1592 #[test]
1593 fn a_strict_detached_spawn_still_rejects_kill_on_parent_exit() {
1594 let mut command = super::OwnedCommand::new("unused");
1595 command.kill_on_parent_exit(true);
1596 let Err(error) = command.spawn_uncontained_detached_strict() else {
1597 panic!("a detached spawn must not accept the parent-death tie");
1598 };
1599 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1600 }
1601
1602 #[test]
1603 fn a_detached_spawn_rejects_kill_on_parent_exit() {
1604 let mut command = super::OwnedCommand::new("unused");
1606 command.kill_on_parent_exit(true);
1607 let Err(error) = command.spawn_uncontained_detached() else {
1608 panic!("a detached spawn must not accept the parent-death tie");
1609 };
1610 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1611 }
1612
1613 #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
1616 #[test]
1617 fn a_host_without_a_parent_death_primitive_refuses_before_starting_anything() {
1618 let marker =
1619 std::env::temp_dir().join(format!("rightkit-process-untied-{}", std::process::id()));
1620 let _ = std::fs::remove_file(&marker);
1621 let mut command = super::OwnedCommand::new("/usr/bin/touch");
1622 command.command_mut().arg(&marker);
1623 command.kill_on_parent_exit(true);
1624 let Err(error) = command.spawn() else {
1625 panic!("kill_on_parent_exit must not run the child untied");
1626 };
1627 assert_eq!(error.kind(), std::io::ErrorKind::Unsupported);
1628 std::thread::sleep(std::time::Duration::from_millis(200));
1629 assert!(!marker.exists(), "the program ran");
1630 }
1631
1632 #[test]
1633 fn option_builders_keep_every_requested_value() {
1634 use super::{JobAssignmentMode, OwnedCommand, TerminationOptions, UnixContainment};
1635 use std::time::Duration;
1636 let mut command = OwnedCommand::new("unused");
1637 command
1638 .job_assignment_mode(JobAssignmentMode::BestEffort)
1639 .unix_containment(UnixContainment::Session)
1640 .partial_spawn_cleanup_timeout(Duration::from_secs(5));
1641 assert_eq!(command.job_assignment, JobAssignmentMode::BestEffort);
1642 assert_eq!(command.unix_containment, UnixContainment::Session);
1643 assert_eq!(
1644 command.partial_spawn_cleanup_timeout,
1645 Some(Duration::from_secs(5))
1646 );
1647 let options = TerminationOptions::default()
1648 .with_grace(Duration::from_millis(100))
1649 .with_windows_exit_code(1067);
1650 assert_eq!(options.grace, Duration::from_millis(100));
1651 assert_eq!(options.windows_exit_code, 1067);
1652 }
1653
1654 #[test]
1655 fn zero_bound_partial_spawn_failure_still_reaps_the_child() {
1656 use std::{
1657 num::NonZeroU32,
1658 process::Command,
1659 sync::{
1660 atomic::{AtomicU32, Ordering},
1661 Arc,
1662 },
1663 thread,
1664 time::{Duration, Instant},
1665 };
1666 let command = if cfg!(windows) {
1667 let mut command = Command::new("powershell.exe");
1668 command.args([
1669 "-NoLogo",
1670 "-NoProfile",
1671 "-NonInteractive",
1672 "-Command",
1673 "Start-Sleep -Seconds 30",
1674 ]);
1675 command
1676 } else {
1677 let mut command = Command::new("/bin/sh");
1678 command.args(["-c", "exec sleep 30"]);
1679 command
1680 };
1681 let mut command = super::OwnedCommand::from_command(command);
1682 command.partial_spawn_cleanup_timeout(Duration::ZERO);
1683 let captured = Arc::new(AtomicU32::new(0));
1684 let started = Instant::now();
1685 assert!(command
1686 .spawn_inner(false, true, Some(Arc::clone(&captured)))
1687 .is_err());
1688 assert!(started.elapsed() < Duration::from_secs(3));
1689 let pid = captured.load(Ordering::SeqCst);
1690 assert_ne!(pid, 0);
1691 let process = super::AdoptedProcess::new(NonZeroU32::new(pid).unwrap());
1692 let started = Instant::now();
1693 while process.is_running().unwrap() {
1694 assert!(started.elapsed() < Duration::from_secs(5));
1695 thread::sleep(Duration::from_millis(5));
1696 }
1697 }
1698
1699 #[test]
1700 fn owned_commands_hide_their_console_window_by_default() {
1701 assert!(super::OwnedCommand::new("x").windows_hidden);
1702 assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
1703 }
1704
1705 #[cfg(windows)]
1707 fn host_job() -> std::os::windows::io::OwnedHandle {
1708 use std::os::windows::io::FromRawHandle;
1709 use windows::Win32::System::JobObjects::{
1710 CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject,
1711 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
1712 };
1713 let job = unsafe { CreateJobObjectW(None, None) }.unwrap();
1714 let job_handle = unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(job.0 as _) };
1715 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
1716 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
1717 unsafe {
1718 SetInformationJobObject(
1719 job,
1720 JobObjectExtendedLimitInformation,
1721 &info as *const _ as *const _,
1722 std::mem::size_of_val(&info) as u32,
1723 )
1724 }
1725 .unwrap();
1726 job_handle
1727 }
1728
1729 #[cfg(windows)]
1730 fn in_job(pid: u32, job: &std::os::windows::io::OwnedHandle) -> bool {
1731 use std::os::windows::io::AsRawHandle;
1732 #[link(name = "kernel32")]
1733 unsafe extern "system" {
1734 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> *mut std::ffi::c_void;
1735 fn IsProcessInJob(
1736 process: *mut std::ffi::c_void,
1737 job: *mut std::ffi::c_void,
1738 result: *mut i32,
1739 ) -> i32;
1740 fn CloseHandle(handle: *mut std::ffi::c_void) -> i32;
1741 }
1742 unsafe {
1743 let process = OpenProcess(0x1000, 0, pid); assert!(!process.is_null());
1745 let mut inside = 0;
1746 assert_ne!(
1747 IsProcessInJob(process, job.as_raw_handle() as _, &mut inside),
1748 0
1749 );
1750 CloseHandle(process);
1751 inside != 0
1752 }
1753 }
1754
1755 #[cfg(windows)]
1756 #[test]
1757 fn children_join_the_callers_job_and_die_when_the_caller_closes_it() {
1758 use std::os::windows::io::AsHandle;
1759 use std::time::{Duration, Instant};
1760 let job = host_job();
1761 let sleeper = || {
1762 let mut command = super::OwnedCommand::new("powershell.exe");
1763 command.command_mut().args([
1764 "-NoLogo",
1765 "-NoProfile",
1766 "-NonInteractive",
1767 "-Command",
1768 "Start-Sleep -Seconds 60",
1769 ]);
1770 command.windows_job(job.as_handle()).unwrap();
1771 command.spawn().unwrap()
1772 };
1773 let (first, second) = (sleeper(), sleeper());
1776 let pids = [first.id(), second.id()];
1777 assert!(
1778 pids.iter().all(|pid| in_job(*pid, &job)),
1779 "children must be in the caller's job"
1780 );
1781
1782 drop(job);
1784 let deadline = Instant::now() + Duration::from_secs(5);
1785 for pid in pids {
1786 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1787 while process.is_running().unwrap_or(false) {
1788 assert!(
1789 Instant::now() < deadline,
1790 "child {pid} survived its caller's job"
1791 );
1792 std::thread::sleep(Duration::from_millis(20));
1793 }
1794 }
1795 drop((first, second));
1796 }
1797
1798 #[test]
1799 fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
1800 use std::{
1801 process::Command,
1802 sync::{
1803 atomic::{AtomicU32, Ordering},
1804 Arc,
1805 },
1806 thread,
1807 time::{Duration, Instant},
1808 };
1809
1810 let command = if cfg!(windows) {
1812 let mut command = Command::new("powershell.exe");
1814 command.args([
1815 "-NoLogo",
1816 "-NoProfile",
1817 "-NonInteractive",
1818 "-Command",
1819 "Start-Sleep -Seconds 60",
1820 ]);
1821 command
1822 } else {
1823 let mut command = Command::new("sleep");
1825 command.arg("60");
1826 command
1827 };
1828 let spawned_pid = Arc::new(AtomicU32::new(0));
1829
1830 assert!(super::spawn_for_test(command, Arc::clone(&spawned_pid)).is_err());
1831 let pid = spawned_pid.load(Ordering::SeqCst);
1832 assert_ne!(pid, 0, "test must fail after the OS process was spawned");
1833 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1834 let deadline = Instant::now() + Duration::from_secs(5);
1835 while process.is_running().unwrap_or(false) {
1836 assert!(
1837 Instant::now() < deadline,
1838 "partially spawned process leaked"
1839 );
1840 thread::sleep(Duration::from_millis(20));
1841 }
1842 }
1843}