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(unix)]
147 inherited_fds: Vec<std::os::fd::RawFd>,
148 inherit_all_descriptors: bool,
150 #[cfg(windows)]
151 windows_spawn_config: Option<crate::WindowsSpawnConfig>,
152 #[cfg(windows)]
153 windows_spawn_config_auto: bool,
154 run_stdio: Option<RunStdio>,
157 env_cleared: bool,
158}
159
160#[derive(Clone, Copy, Debug)]
163struct RunStdio {
164 stdin_piped: bool,
165}
166#[cfg(windows)]
167type CallerJob = Option<std::os::windows::io::OwnedHandle>;
168#[cfg(not(windows))]
169type CallerJob = Option<std::convert::Infallible>;
170
171impl OwnedCommand {
172 pub fn new(program: impl AsRef<OsStr>) -> Self {
173 let command = Self::from_command(Command::new(program));
174 #[cfg(windows)]
175 let command = {
176 let mut command = command;
177 command.windows_spawn_config = Some(crate::WindowsSpawnConfig::default());
180 command.windows_spawn_config_auto = true;
181 command
182 };
183 command
184 }
185 pub fn from_command(command: Command) -> Self {
191 Self {
192 command,
193 windows_hidden: true,
194 windows_breakaway: BreakawayMode::None,
195 caller_job: None,
196 job_assignment: JobAssignmentMode::Strict,
197 unix_containment: UnixContainment::ProcessGroup,
198 process_priority: ProcessPriority::Normal,
199 kill_on_parent_exit: false,
200 hold_suspended: false,
201 partial_spawn_cleanup_timeout: None,
202 inherited_handles: None,
203 #[cfg(unix)]
204 inherited_fds: Vec::new(),
205 inherit_all_descriptors: false,
206 #[cfg(windows)]
207 windows_spawn_config: None,
208 #[cfg(windows)]
209 windows_spawn_config_auto: false,
210 run_stdio: None,
211 env_cleared: false,
212 }
213 }
214 pub(crate) fn pipe_stdio(&mut self, stdin_piped: bool) -> &mut Self {
221 self.run_stdio = Some(RunStdio { stdin_piped });
222 self
223 }
224 pub fn command_mut(&mut self) -> &mut Command {
225 #[cfg(windows)]
226 if self.windows_spawn_config_auto {
227 self.windows_spawn_config = None;
229 }
230 &mut self.command
231 }
232 pub fn inherit_handles_only(
234 &mut self,
235 handles: impl IntoIterator<Item = RawHandleOrFd>,
236 ) -> &mut Self {
237 #[cfg(windows)]
238 let handles = handles.into_iter().map(|handle| handle as usize).collect();
239 #[cfg(unix)]
240 let handles = handles.into_iter().collect();
241 self.inherited_handles = Some(handles);
242 self
243 }
244 #[cfg(unix)]
255 pub fn inherit_fd(&mut self, fd: std::os::fd::RawFd) -> &mut Self {
256 self.inherited_fds.push(fd);
257 self
258 }
259 #[cfg(unix)]
261 pub fn inherit_fds(&mut self, fds: &[std::os::fd::RawFd]) -> &mut Self {
262 self.inherited_fds.extend_from_slice(fds);
263 self
264 }
265 pub fn inherit_all_descriptors(&mut self, enabled: bool) -> &mut Self {
274 self.inherit_all_descriptors = enabled;
275 self
276 }
277 #[cfg(windows)]
283 pub fn windows_spawn_config(&mut self, config: crate::WindowsSpawnConfig) -> &mut Self {
284 self.windows_spawn_config = Some(config);
285 self.windows_spawn_config_auto = false;
286 self
287 }
288 pub fn env_allowlist<I, S>(&mut self, keys: I) -> &mut Self
290 where
291 I: IntoIterator<Item = S>,
292 S: AsRef<OsStr>,
293 {
294 let kept: Vec<_> = keys
295 .into_iter()
296 .filter_map(|k| std::env::var_os(k.as_ref()).map(|v| (k.as_ref().to_os_string(), v)))
297 .collect();
298 self.command.env_clear();
299 self.env_cleared = true;
300 for (k, v) in kept {
301 self.command.env(k, v);
302 }
303 self
304 }
305 pub fn env_allowlist_with_defaults<I, S>(&mut self, extra: I) -> &mut Self
309 where
310 I: IntoIterator<Item = S>,
311 S: AsRef<OsStr>,
312 {
313 let keys: Vec<std::ffi::OsString> = DEFAULT_ENV_ALLOWLIST
314 .iter()
315 .map(std::ffi::OsString::from)
316 .chain(extra.into_iter().map(|k| k.as_ref().to_os_string()))
317 .collect();
318 self.env_allowlist(keys)
319 }
320 pub fn env_is_cleared(&self) -> bool {
325 self.env_cleared
326 }
327 pub fn env_strip<I, S>(&mut self, keys: I) -> &mut Self
328 where
329 I: IntoIterator<Item = S>,
330 S: AsRef<OsStr>,
331 {
332 for k in keys {
333 self.command.env_remove(k);
334 }
335 self
336 }
337 pub fn windows_hide(&mut self) -> &mut Self {
339 self.windows_hidden = true;
340 self
341 }
342 pub fn windows_visible(&mut self) -> &mut Self {
347 self.windows_hidden = false;
348 self
349 }
350 pub fn windows_allow_breakaway(&mut self, allow: bool) -> &mut Self {
357 self.windows_breakaway = if allow {
358 BreakawayMode::Explicit
359 } else {
360 BreakawayMode::None
361 };
362 self
363 }
364 pub fn windows_allow_silent_breakaway(&mut self, allow: bool) -> &mut Self {
369 self.windows_breakaway = if allow {
370 BreakawayMode::Silent
371 } else {
372 BreakawayMode::None
373 };
374 self
375 }
376 #[cfg(windows)]
378 pub fn windows_job(
379 &mut self,
380 job: std::os::windows::io::BorrowedHandle<'_>,
381 ) -> io::Result<&mut Self> {
382 self.caller_job = Some(job.try_clone_to_owned()?);
383 Ok(self)
384 }
385 pub fn job_assignment_mode(&mut self, mode: JobAssignmentMode) -> &mut Self {
387 self.job_assignment = mode;
388 self
389 }
390 pub fn unix_containment(&mut self, mode: UnixContainment) -> &mut Self {
392 self.unix_containment = mode;
393 self
394 }
395 pub fn process_priority(&mut self, priority: ProcessPriority) -> &mut Self {
399 self.process_priority = priority;
400 self
401 }
402 pub fn kill_on_parent_exit(&mut self, enabled: bool) -> &mut Self {
424 self.kill_on_parent_exit = enabled;
425 self
426 }
427 pub fn partial_spawn_cleanup_timeout(&mut self, timeout: Duration) -> &mut Self {
429 self.partial_spawn_cleanup_timeout = Some(timeout);
430 self
431 }
432 pub(crate) fn bound_runner_cleanup(&mut self, timeout: Duration) {
433 self.partial_spawn_cleanup_timeout = Some(
434 self.partial_spawn_cleanup_timeout
435 .map_or(timeout, |current| current.min(timeout)),
436 );
437 }
438 pub fn spawn(self) -> io::Result<OwnedChild> {
439 self.spawn_inner(false, false, None)
440 }
441 pub fn spawn_suspended(mut self) -> io::Result<OwnedChild> {
451 self.hold_suspended = true;
452 self.spawn_inner(false, false, None)
453 }
454 pub fn spawn_uncontained_detached(self) -> io::Result<OwnedChild> {
469 self.spawn_inner_with(true, false, None, true)
470 }
471 pub fn spawn_uncontained_detached_strict(self) -> io::Result<OwnedChild> {
477 match self.spawn_inner_with(true, false, None, false) {
478 Err(error) if cfg!(windows) && is_denied_breakaway(&error) => Err(io::Error::new(
479 io::ErrorKind::PermissionDenied,
480 BreakawayDenied,
481 )),
482 other => other,
483 }
484 }
485 fn spawn_inner(
486 self,
487 detached: bool,
488 fail_after_spawn: bool,
489 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
490 ) -> io::Result<OwnedChild> {
491 self.spawn_inner_with(detached, fail_after_spawn, captured_pid, false)
492 }
493 fn spawn_inner_with(
494 mut self,
495 detached: bool,
496 fail_after_spawn: bool,
497 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
498 in_job_fallback: bool,
499 ) -> io::Result<OwnedChild> {
500 if detached && self.caller_job.is_some() {
501 return Err(io::Error::new(
502 io::ErrorKind::InvalidInput,
503 "detached launch cannot join a caller job",
504 ));
505 }
506 if detached && self.kill_on_parent_exit {
507 return Err(io::Error::new(
508 io::ErrorKind::InvalidInput,
509 "kill_on_parent_exit cannot combine with a detached launch",
510 ));
511 }
512 #[cfg(unix)]
513 let lists_fds = !self.inherited_fds.is_empty();
514 #[cfg(not(unix))]
515 let lists_fds = false;
516 if self.inherit_all_descriptors && (self.inherited_handles.is_some() || lists_fds) {
517 return Err(io::Error::new(
518 io::ErrorKind::InvalidInput,
519 "inherit_all_descriptors conflicts with inherit_handles_only and inherit_fd(s)",
520 ));
521 }
522 #[cfg(windows)]
527 if !self.inherit_all_descriptors
528 && self.inherited_handles.is_none()
529 && self.windows_spawn_config.is_some()
530 {
531 self.inherited_handles = Some(Vec::new());
532 }
533 if let Some(RunStdio { stdin_piped }) = self.run_stdio {
534 self.command
537 .stdin(if stdin_piped {
538 Stdio::piped()
539 } else {
540 Stdio::null()
541 })
542 .stdout(Stdio::piped())
543 .stderr(Stdio::piped());
544 #[cfg(windows)]
545 if let Some(config) = self.windows_spawn_config.as_mut() {
546 config.stdin = if stdin_piped {
547 crate::WindowsStdio::Piped
548 } else {
549 crate::WindowsStdio::Null
550 };
551 config.stdout = crate::WindowsStdio::Piped;
552 config.stderr = crate::WindowsStdio::Piped;
553 }
554 }
555 #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
556 if self.kill_on_parent_exit {
557 return Err(io::Error::new(
558 io::ErrorKind::Unsupported,
559 "kill_on_parent_exit is supported on Linux and macOS only",
560 ));
561 }
562 #[cfg(unix)]
563 {
564 use std::os::unix::process::CommandExt;
565 let session = detached || self.unix_containment == UnixContainment::Session;
566 let nice_floor = self.process_priority.nice_floor();
567 #[cfg(target_os = "macos")]
568 let utility_qos = self.process_priority == ProcessPriority::Efficient;
569 let inheritance = if self.inherit_all_descriptors {
572 None
573 } else {
574 let mut allowed = self.inherited_handles.take().unwrap_or_default();
575 allowed.extend(self.inherited_fds.iter().copied());
576 Some(crate::process_unix::Inheritance::new(allowed)?)
577 };
578 #[cfg(target_os = "linux")]
580 let supervisor = self
581 .kill_on_parent_exit
582 .then(|| unsafe { libc::getpid() });
584 unsafe {
586 self.command.pre_exec(move || {
587 let result = if session {
588 libc::setsid()
589 } else {
590 libc::setpgid(0, 0)
591 };
592 if result < 0 {
593 return Err(io::Error::last_os_error());
594 }
595 if let Some(floor) = nice_floor {
596 crate::process_unix::raise_nice_to(floor)?;
597 }
598 #[cfg(target_os = "macos")]
599 if utility_qos {
600 crate::process_unix::set_utility_qos();
601 }
602 if let Some(inheritance) = &inheritance {
603 inheritance.apply()?;
604 }
605 #[cfg(target_os = "linux")]
606 if let Some(supervisor) = supervisor {
607 crate::process_unix::die_with_parent(supervisor)?;
608 }
609 Ok(())
610 });
611 }
612 }
613 #[cfg(windows)]
614 crate::process_windows::configure(
615 &mut self.command,
616 detached,
617 self.windows_hidden,
618 self.process_priority,
619 );
620 #[cfg(unix)]
621 let _ = (
622 self.windows_hidden,
623 self.windows_breakaway,
624 self.job_assignment,
625 );
626 #[cfg(windows)]
627 let _ = (self.unix_containment, self.kill_on_parent_exit);
628 let spawned_at = SystemTime::now();
629 let hold_suspended = self.hold_suspended && !detached;
630 #[cfg(unix)]
631 let _ = hold_suspended;
632 #[cfg(unix)]
633 let child = {
634 let mut child = crate::unix_child::UnixChild::new(self.command.spawn()?);
635 if let Err(error) = child.require_identity() {
638 let reaped = child.cleanup_unidentified(self.partial_spawn_cleanup_timeout);
639 return Err(spawn_cleanup_failure(error, reaped));
640 }
641 #[cfg(target_os = "macos")]
644 if self.kill_on_parent_exit {
645 if let Err(error) = crate::process_unix::arm_parent_death_watch(child.id()) {
646 let _ = child.signal(SignalTarget::Group, libc::SIGKILL);
647 let reaped = cleanup_direct(
648 Arc::new(Mutex::new(child)),
649 self.partial_spawn_cleanup_timeout,
650 );
651 return Err(spawn_cleanup_failure(error, reaped));
652 }
653 }
654 child
655 };
656 #[cfg(windows)]
657 let (child, stayed_in_parent_job) = match self.inherited_handles.as_deref() {
658 None => {
659 let _spawning = crate::spawn_windows::spawn_guard();
662 match self.command.spawn() {
663 Ok(child) => (PlatformChild::from(child), false),
664 Err(error)
666 if detached
667 && in_job_fallback
668 && crate::process_windows::is_breakaway_denied(&error) =>
669 {
670 crate::process_windows::configure_in_job_detached(
671 &mut self.command,
672 self.windows_hidden,
673 self.process_priority,
674 );
675 (PlatformChild::from(self.command.spawn()?), true)
676 }
677 Err(error) => return Err(error),
678 }
679 }
680 Some(handles) => {
681 let config = self.windows_spawn_config.take().ok_or_else(|| {
682 io::Error::new(io::ErrorKind::InvalidInput,
683 "allowlisted Windows spawn requires windows_spawn_config for opaque Command settings")
684 })?;
685 crate::spawn_windows::spawn(
686 &self.command,
687 config,
688 handles,
689 detached,
690 self.windows_hidden,
691 self.process_priority,
692 self.env_cleared,
693 self.partial_spawn_cleanup_timeout,
694 in_job_fallback,
695 )?
696 }
697 };
698 #[cfg(not(windows))]
699 let _ = in_job_fallback;
700 #[cfg(not(windows))]
701 let stayed_in_parent_job = false;
702 let pid = child.id();
703 if let Some(captured) = captured_pid {
704 captured.store(pid, std::sync::atomic::Ordering::SeqCst);
705 }
706 let child = Arc::new(Mutex::new(child));
707 let handle = match ProcessHandle::new(&child, spawned_at) {
708 Ok(handle) => handle,
709 Err(error) => {
710 let reaped = cleanup_direct(child, self.partial_spawn_cleanup_timeout);
711 return Err(spawn_cleanup_failure(error, reaped));
712 }
713 };
714 let mut partial = PartialChild {
715 child: Some(OwnedChild {
716 child,
717 handle,
718 pid,
719 detached,
720 terminate_on_drop: !detached,
721 drop_reap_timeout: DEFAULT_DROP_REAP_TIMEOUT,
722 tree_terminated: false,
723 assignment_failures: Vec::new(),
724 suspended: std::sync::atomic::AtomicBool::new(cfg!(windows) && hold_suspended),
725 stayed_in_parent_job,
726 #[cfg(windows)]
727 job: None,
728 }),
729 timeout: self.partial_spawn_cleanup_timeout,
730 };
731 #[cfg(windows)]
732 if !detached {
733 let owned = partial.child.as_mut().expect("partial child is present");
734 let assignment = crate::process_windows::assign_jobs(
735 &owned.handle,
736 self.caller_job.take(),
737 self.job_assignment,
738 self.windows_breakaway,
739 &mut owned.job,
740 &mut owned.assignment_failures,
741 );
742 if let Err(error) = assignment {
743 return Err(partial.fail(error));
744 }
745 }
746 #[cfg(windows)]
747 {
748 let owned = partial.child.as_ref().expect("partial child is present");
749 if self.process_priority == ProcessPriority::Efficient {
750 let _ = crate::process_windows::apply_efficiency(&*owned.child());
752 }
753 if !hold_suspended {
754 let resumed = owned.child().resume(detached);
755 if let Err(error) = resumed {
756 return Err(partial.fail(error));
757 }
758 }
759 }
760 if fail_after_spawn {
761 return Err(partial.fail(io::Error::other("injected post-spawn wrapping failure")));
762 }
763 let owned = partial.child.take().expect("partial child is present");
764 if !detached {
765 #[cfg(unix)]
767 crate::registry::register(&owned.child, owned.pid);
768 #[cfg(windows)]
769 crate::registry::register(
770 &owned.child,
771 &owned.handle,
772 owned
773 .job
774 .as_ref()
775 .map_or_else(std::sync::Weak::new, std::sync::Arc::downgrade),
776 owned.pid,
777 );
778 }
779 Ok(owned)
780 }
781}
782
783#[derive(Clone, Copy, Debug, Eq, PartialEq)]
789pub struct BreakawayDenied;
790
791impl std::fmt::Display for BreakawayDenied {
792 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
793 f.write_str(
794 "detached launch denied: the caller's job object does not allow breakaway \
795 (ERROR_ACCESS_DENIED); no fallback was attempted",
796 )
797 }
798}
799
800impl std::error::Error for BreakawayDenied {}
801
802#[cfg(windows)]
803fn is_denied_breakaway(error: &io::Error) -> bool {
804 crate::process_windows::is_breakaway_denied(error)
805}
806#[cfg(not(windows))]
807fn is_denied_breakaway(_error: &io::Error) -> bool {
808 false
809}
810
811pub const DEFAULT_DROP_REAP_TIMEOUT: Duration = Duration::from_secs(2);
814
815pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
816 "PATH",
817 "HOME",
818 "USER",
819 "LOGNAME",
820 "LANG",
821 "LC_ALL",
822 "TMPDIR",
823 "TEMP",
824 "TMP",
825 "SystemRoot",
826 "SYSTEMROOT",
827 "USERPROFILE",
828 "APPDATA",
829 "LOCALAPPDATA",
830 "ProgramData",
831 "COMSPEC",
832 "PATHEXT",
833 "XDG_DATA_HOME",
834 "XDG_RUNTIME_DIR",
835];
836
837#[derive(Debug)]
838pub struct OwnedChild {
839 child: Arc<Mutex<PlatformChild>>,
840 handle: ProcessHandle,
841 pid: u32,
843 detached: bool,
844 terminate_on_drop: bool,
845 drop_reap_timeout: Duration,
846 tree_terminated: bool,
847 assignment_failures: Vec<JobAssignmentFailure>,
848 suspended: std::sync::atomic::AtomicBool,
850 stayed_in_parent_job: bool,
852 #[cfg(windows)]
855 job: Option<std::sync::Arc<crate::process_windows::KillOnCloseJob>>,
856}
857#[derive(Clone, Debug, Eq, PartialEq)]
859pub struct TreeTermination {
860 pub status: ExitStatus,
862 pub descendants: crate::SweepReport,
864}
865#[derive(Clone, Copy, Debug, Eq, PartialEq)]
866pub enum ReapOutcome {
867 Exited(ExitStatus),
868 TimedOut,
869}
870#[derive(Clone, Copy, Debug, Eq, PartialEq)]
871pub enum WaitOutcome {
872 Exited(ExitStatus),
873 Terminated(ExitStatus),
874}
875
876impl OwnedChild {
877 pub fn id(&self) -> u32 {
878 self.pid
879 }
880 pub fn stayed_in_parent_job(&self) -> bool {
885 self.stayed_in_parent_job
886 }
887 pub fn child(&self) -> MutexGuard<'_, PlatformChild> {
903 lock_child(&self.child)
904 }
905 pub fn resume(&self) -> io::Result<()> {
910 if self
911 .suspended
912 .swap(false, std::sync::atomic::Ordering::SeqCst)
913 {
914 #[cfg(windows)]
915 if let Err(error) = self.child().resume(self.detached) {
916 self.suspended
917 .store(true, std::sync::atomic::Ordering::SeqCst);
918 return Err(error);
919 }
920 }
921 Ok(())
922 }
923 pub fn is_suspended(&self) -> bool {
925 self.suspended.load(std::sync::atomic::Ordering::SeqCst)
926 }
927 pub fn while_unreaped<R>(&self, act: impl FnOnce(u32) -> R) -> io::Result<Option<R>> {
936 #[cfg(unix)]
937 {
938 let mut child = lock_child(&self.child);
939 if child.leader_state()? == LeaderState::Reaped {
940 return Ok(None);
941 }
942 Ok(Some(act(self.pid)))
943 }
944 #[cfg(windows)]
945 Ok(Some(act(self.pid)))
946 }
947 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
948 self.child().stdin.take()
949 }
950 pub fn close_stdin(&mut self) {
954 drop(self.child().stdin.take());
955 }
956 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
957 self.child().stdout.take()
958 }
959 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
960 self.child().stderr.take()
961 }
962 pub fn job_assignment_failures(&self) -> &[JobAssignmentFailure] {
963 &self.assignment_failures
964 }
965 pub fn process_handle(&self) -> io::Result<ProcessHandle> {
969 self.handle.try_clone()
970 }
971 pub fn creation_time(&self) -> Option<SystemTime> {
972 self.handle.creation_time()
973 }
974 #[cfg(windows)]
975 pub fn creation_time_ticks(&self) -> Option<u64> {
976 self.handle.creation_time_ticks()
977 }
978 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
979 self.handle.try_wait()
981 }
982 pub fn wait(&mut self) -> io::Result<ExitStatus> {
985 drop(self.child().stdin.take());
986 self.handle.wait()
987 }
988 pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
989 self.handle.wait_timeout(timeout)
990 }
991 pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
1001 self.wait_or_kill_inner(grace, true)
1002 }
1003 pub fn wait_then_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
1009 self.wait_or_kill_inner(grace, false)
1010 }
1011 fn wait_or_kill_inner(&mut self, grace: Duration, term_first: bool) -> io::Result<WaitOutcome> {
1012 #[cfg(not(unix))]
1013 let _ = term_first;
1014 #[cfg(unix)]
1015 {
1016 if term_first && !self.tree_terminated {
1017 self.signal(libc::SIGTERM)?;
1018 }
1019 if self.handle.wait_exited_timeout(grace)? {
1021 self.force_kill(1)?;
1026 return self.wait().map(WaitOutcome::Exited);
1027 }
1028 self.terminate_tree().map(WaitOutcome::Terminated)
1029 }
1030 #[cfg(windows)]
1031 {
1032 if let Some(status) = self.wait_timeout(grace)? {
1033 self.force_kill(1)?;
1037 return Ok(WaitOutcome::Exited(status));
1038 }
1039 self.terminate_tree().map(WaitOutcome::Terminated)
1040 }
1041 }
1042 pub(crate) fn wait_exit_then_sweep(
1048 &mut self,
1049 timeout: Duration,
1050 ) -> io::Result<Option<ExitStatus>> {
1051 if self.handle.wait_exited_timeout(timeout)? {
1052 self.force_kill(1)?;
1053 return self.wait().map(Some);
1054 }
1055 Ok(None)
1056 }
1057 pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
1062 self.force_kill(1)?;
1063 self.wait()
1064 }
1065 pub fn terminate_tree_with_options(
1070 &mut self,
1071 options: TerminationOptions,
1072 ) -> io::Result<ExitStatus> {
1073 self.start_termination(options)?;
1074 self.wait()
1075 }
1076 pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
1077 self.terminate_tree_bounded_with_options(timeout, TerminationOptions::default())
1078 }
1079 pub fn terminate_tree_bounded_with_options(
1081 &mut self,
1082 timeout: Duration,
1083 mut options: TerminationOptions,
1084 ) -> io::Result<ReapOutcome> {
1085 let started = Instant::now();
1086 let deadline = started.checked_add(timeout);
1090 let left = || {
1091 deadline.map_or(Duration::MAX, |d| {
1092 d.saturating_duration_since(Instant::now())
1093 })
1094 };
1095 options.grace = options.grace.min(timeout);
1096 self.start_termination_within(options, deadline)?;
1097 #[cfg(test)]
1098 if cleanup_test_fault::refuse(left()) {
1099 return Ok(ReapOutcome::TimedOut);
1100 }
1101 let status = match self.wait_timeout(left())? {
1102 Some(status) => status,
1103 None => return Ok(ReapOutcome::TimedOut),
1104 };
1105 #[cfg(windows)]
1111 if let Some(job) = self.job.as_ref() {
1112 #[cfg(test)]
1113 cleanup_test_fault::handoff_delay();
1114 if !job.wait_empty_until(deadline)? {
1115 return Ok(ReapOutcome::TimedOut);
1116 }
1117 }
1118 Ok(ReapOutcome::Exited(status))
1119 }
1120 pub fn terminate_tree_deep(
1144 &mut self,
1145 options: TerminationOptions,
1146 ) -> io::Result<TreeTermination> {
1147 let mut found = self.descendant_identities()?;
1148 found.reverse(); #[cfg(unix)]
1150 if !self.tree_terminated {
1151 signal_shared(&self.child, SignalTarget::Group, libc::SIGTERM)?;
1152 await_tree_shared(&self.child, &self.handle, options.grace, true)?;
1153 signal_shared(&self.child, SignalTarget::Group, libc::SIGKILL)?;
1154 self.tree_terminated = true;
1155 }
1156 #[cfg(windows)]
1157 self.force_kill(options.windows_exit_code)?;
1158 let status = self.wait()?;
1159 let descendants = crate::sweep(&found, options.grace)?;
1160 Ok(TreeTermination {
1161 status,
1162 descendants,
1163 })
1164 }
1165 fn start_termination(&mut self, options: TerminationOptions) -> io::Result<()> {
1166 self.start_termination_within(options, None)
1167 }
1168 fn start_termination_within(
1171 &mut self,
1172 options: TerminationOptions,
1173 deadline: Option<Instant>,
1174 ) -> io::Result<()> {
1175 #[cfg(unix)]
1176 if !self.tree_terminated {
1177 self.signal(libc::SIGTERM)?;
1178 self.await_tree(options.grace)?;
1182 }
1183 self.force_kill_within(options.windows_exit_code, deadline)
1184 }
1185 #[cfg(unix)]
1192 fn await_tree(&self, grace: Duration) -> io::Result<()> {
1193 await_tree_shared(&self.child, &self.handle, grace, !self.detached)
1195 }
1196 #[cfg(unix)]
1200 fn signal(&self, signal: i32) -> io::Result<()> {
1201 let target = if self.detached {
1204 SignalTarget::Leader
1205 } else {
1206 SignalTarget::Group
1207 };
1208 signal_shared(&self.child, target, signal).map(|_| ())
1209 }
1210 fn force_kill(&mut self, code: u32) -> io::Result<()> {
1211 self.force_kill_within(code, None)
1212 }
1213 fn force_kill_within(&mut self, code: u32, deadline: Option<Instant>) -> io::Result<()> {
1217 if self.tree_terminated {
1218 return Ok(());
1219 }
1220 #[cfg(unix)]
1221 {
1222 let _ = (code, deadline);
1223 self.signal(libc::SIGKILL)?;
1224 }
1225 #[cfg(windows)]
1226 crate::process_windows::terminate(&self.handle, self.job.as_deref(), code, deadline)?;
1227 self.tree_terminated = true;
1228 Ok(())
1229 }
1230}
1231impl OwnedChild {
1232 pub fn set_drop_reap_timeout(&mut self, timeout: Duration) -> &mut Self {
1239 self.drop_reap_timeout = timeout;
1240 self
1241 }
1242 pub fn drop_reap_timeout(&self) -> Duration {
1244 self.drop_reap_timeout
1245 }
1246 fn reap_in_background(&self) {
1248 #[cfg(unix)]
1249 if let Ok(reader) = self.handle.try_clone() {
1250 let _ = thread::Builder::new()
1251 .name("rightkit-drop-reap".into())
1252 .spawn(move || {
1253 let _ = reader.wait();
1254 });
1255 }
1256 }
1257 pub fn tree_members(&self) -> io::Result<Vec<u32>> {
1268 let leader = self.pid;
1269 let mut pids: Vec<u32> = Vec::new();
1270 #[cfg(windows)]
1271 if let Some(job) = self.job.as_ref() {
1272 pids.extend(job.process_ids()?);
1273 }
1274 #[cfg(unix)]
1275 if !self.detached {
1276 let group = match lock_child(&self.child).group_other_members()? {
1277 crate::unix_child::Members::Pids(group) => Some(group),
1278 crate::unix_child::Members::Unknown => None,
1279 };
1280 pids.extend(listed_or_error(group)?);
1281 }
1282 for identity in self.descendant_identities().unwrap_or_default() {
1283 pids.push(identity.pid());
1284 }
1285 pids.retain(|p| *p != leader);
1286 pids.sort_unstable();
1287 pids.dedup();
1288 Ok(pids)
1289 }
1290 pub fn descendant_identities(&self) -> io::Result<Vec<crate::AdoptedIdentity>> {
1294 let root = self.while_unreaped(|pid| match NonZeroU32::new(pid) {
1295 Some(pid) => crate::AdoptedIdentity::capture(pid),
1296 None => Ok(None),
1297 })?;
1298 match root {
1299 Some(Ok(Some(root))) => root.descendants(),
1300 Some(Err(error)) => Err(error),
1301 _ => Ok(Vec::new()),
1302 }
1303 }
1304}
1305#[cfg_attr(not(unix), allow(dead_code))]
1309fn listed_or_error(listing: Option<Vec<u32>>) -> io::Result<Vec<u32>> {
1310 listing.ok_or_else(|| {
1311 io::Error::other("the process group could not be fully listed (too large, or the host refused the enumeration)")
1312 })
1313}
1314impl Drop for OwnedChild {
1315 fn drop(&mut self) {
1316 crate::registry::forget(&self.child);
1318 if self.terminate_on_drop {
1319 if self.force_kill(1).is_ok() {
1323 drop(self.child().stdin.take());
1324 if !matches!(
1325 self.handle.wait_timeout(self.drop_reap_timeout),
1326 Ok(Some(_))
1327 ) {
1328 self.reap_in_background();
1329 }
1330 }
1331 } else if self.detached {
1332 #[cfg(unix)]
1333 {
1334 {
1338 let mut child = self.child();
1339 drop(child.stdin.take());
1340 drop(child.stdout.take());
1341 drop(child.stderr.take());
1342 }
1343 if let Ok(reader) = self.handle.try_clone() {
1344 let _ = thread::Builder::new()
1345 .name("rightkit-detached-reap".into())
1346 .spawn(move || {
1347 let _ = reader.wait();
1348 });
1349 }
1350 }
1351 }
1352 }
1353}
1354
1355pub(crate) fn spawn_setup_failure(error: io::Error) -> io::Error {
1358 crate::run::failure(
1359 crate::RunnerPhase::Setup,
1360 crate::LeaderTermination::Unconfirmed,
1361 error,
1362 )
1363}
1364
1365pub(crate) fn spawn_cleanup_failure(error: io::Error, reaped: bool) -> io::Error {
1366 if reaped {
1367 spawn_setup_failure(error)
1368 } else {
1369 crate::run::failure(
1370 crate::RunnerPhase::Cleanup,
1371 crate::LeaderTermination::Unconfirmed,
1372 error,
1373 )
1374 }
1375}
1376
1377struct PartialChild {
1378 child: Option<OwnedChild>,
1379 timeout: Option<Duration>,
1380}
1381impl PartialChild {
1382 fn cleanup(&mut self) -> bool {
1383 if let Some(mut child) = self.child.take() {
1384 child.terminate_on_drop = false;
1385 let _ = child.force_kill(1);
1386 cleanup_direct(Arc::clone(&child.child), self.timeout)
1387 } else {
1388 true
1389 }
1390 }
1391
1392 fn fail(mut self, error: io::Error) -> io::Error {
1393 let reaped = self.cleanup();
1394 spawn_cleanup_failure(error, reaped)
1395 }
1396}
1397impl Drop for PartialChild {
1398 fn drop(&mut self) {
1399 self.cleanup();
1400 }
1401}
1402#[cfg(unix)]
1405const MID_EXIT_RETRY: Duration = Duration::from_millis(300);
1406
1407#[cfg(unix)]
1411pub(crate) fn signal_shared(
1412 shared: &Mutex<crate::unix_child::UnixChild>,
1413 target: SignalTarget,
1414 signal: i32,
1415) -> io::Result<SignalOutcome> {
1416 let started = Instant::now();
1417 loop {
1418 let (outcome, watch) = {
1419 let mut child = lock_child(shared);
1420 (child.signal(target, signal)?, child.exit_watch())
1421 };
1422 if outcome != SignalOutcome::MidExit {
1423 return Ok(outcome);
1424 }
1425 let remaining = MID_EXIT_RETRY.saturating_sub(started.elapsed());
1428 if remaining.is_zero() {
1429 return Err(io::Error::from_raw_os_error(libc::EPERM));
1430 }
1431 watch.pause(remaining);
1432 }
1433}
1434
1435#[cfg(unix)]
1437pub(crate) fn await_tree_shared(
1438 shared: &Mutex<crate::unix_child::UnixChild>,
1439 handle: &ProcessHandle,
1440 grace: Duration,
1441 whole_group: bool,
1442) -> io::Result<()> {
1443 use crate::{exit_watch::POLL_INTERVAL, unix_child::Members};
1444 let Some(deadline) = Instant::now().checked_add(grace) else {
1445 return Ok(());
1446 };
1447 let left = |deadline: Instant| deadline.saturating_duration_since(Instant::now());
1448 if !handle.wait_exited_timeout(left(deadline))? {
1449 return Ok(()); }
1451 if !whole_group {
1452 return Ok(());
1453 }
1454 loop {
1455 match lock_child(shared).group_other_members()? {
1456 Members::Pids(pids) if pids.is_empty() => return Ok(()),
1457 Members::Pids(_) => {}
1458 Members::Unknown => return wait_out(left(deadline)),
1459 }
1460 let remaining = left(deadline);
1461 if remaining.is_zero() {
1462 return Ok(()); }
1464 thread::sleep(POLL_INTERVAL.min(remaining));
1465 }
1466}
1467
1468#[cfg(unix)]
1471fn wait_out(remaining: Duration) -> io::Result<()> {
1472 thread::sleep(remaining);
1473 Ok(())
1474}
1475pub(crate) fn cleanup_direct(child: Arc<Mutex<PlatformChild>>, timeout: Option<Duration>) -> bool {
1482 let _ = lock_child(&child).kill();
1483 let Some(timeout) = timeout else {
1484 return lock_child(&child).wait().is_ok();
1486 };
1487 #[cfg(test)]
1488 let refused = cleanup_test_fault::refuse(timeout);
1489 #[cfg(not(test))]
1490 let refused = false;
1491 #[cfg(unix)]
1492 {
1493 let watch = lock_child(&child).exit_watch();
1496 let deadline = Instant::now().checked_add(timeout);
1497 if !refused
1498 && matches!(
1499 watch.wait_for(deadline, || lock_child(&child).try_wait()),
1500 Ok(Some(_))
1501 )
1502 {
1503 return true;
1504 }
1505 }
1506 #[cfg(windows)]
1507 if !refused {
1508 let started = Instant::now();
1509 loop {
1510 match lock_child(&child).try_wait() {
1511 Ok(Some(_)) => return true,
1512 Err(_) => break,
1513 Ok(None) => {}
1514 }
1515 if started.elapsed() >= timeout {
1516 break;
1517 }
1518 thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
1519 }
1520 }
1521 let _ = thread::Builder::new()
1525 .name("rightkit-partial-reap".into())
1526 .spawn(move || {
1527 let _ = lock_child(&child).wait();
1528 });
1529 false
1530}
1531
1532#[cfg(test)]
1533pub(crate) mod cleanup_test_fault {
1534 use std::{cell::Cell, time::Duration};
1535 thread_local! { static REFUSE_REAP: Cell<bool> = const { Cell::new(false) }; }
1536 thread_local! { static JOB_EMPTY_AT: Cell<Option<std::time::Instant>> = const { Cell::new(None) }; }
1537 thread_local! { static TERMINATION_REFUSED: Cell<bool> = const { Cell::new(false) }; }
1538 thread_local! { static HANDOFF_DELAY: Cell<Duration> = const { Cell::new(Duration::ZERO) }; }
1539 #[allow(dead_code)]
1542 pub(crate) fn arm_handoff_delay(delay: Duration) {
1543 HANDOFF_DELAY.with(|armed| armed.set(delay));
1544 }
1545 #[allow(dead_code)]
1546 pub(crate) fn handoff_delay() {
1547 let delay = HANDOFF_DELAY.with(|armed| armed.replace(Duration::ZERO));
1548 if !delay.is_zero() {
1549 std::thread::sleep(delay);
1550 }
1551 }
1552 #[allow(dead_code)]
1555 pub(crate) fn arm_job_empty_at(at: std::time::Instant) {
1556 JOB_EMPTY_AT.with(|armed| armed.set(Some(at)));
1557 }
1558 #[allow(dead_code)]
1559 pub(crate) fn disarm_job_empty_at() {
1560 JOB_EMPTY_AT.with(|armed| armed.set(None));
1561 }
1562 #[allow(dead_code)]
1564 pub(crate) fn job_count() -> Option<u32> {
1565 JOB_EMPTY_AT
1566 .with(|armed| armed.get())
1567 .map(|at| u32::from(std::time::Instant::now() < at))
1568 }
1569 #[allow(dead_code)]
1573 pub(crate) fn arm_termination_refused() {
1574 TERMINATION_REFUSED.with(|armed| armed.set(true));
1575 }
1576 #[allow(dead_code)]
1577 pub(crate) fn termination_refused() -> bool {
1578 TERMINATION_REFUSED.with(|armed| armed.replace(false))
1579 }
1580 pub(crate) fn arm() {
1581 REFUSE_REAP.with(|armed| armed.set(true));
1582 }
1583 pub(crate) fn refuse(timeout: Duration) -> bool {
1584 if REFUSE_REAP.with(|armed| armed.replace(false)) {
1585 std::thread::sleep(timeout);
1588 true
1589 } else {
1590 false
1591 }
1592 }
1593}
1594
1595#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1596pub struct AdoptedProcess {
1597 pid: NonZeroU32,
1598}
1599impl AdoptedProcess {
1600 pub fn new(pid: NonZeroU32) -> Self {
1601 Self { pid }
1602 }
1603 pub fn id(&self) -> u32 {
1604 self.pid.get()
1605 }
1606 pub fn is_running(&self) -> io::Result<bool> {
1610 process_is_running(self.pid)
1611 }
1612 pub fn identity(&self) -> io::Result<Option<crate::AdoptedIdentity>> {
1617 crate::AdoptedIdentity::capture(self.pid)
1618 }
1619}
1620#[cfg(test)]
1621fn spawn_for_test(
1622 command: Command,
1623 captured_pid: Arc<std::sync::atomic::AtomicU32>,
1624) -> io::Result<OwnedChild> {
1625 OwnedCommand::from_command(command).spawn_inner(false, true, Some(captured_pid))
1626}
1627
1628#[cfg(windows)]
1629fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1630 use windows::Win32::{
1631 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
1632 System::Threading::{
1633 OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
1634 PROCESS_SYNCHRONIZE,
1635 },
1636 };
1637
1638 let handle = unsafe {
1639 OpenProcess(
1640 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
1641 false,
1642 pid.get(),
1643 )
1644 };
1645 let handle = match handle {
1646 Ok(handle) => handle,
1647 Err(error) => {
1648 if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
1649 return Ok(false);
1650 }
1651 return Err(io::Error::other(error.to_string()));
1652 }
1653 };
1654 let wait = unsafe { WaitForSingleObject(handle, 0) };
1655 let close = unsafe { CloseHandle(handle) };
1656 close.map_err(|error| io::Error::other(error.to_string()))?;
1657 match wait.0 {
1658 0 => Ok(false),
1659 value if value == WAIT_TIMEOUT.0 => Ok(true),
1660 _ => Err(io::Error::last_os_error()),
1661 }
1662}
1663
1664#[cfg(unix)]
1665fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1666 let raw_pid = i32::try_from(pid.get()).map_err(io::Error::other)?;
1667 loop {
1668 if unsafe { libc::kill(raw_pid, 0) } == 0 {
1670 return Ok(!crate::foreign::process_is_zombie(pid.get()));
1673 }
1674 let error = io::Error::last_os_error();
1675 match error.raw_os_error() {
1676 Some(libc::EINTR) => continue,
1677 Some(libc::EPERM) => return Ok(!crate::foreign::process_is_zombie(pid.get())),
1678 Some(libc::ESRCH) => return Ok(false),
1679 _ => return Err(error),
1680 }
1681 }
1682}
1683
1684#[cfg(test)]
1685mod tests {
1686 #[test]
1687 fn option_defaults_preserve_existing_behavior() {
1688 let command = super::OwnedCommand::new("unused");
1689 assert_eq!(command.job_assignment, super::JobAssignmentMode::Strict);
1690 assert_eq!(
1691 command.unix_containment,
1692 super::UnixContainment::ProcessGroup
1693 );
1694 assert_eq!(command.partial_spawn_cleanup_timeout, None);
1695 assert_eq!(
1696 super::TerminationOptions::default().grace,
1697 std::time::Duration::ZERO
1698 );
1699 assert_eq!(super::TerminationOptions::default().windows_exit_code, 1);
1700 }
1701
1702 #[cfg(not(windows))]
1703 #[test]
1704 fn new_and_from_command_share_spawn_settings() {
1705 use super::OwnedCommand;
1706 use std::process::Command;
1707 let by_new = OwnedCommand::new("unused");
1709 let by_from = OwnedCommand::from_command(Command::new("unused"));
1710 assert_eq!(format!("{by_new:?}"), format!("{by_from:?}"));
1711 }
1712
1713 #[cfg(windows)]
1717 #[test]
1718 fn only_new_supplies_a_default_windows_spawn_config() {
1719 use super::OwnedCommand;
1720 use std::process::Command;
1721 let by_new = OwnedCommand::new("unused");
1722 let by_from = OwnedCommand::from_command(Command::new("unused"));
1723 assert!(by_new.windows_spawn_config.is_some() && by_new.windows_spawn_config_auto);
1724 assert!(by_from.windows_spawn_config.is_none() && !by_from.windows_spawn_config_auto);
1725 }
1726
1727 #[test]
1728 fn process_priority_defaults_to_normal_and_keeps_the_requested_value() {
1729 use super::{OwnedCommand, ProcessPriority};
1730 let mut command = OwnedCommand::new("unused");
1731 assert_eq!(command.process_priority, ProcessPriority::Normal);
1732 command.process_priority(ProcessPriority::BelowNormal);
1733 assert_eq!(command.process_priority, ProcessPriority::BelowNormal);
1734 command.process_priority(ProcessPriority::Idle);
1735 assert_eq!(command.process_priority, ProcessPriority::Idle);
1736 }
1737
1738 #[cfg(unix)]
1741 #[test]
1742 fn process_priority_maps_to_nice_floors() {
1743 use super::ProcessPriority;
1744 assert_eq!(ProcessPriority::Normal.nice_floor(), None);
1745 assert_eq!(ProcessPriority::BelowNormal.nice_floor(), Some(10));
1746 assert_eq!(ProcessPriority::Idle.nice_floor(), Some(19));
1747 assert_eq!(ProcessPriority::Efficient.nice_floor(), Some(10));
1748 }
1749
1750 #[cfg(windows)]
1752 #[test]
1753 fn below_normal_child_has_below_normal_priority_class() {
1754 use super::{OwnedCommand, ProcessPriority};
1755 use std::process::Stdio;
1756 use windows::Win32::{
1757 Foundation::CloseHandle,
1758 System::Threading::{
1759 GetPriorityClass, OpenProcess, BELOW_NORMAL_PRIORITY_CLASS,
1760 PROCESS_QUERY_LIMITED_INFORMATION,
1761 },
1762 };
1763 let mut command = OwnedCommand::new("ping");
1764 command
1765 .command_mut()
1766 .args(["-n", "6", "127.0.0.1"])
1767 .stdin(Stdio::null())
1768 .stdout(Stdio::null())
1769 .stderr(Stdio::null());
1770 command.process_priority(ProcessPriority::BelowNormal);
1771 let mut child = command.spawn().expect("spawn ping");
1772 let class = unsafe {
1774 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, child.id())
1775 .expect("open the child");
1776 let class = GetPriorityClass(handle);
1777 let _ = CloseHandle(handle);
1778 class
1779 };
1780 let _ = child.terminate_tree();
1781 assert_eq!(class, BELOW_NORMAL_PRIORITY_CLASS.0);
1782 }
1783
1784 #[cfg(unix)]
1786 #[test]
1787 fn lowered_priority_child_runs_at_or_above_the_nice_floor() {
1788 use super::{OwnedCommand, ProcessPriority};
1789 use std::{io::Read, process::Stdio};
1790 for (priority, floor) in [
1791 (ProcessPriority::BelowNormal, 10),
1792 (ProcessPriority::Idle, 19),
1793 ] {
1794 let mut command = OwnedCommand::new("/bin/sh");
1795 command
1796 .command_mut()
1797 .args(["-c", "ps -o ni= -p $$"])
1798 .stdin(Stdio::null())
1799 .stdout(Stdio::piped())
1800 .stderr(Stdio::null());
1801 command.process_priority(priority);
1802 let mut child = command.spawn().expect("spawn sh");
1803 let mut text = String::new();
1804 child
1805 .take_stdout()
1806 .expect("piped stdout")
1807 .read_to_string(&mut text)
1808 .expect("read ps output");
1809 let _ = child.wait();
1810 let nice: i32 = text.trim().parse().expect("ps printed a nice value");
1811 assert!(nice >= floor, "nice {nice} below floor {floor}");
1812 }
1813 }
1814
1815 #[test]
1816 fn breakaway_is_off_by_default_explicit_by_default_opt_in_and_silent_is_separate() {
1817 use super::BreakawayMode;
1818 let mut command = super::OwnedCommand::new("unused");
1819 assert_eq!(command.windows_breakaway, BreakawayMode::None);
1820 command.windows_allow_breakaway(true);
1821 assert_eq!(command.windows_breakaway, BreakawayMode::Explicit);
1822 command.windows_allow_silent_breakaway(true);
1823 assert_eq!(command.windows_breakaway, BreakawayMode::Silent);
1824 command.windows_allow_silent_breakaway(false);
1825 assert_eq!(command.windows_breakaway, BreakawayMode::None);
1826 }
1827
1828 #[test]
1829 fn kill_on_parent_exit_is_opt_in() {
1830 let mut command = super::OwnedCommand::new("unused");
1831 assert!(!command.kill_on_parent_exit);
1832 command.kill_on_parent_exit(true);
1833 assert!(command.kill_on_parent_exit);
1834 }
1835
1836 #[test]
1837 fn breakaway_denied_is_a_typed_recoverable_error() {
1838 let error =
1839 std::io::Error::new(std::io::ErrorKind::PermissionDenied, super::BreakawayDenied);
1840 assert!(error
1841 .get_ref()
1842 .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1843 .is_some());
1844 assert!(super::BreakawayDenied.to_string().contains("breakaway"));
1845 }
1846
1847 #[test]
1848 fn a_strict_detached_spawn_passes_other_failures_through_untyped() {
1849 let command = super::OwnedCommand::new("rightkit-no-such-program-for-strict-detach");
1850 let Err(error) = command.spawn_uncontained_detached_strict() else {
1851 panic!("a missing program must not spawn");
1852 };
1853 assert!(error
1854 .get_ref()
1855 .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1856 .is_none());
1857 }
1858
1859 #[test]
1860 fn a_strict_detached_spawn_still_rejects_kill_on_parent_exit() {
1861 let mut command = super::OwnedCommand::new("unused");
1862 command.kill_on_parent_exit(true);
1863 let Err(error) = command.spawn_uncontained_detached_strict() else {
1864 panic!("a detached spawn must not accept the parent-death tie");
1865 };
1866 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1867 }
1868
1869 #[test]
1870 fn a_detached_spawn_rejects_kill_on_parent_exit() {
1871 let mut command = super::OwnedCommand::new("unused");
1873 command.kill_on_parent_exit(true);
1874 let Err(error) = command.spawn_uncontained_detached() else {
1875 panic!("a detached spawn must not accept the parent-death tie");
1876 };
1877 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1878 }
1879
1880 #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
1883 #[test]
1884 fn a_host_without_a_parent_death_primitive_refuses_before_starting_anything() {
1885 let marker =
1886 std::env::temp_dir().join(format!("rightkit-process-untied-{}", std::process::id()));
1887 let _ = std::fs::remove_file(&marker);
1888 let mut command = super::OwnedCommand::new("/usr/bin/touch");
1889 command.command_mut().arg(&marker);
1890 command.kill_on_parent_exit(true);
1891 let Err(error) = command.spawn() else {
1892 panic!("kill_on_parent_exit must not run the child untied");
1893 };
1894 assert_eq!(error.kind(), std::io::ErrorKind::Unsupported);
1895 std::thread::sleep(std::time::Duration::from_millis(200));
1896 assert!(!marker.exists(), "the program ran");
1897 }
1898
1899 #[test]
1900 fn option_builders_keep_every_requested_value() {
1901 use super::{JobAssignmentMode, OwnedCommand, TerminationOptions, UnixContainment};
1902 use std::time::Duration;
1903 let mut command = OwnedCommand::new("unused");
1904 command
1905 .job_assignment_mode(JobAssignmentMode::BestEffort)
1906 .unix_containment(UnixContainment::Session)
1907 .partial_spawn_cleanup_timeout(Duration::from_secs(5));
1908 assert_eq!(command.job_assignment, JobAssignmentMode::BestEffort);
1909 assert_eq!(command.unix_containment, UnixContainment::Session);
1910 assert_eq!(
1911 command.partial_spawn_cleanup_timeout,
1912 Some(Duration::from_secs(5))
1913 );
1914 let options = TerminationOptions::default()
1915 .with_grace(Duration::from_millis(100))
1916 .with_windows_exit_code(1067);
1917 assert_eq!(options.grace, Duration::from_millis(100));
1918 assert_eq!(options.windows_exit_code, 1067);
1919 }
1920
1921 #[test]
1922 fn zero_bound_partial_spawn_failure_still_reaps_the_child() {
1923 use std::{
1924 num::NonZeroU32,
1925 process::Command,
1926 sync::{
1927 atomic::{AtomicU32, Ordering},
1928 Arc,
1929 },
1930 thread,
1931 time::{Duration, Instant},
1932 };
1933 let command = if cfg!(windows) {
1934 let mut command = Command::new("powershell.exe");
1935 command.args([
1936 "-NoLogo",
1937 "-NoProfile",
1938 "-NonInteractive",
1939 "-Command",
1940 "Start-Sleep -Seconds 30",
1941 ]);
1942 command
1943 } else {
1944 let mut command = Command::new("/bin/sh");
1945 command.args(["-c", "exec sleep 30"]);
1946 command
1947 };
1948 let mut command = super::OwnedCommand::from_command(command);
1949 command.partial_spawn_cleanup_timeout(Duration::ZERO);
1950 let captured = Arc::new(AtomicU32::new(0));
1951 let started = Instant::now();
1952 super::cleanup_test_fault::arm();
1953 let error = command
1954 .spawn_inner(false, true, Some(Arc::clone(&captured)))
1955 .err()
1956 .unwrap();
1957 let detail = crate::RunnerFailure::from_io(&error).unwrap();
1958 assert_eq!(detail.phase(), crate::RunnerPhase::Cleanup);
1959 assert_eq!(detail.termination(), crate::LeaderTermination::Unconfirmed);
1960 assert!(started.elapsed() < Duration::from_secs(3));
1961 let pid = captured.load(Ordering::SeqCst);
1962 assert_ne!(pid, 0);
1963 let process = super::AdoptedProcess::new(NonZeroU32::new(pid).unwrap());
1964 let started = Instant::now();
1965 while process.is_running().unwrap() {
1966 assert!(started.elapsed() < Duration::from_secs(5));
1967 thread::sleep(Duration::from_millis(5));
1968 }
1969 }
1970
1971 #[test]
1972 fn owned_commands_hide_their_console_window_by_default() {
1973 assert!(super::OwnedCommand::new("x").windows_hidden);
1974 assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
1975 }
1976
1977 #[cfg(windows)]
1979 fn host_job() -> std::os::windows::io::OwnedHandle {
1980 use std::os::windows::io::FromRawHandle;
1981 use windows::Win32::System::JobObjects::{
1982 CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject,
1983 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
1984 };
1985 let job = unsafe { CreateJobObjectW(None, None) }.unwrap();
1986 let job_handle = unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(job.0 as _) };
1987 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
1988 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
1989 unsafe {
1990 SetInformationJobObject(
1991 job,
1992 JobObjectExtendedLimitInformation,
1993 &info as *const _ as *const _,
1994 std::mem::size_of_val(&info) as u32,
1995 )
1996 }
1997 .unwrap();
1998 job_handle
1999 }
2000
2001 #[cfg(windows)]
2002 fn in_job(pid: u32, job: &std::os::windows::io::OwnedHandle) -> bool {
2003 use std::os::windows::io::AsRawHandle;
2004 #[link(name = "kernel32")]
2005 unsafe extern "system" {
2006 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> *mut std::ffi::c_void;
2007 fn IsProcessInJob(
2008 process: *mut std::ffi::c_void,
2009 job: *mut std::ffi::c_void,
2010 result: *mut i32,
2011 ) -> i32;
2012 fn CloseHandle(handle: *mut std::ffi::c_void) -> i32;
2013 }
2014 unsafe {
2015 let process = OpenProcess(0x1000, 0, pid); assert!(!process.is_null());
2017 let mut inside = 0;
2018 assert_ne!(
2019 IsProcessInJob(process, job.as_raw_handle() as _, &mut inside),
2020 0
2021 );
2022 CloseHandle(process);
2023 inside != 0
2024 }
2025 }
2026
2027 #[cfg(windows)]
2028 #[test]
2029 fn children_join_the_callers_job_and_die_when_the_caller_closes_it() {
2030 use std::os::windows::io::AsHandle;
2031 use std::time::{Duration, Instant};
2032 let job = host_job();
2033 let sleeper = || {
2034 let mut command = super::OwnedCommand::new("powershell.exe");
2035 command.command_mut().args([
2036 "-NoLogo",
2037 "-NoProfile",
2038 "-NonInteractive",
2039 "-Command",
2040 "Start-Sleep -Seconds 60",
2041 ]);
2042 command.windows_job(job.as_handle()).unwrap();
2043 command.spawn().unwrap()
2044 };
2045 let (first, second) = (sleeper(), sleeper());
2048 let pids = [first.id(), second.id()];
2049 assert!(
2050 pids.iter().all(|pid| in_job(*pid, &job)),
2051 "children must be in the caller's job"
2052 );
2053
2054 drop(job);
2056 let deadline = Instant::now() + Duration::from_secs(5);
2057 for pid in pids {
2058 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
2059 while process.is_running().unwrap_or(false) {
2060 assert!(
2061 Instant::now() < deadline,
2062 "child {pid} survived its caller's job"
2063 );
2064 std::thread::sleep(Duration::from_millis(20));
2065 }
2066 }
2067 drop((first, second));
2068 }
2069
2070 #[test]
2071 fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
2072 use std::{
2073 process::Command,
2074 sync::{
2075 atomic::{AtomicU32, Ordering},
2076 Arc,
2077 },
2078 thread,
2079 time::{Duration, Instant},
2080 };
2081
2082 let command = if cfg!(windows) {
2084 let mut command = Command::new("powershell.exe");
2086 command.args([
2087 "-NoLogo",
2088 "-NoProfile",
2089 "-NonInteractive",
2090 "-Command",
2091 "Start-Sleep -Seconds 60",
2092 ]);
2093 command
2094 } else {
2095 let mut command = Command::new("sleep");
2097 command.arg("60");
2098 command
2099 };
2100 let spawned_pid = Arc::new(AtomicU32::new(0));
2101
2102 let error = match super::spawn_for_test(command, Arc::clone(&spawned_pid)) {
2103 Ok(_) => panic!("injected wrapping failure must fail after spawning"),
2104 Err(error) => error,
2105 };
2106 let detail = crate::RunnerFailure::from_io(&error).expect("partial-spawn provenance");
2107 assert_eq!(detail.phase(), crate::RunnerPhase::Setup);
2108 assert_eq!(detail.termination(), crate::LeaderTermination::Unconfirmed);
2109 let pid = spawned_pid.load(Ordering::SeqCst);
2110 assert_ne!(pid, 0, "test must fail after the OS process was spawned");
2111 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
2112 let deadline = Instant::now() + Duration::from_secs(5);
2113 while process.is_running().unwrap_or(false) {
2114 assert!(
2115 Instant::now() < deadline,
2116 "partially spawned process leaked"
2117 );
2118 thread::sleep(Duration::from_millis(20));
2119 }
2120 }
2121
2122 #[cfg(windows)]
2125 fn tree_with_grandchild(pid_file: &std::path::Path, wait: bool) -> (super::OwnedChild, u32) {
2126 use std::time::{Duration, Instant};
2127 let _ = std::fs::remove_file(pid_file);
2128 let quoted = pid_file.display().to_string().replace('\'', "''");
2129 let tail = if wait {
2130 "; Wait-Process -Id $child.Id"
2131 } else {
2132 ""
2133 };
2134 let mut command = std::process::Command::new("powershell.exe");
2135 command
2136 .args([
2137 "-NoLogo",
2138 "-NoProfile",
2139 "-NonInteractive",
2140 "-Command",
2141 &format!(
2142 "$child = Start-Process powershell.exe -WindowStyle Hidden -ArgumentList '-NoLogo','-NoProfile','-NonInteractive','-Command','Start-Sleep -Seconds 60' -PassThru; Set-Content -LiteralPath '{quoted}' -Value $child.Id{tail}"
2143 ),
2144 ])
2145 .stdin(std::process::Stdio::null())
2146 .stdout(std::process::Stdio::null())
2147 .stderr(std::process::Stdio::null());
2148 let mut command = super::OwnedCommand::from_command(command);
2149 command.windows_hide();
2150 let owned = command.spawn().unwrap();
2151 let deadline = Instant::now() + Duration::from_secs(15);
2152 let grandchild = loop {
2153 if let Some(pid) = std::fs::read_to_string(pid_file)
2154 .ok()
2155 .and_then(|text| text.trim().parse().ok())
2156 {
2157 break pid;
2158 }
2159 assert!(Instant::now() < deadline, "grandchild pid was not recorded");
2160 std::thread::sleep(Duration::from_millis(20));
2161 };
2162 (owned, grandchild)
2163 }
2164
2165 #[cfg(windows)]
2166 fn job_active(owned: &super::OwnedChild) -> u32 {
2167 let job = owned
2168 .job
2169 .as_ref()
2170 .expect("an owned child keeps its private job");
2171 crate::windows_job::WindowsJob::active_count_of(crate::process_windows::raw_job(job))
2172 .unwrap()
2173 }
2174
2175 #[cfg(windows)]
2178 #[test]
2179 fn bounded_reap_reports_exited_only_once_the_job_is_empty() {
2180 use std::time::Duration;
2181 let pid_file =
2182 std::env::temp_dir().join(format!("rightkit-1260-live-{}.pid", std::process::id()));
2183 let (mut owned, grandchild) = tree_with_grandchild(&pid_file, true);
2184 let grandchild = super::AdoptedProcess::new(std::num::NonZeroU32::new(grandchild).unwrap());
2185 assert!(grandchild.is_running().unwrap());
2186 assert!(
2187 job_active(&owned) >= 2,
2188 "root and grandchild are in the job"
2189 );
2190
2191 let outcome = owned
2192 .terminate_tree_bounded(Duration::from_secs(10))
2193 .unwrap();
2194 assert!(
2195 matches!(outcome, super::ReapOutcome::Exited(_)),
2196 "{outcome:?}"
2197 );
2198 assert_eq!(
2199 job_active(&owned),
2200 0,
2201 "Exited while the job still held a process"
2202 );
2203 assert!(
2204 !grandchild.is_running().unwrap_or(false),
2205 "grandchild outlived Exited"
2206 );
2207 let _ = std::fs::remove_file(&pid_file);
2208 }
2209
2210 #[cfg(windows)]
2213 #[test]
2214 fn bounded_reap_sweeps_descendants_of_an_already_exited_root() {
2215 use std::time::Duration;
2216 let pid_file =
2217 std::env::temp_dir().join(format!("rightkit-1260-orphan-{}.pid", std::process::id()));
2218 let (mut owned, grandchild) = tree_with_grandchild(&pid_file, false);
2219 let grandchild = super::AdoptedProcess::new(std::num::NonZeroU32::new(grandchild).unwrap());
2220 assert!(
2221 owned
2222 .wait_timeout(Duration::from_secs(15))
2223 .unwrap()
2224 .is_some(),
2225 "root must exit on its own"
2226 );
2227 assert!(
2228 grandchild.is_running().unwrap(),
2229 "grandchild outlives its root"
2230 );
2231 assert!(
2232 job_active(&owned) >= 1,
2233 "grandchild stays in the root's job"
2234 );
2235
2236 let outcome = owned
2237 .terminate_tree_bounded(Duration::from_secs(10))
2238 .unwrap();
2239 assert!(
2240 matches!(outcome, super::ReapOutcome::Exited(_)),
2241 "{outcome:?}"
2242 );
2243 assert_eq!(job_active(&owned), 0);
2244 assert!(
2245 !grandchild.is_running().unwrap_or(false),
2246 "descendant was not swept"
2247 );
2248 let _ = std::fs::remove_file(&pid_file);
2249 }
2250
2251 #[cfg(windows)]
2254 #[test]
2255 fn bounded_reap_is_unconfirmed_while_the_job_is_not_empty() {
2256 use std::time::{Duration, Instant};
2257 let pid_file =
2258 std::env::temp_dir().join(format!("rightkit-1260-timeout-{}.pid", std::process::id()));
2259 let (mut owned, _) = tree_with_grandchild(&pid_file, true);
2260 let budget = Duration::from_millis(500);
2261 super::cleanup_test_fault::arm_job_empty_at(Instant::now() + Duration::from_secs(3600));
2262 let started = Instant::now();
2263 let outcome = owned.terminate_tree_bounded(budget);
2264 let elapsed = started.elapsed();
2265 super::cleanup_test_fault::disarm_job_empty_at();
2266 assert!(
2267 matches!(outcome, Ok(super::ReapOutcome::TimedOut)),
2268 "{outcome:?}"
2269 );
2270 assert!(
2271 elapsed < budget + Duration::from_millis(250),
2272 "budget overrun: {elapsed:?}"
2273 );
2274 let _ = std::fs::remove_file(&pid_file);
2275 }
2276
2277 #[cfg(windows)]
2280 #[test]
2281 fn job_seen_empty_only_after_the_deadline_is_not_success() {
2282 use std::time::{Duration, Instant};
2283 let pid_file =
2284 std::env::temp_dir().join(format!("rightkit-1260-late-{}.pid", std::process::id()));
2285 let (owned, _) = tree_with_grandchild(&pid_file, true);
2286 let job = owned.job.clone().expect("private job");
2287 let timeout = Duration::from_millis(100);
2288 super::cleanup_test_fault::arm_job_empty_at(
2290 Instant::now() + timeout + Duration::from_millis(5),
2291 );
2292 let late = job.wait_empty_until(Some(Instant::now() + timeout));
2293 super::cleanup_test_fault::arm_job_empty_at(Instant::now() + Duration::from_millis(20));
2295 let early = job.wait_empty_until(Some(Instant::now() + Duration::from_millis(500)));
2296 super::cleanup_test_fault::disarm_job_empty_at();
2297 assert!(
2298 !late.unwrap(),
2299 "a job seen empty after the deadline was reported empty"
2300 );
2301 assert!(
2302 early.unwrap(),
2303 "control: a job empty inside the budget is reported empty"
2304 );
2305 drop(owned);
2306 let _ = std::fs::remove_file(&pid_file);
2307 }
2308
2309 #[cfg(windows)]
2312 #[test]
2313 fn bounded_reap_after_a_delayed_handoff_keeps_the_original_deadline() {
2314 use std::time::{Duration, Instant};
2315 let pid_file =
2316 std::env::temp_dir().join(format!("rightkit-1260-handoff-{}.pid", std::process::id()));
2317 let (mut owned, _) = tree_with_grandchild(&pid_file, true);
2318 let budget = Duration::from_millis(300);
2319 let started = Instant::now();
2320 super::cleanup_test_fault::arm_job_empty_at(started + budget + Duration::from_millis(20));
2323 super::cleanup_test_fault::arm_handoff_delay(budget + Duration::from_millis(50));
2324 let outcome = owned.terminate_tree_bounded(budget);
2325 super::cleanup_test_fault::disarm_job_empty_at();
2326 assert!(
2327 matches!(outcome, Ok(super::ReapOutcome::TimedOut)),
2328 "a job empty only after the original deadline was confirmed: {outcome:?}"
2329 );
2330 drop(owned);
2331 let _ = std::fs::remove_file(&pid_file);
2332 }
2333
2334 #[cfg(windows)]
2337 #[test]
2338 fn bounded_reap_never_waits_past_its_budget_for_an_exiting_leader() {
2339 use std::time::{Duration, Instant};
2340 let pid_file =
2341 std::env::temp_dir().join(format!("rightkit-1260-refused-{}.pid", std::process::id()));
2342 let (mut owned, _) = tree_with_grandchild(&pid_file, true);
2343 let budget = Duration::from_millis(300);
2344 super::cleanup_test_fault::arm_termination_refused();
2345 let started = Instant::now();
2346 let outcome = owned.terminate_tree_bounded(budget);
2347 let elapsed = started.elapsed();
2348 assert!(
2349 !matches!(outcome, Ok(super::ReapOutcome::Exited(_))),
2350 "a refused termination was confirmed: {outcome:?}"
2351 );
2352 assert!(
2353 elapsed < budget + Duration::from_millis(250),
2354 "the kill step outlasted the budget: {elapsed:?}"
2355 );
2356 drop(owned);
2357 let _ = std::fs::remove_file(&pid_file);
2358 }
2359}
2360
2361#[cfg(test)]
2362mod tree_listing_tests {
2363 use super::listed_or_error;
2364
2365 #[test]
2366 fn an_unknown_group_listing_is_an_error_not_an_empty_tree() {
2367 let error = listed_or_error(None).expect_err("unknown must not read as empty");
2368 assert!(
2369 error.to_string().contains("could not be fully listed"),
2370 "{error}"
2371 );
2372 }
2373
2374 #[test]
2375 fn a_known_listing_passes_through_including_an_empty_one() {
2376 assert_eq!(listed_or_error(Some(vec![7, 9])).unwrap(), vec![7, 9]);
2377 assert!(listed_or_error(Some(Vec::new())).unwrap().is_empty());
2378 }
2379}