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(crate) fn bound_runner_cleanup(&mut self, timeout: Duration) {
392 self.partial_spawn_cleanup_timeout = Some(
393 self.partial_spawn_cleanup_timeout
394 .map_or(timeout, |current| current.min(timeout)),
395 );
396 }
397 pub fn spawn(self) -> io::Result<OwnedChild> {
398 self.spawn_inner(false, false, None)
399 }
400 pub fn spawn_suspended(mut self) -> io::Result<OwnedChild> {
410 self.hold_suspended = true;
411 self.spawn_inner(false, false, None)
412 }
413 pub fn spawn_uncontained_detached(self) -> io::Result<OwnedChild> {
428 self.spawn_inner_with(true, false, None, true)
429 }
430 pub fn spawn_uncontained_detached_strict(self) -> io::Result<OwnedChild> {
436 match self.spawn_inner_with(true, false, None, false) {
437 Err(error) if cfg!(windows) && is_denied_breakaway(&error) => Err(io::Error::new(
438 io::ErrorKind::PermissionDenied,
439 BreakawayDenied,
440 )),
441 other => other,
442 }
443 }
444 fn spawn_inner(
445 self,
446 detached: bool,
447 fail_after_spawn: bool,
448 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
449 ) -> io::Result<OwnedChild> {
450 self.spawn_inner_with(detached, fail_after_spawn, captured_pid, false)
451 }
452 fn spawn_inner_with(
453 mut self,
454 detached: bool,
455 fail_after_spawn: bool,
456 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
457 in_job_fallback: bool,
458 ) -> io::Result<OwnedChild> {
459 if detached && self.caller_job.is_some() {
460 return Err(io::Error::new(
461 io::ErrorKind::InvalidInput,
462 "detached launch cannot join a caller job",
463 ));
464 }
465 if detached && self.kill_on_parent_exit {
466 return Err(io::Error::new(
467 io::ErrorKind::InvalidInput,
468 "kill_on_parent_exit cannot combine with a detached launch",
469 ));
470 }
471 if let Some(RunStdio { stdin_piped }) = self.run_stdio {
472 self.command
475 .stdin(if stdin_piped {
476 Stdio::piped()
477 } else {
478 Stdio::null()
479 })
480 .stdout(Stdio::piped())
481 .stderr(Stdio::piped());
482 #[cfg(windows)]
483 if let Some(config) = self.windows_spawn_config.as_mut() {
484 config.stdin = if stdin_piped {
485 crate::WindowsStdio::Piped
486 } else {
487 crate::WindowsStdio::Null
488 };
489 config.stdout = crate::WindowsStdio::Piped;
490 config.stderr = crate::WindowsStdio::Piped;
491 }
492 }
493 #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
494 if self.kill_on_parent_exit {
495 return Err(io::Error::new(
496 io::ErrorKind::Unsupported,
497 "kill_on_parent_exit is supported on Linux and macOS only",
498 ));
499 }
500 #[cfg(unix)]
501 {
502 use std::os::unix::process::CommandExt;
503 let session = detached || self.unix_containment == UnixContainment::Session;
504 let nice_floor = self.process_priority.nice_floor();
505 #[cfg(target_os = "macos")]
506 let utility_qos = self.process_priority == ProcessPriority::Efficient;
507 let inheritance = self
508 .inherited_handles
509 .take()
510 .map(crate::process_unix::Inheritance::new)
511 .transpose()?;
512 #[cfg(target_os = "linux")]
514 let supervisor = self
515 .kill_on_parent_exit
516 .then(|| unsafe { libc::getpid() });
518 unsafe {
520 self.command.pre_exec(move || {
521 let result = if session {
522 libc::setsid()
523 } else {
524 libc::setpgid(0, 0)
525 };
526 if result < 0 {
527 return Err(io::Error::last_os_error());
528 }
529 if let Some(floor) = nice_floor {
530 crate::process_unix::raise_nice_to(floor)?;
531 }
532 #[cfg(target_os = "macos")]
533 if utility_qos {
534 crate::process_unix::set_utility_qos();
535 }
536 if let Some(inheritance) = &inheritance {
537 inheritance.apply()?;
538 }
539 #[cfg(target_os = "linux")]
540 if let Some(supervisor) = supervisor {
541 crate::process_unix::die_with_parent(supervisor)?;
542 }
543 Ok(())
544 });
545 }
546 }
547 #[cfg(windows)]
548 crate::process_windows::configure(
549 &mut self.command,
550 detached,
551 self.windows_hidden,
552 self.process_priority,
553 );
554 #[cfg(unix)]
555 let _ = (
556 self.windows_hidden,
557 self.windows_breakaway,
558 self.job_assignment,
559 );
560 #[cfg(windows)]
561 let _ = (self.unix_containment, self.kill_on_parent_exit);
562 let spawned_at = SystemTime::now();
563 let hold_suspended = self.hold_suspended && !detached;
564 #[cfg(unix)]
565 let _ = hold_suspended;
566 #[cfg(unix)]
567 let child = {
568 let mut child = crate::unix_child::UnixChild::new(self.command.spawn()?);
569 if let Err(error) = child.require_identity() {
572 let reaped = child.cleanup_unidentified(self.partial_spawn_cleanup_timeout);
573 return Err(spawn_cleanup_failure(error, reaped));
574 }
575 #[cfg(target_os = "macos")]
578 if self.kill_on_parent_exit {
579 if let Err(error) = crate::process_unix::arm_parent_death_watch(child.id()) {
580 let _ = child.signal(SignalTarget::Group, libc::SIGKILL);
581 let reaped = cleanup_direct(
582 Arc::new(Mutex::new(child)),
583 self.partial_spawn_cleanup_timeout,
584 );
585 return Err(spawn_cleanup_failure(error, reaped));
586 }
587 }
588 child
589 };
590 #[cfg(windows)]
591 let (child, stayed_in_parent_job) = match self.inherited_handles.as_deref() {
592 None => {
593 let _spawning = crate::spawn_windows::spawn_guard();
596 match self.command.spawn() {
597 Ok(child) => (PlatformChild::from(child), false),
598 Err(error)
600 if detached
601 && in_job_fallback
602 && crate::process_windows::is_breakaway_denied(&error) =>
603 {
604 crate::process_windows::configure_in_job_detached(
605 &mut self.command,
606 self.windows_hidden,
607 self.process_priority,
608 );
609 (PlatformChild::from(self.command.spawn()?), true)
610 }
611 Err(error) => return Err(error),
612 }
613 }
614 Some(handles) => {
615 let config = self.windows_spawn_config.take().ok_or_else(|| {
616 io::Error::new(io::ErrorKind::InvalidInput,
617 "allowlisted Windows spawn requires windows_spawn_config for opaque Command settings")
618 })?;
619 crate::spawn_windows::spawn(
620 &self.command,
621 config,
622 handles,
623 detached,
624 self.windows_hidden,
625 self.process_priority,
626 self.env_cleared,
627 self.partial_spawn_cleanup_timeout,
628 in_job_fallback,
629 )?
630 }
631 };
632 #[cfg(not(windows))]
633 let _ = in_job_fallback;
634 #[cfg(not(windows))]
635 let stayed_in_parent_job = false;
636 let pid = child.id();
637 if let Some(captured) = captured_pid {
638 captured.store(pid, std::sync::atomic::Ordering::SeqCst);
639 }
640 let child = Arc::new(Mutex::new(child));
641 let handle = match ProcessHandle::new(&child, spawned_at) {
642 Ok(handle) => handle,
643 Err(error) => {
644 let reaped = cleanup_direct(child, self.partial_spawn_cleanup_timeout);
645 return Err(spawn_cleanup_failure(error, reaped));
646 }
647 };
648 let mut partial = PartialChild {
649 child: Some(OwnedChild {
650 child,
651 handle,
652 pid,
653 detached,
654 terminate_on_drop: !detached,
655 drop_reap_timeout: DEFAULT_DROP_REAP_TIMEOUT,
656 tree_terminated: false,
657 assignment_failures: Vec::new(),
658 suspended: std::sync::atomic::AtomicBool::new(cfg!(windows) && hold_suspended),
659 stayed_in_parent_job,
660 #[cfg(windows)]
661 job: None,
662 }),
663 timeout: self.partial_spawn_cleanup_timeout,
664 };
665 #[cfg(windows)]
666 if !detached {
667 let owned = partial.child.as_mut().expect("partial child is present");
668 let assignment = crate::process_windows::assign_jobs(
669 &owned.handle,
670 self.caller_job.take(),
671 self.job_assignment,
672 self.windows_breakaway,
673 &mut owned.job,
674 &mut owned.assignment_failures,
675 );
676 if let Err(error) = assignment {
677 return Err(partial.fail(error));
678 }
679 }
680 #[cfg(windows)]
681 {
682 let owned = partial.child.as_ref().expect("partial child is present");
683 if self.process_priority == ProcessPriority::Efficient {
684 let _ = crate::process_windows::apply_efficiency(&*owned.child());
686 }
687 if !hold_suspended {
688 let resumed = owned.child().resume(detached);
689 if let Err(error) = resumed {
690 return Err(partial.fail(error));
691 }
692 }
693 }
694 if fail_after_spawn {
695 return Err(partial.fail(io::Error::other("injected post-spawn wrapping failure")));
696 }
697 let owned = partial.child.take().expect("partial child is present");
698 if !detached {
699 #[cfg(unix)]
701 crate::registry::register(&owned.child, owned.pid);
702 #[cfg(windows)]
703 crate::registry::register(
704 &owned.child,
705 &owned.handle,
706 owned
707 .job
708 .as_ref()
709 .map_or_else(std::sync::Weak::new, std::sync::Arc::downgrade),
710 owned.pid,
711 );
712 }
713 Ok(owned)
714 }
715}
716
717#[derive(Clone, Copy, Debug, Eq, PartialEq)]
723pub struct BreakawayDenied;
724
725impl std::fmt::Display for BreakawayDenied {
726 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
727 f.write_str(
728 "detached launch denied: the caller's job object does not allow breakaway \
729 (ERROR_ACCESS_DENIED); no fallback was attempted",
730 )
731 }
732}
733
734impl std::error::Error for BreakawayDenied {}
735
736#[cfg(windows)]
737fn is_denied_breakaway(error: &io::Error) -> bool {
738 crate::process_windows::is_breakaway_denied(error)
739}
740#[cfg(not(windows))]
741fn is_denied_breakaway(_error: &io::Error) -> bool {
742 false
743}
744
745pub const DEFAULT_DROP_REAP_TIMEOUT: Duration = Duration::from_secs(2);
748
749pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
750 "PATH",
751 "HOME",
752 "USER",
753 "LOGNAME",
754 "LANG",
755 "LC_ALL",
756 "TMPDIR",
757 "TEMP",
758 "TMP",
759 "SystemRoot",
760 "SYSTEMROOT",
761 "USERPROFILE",
762 "APPDATA",
763 "LOCALAPPDATA",
764 "ProgramData",
765 "COMSPEC",
766 "PATHEXT",
767 "XDG_DATA_HOME",
768 "XDG_RUNTIME_DIR",
769];
770
771#[derive(Debug)]
772pub struct OwnedChild {
773 child: Arc<Mutex<PlatformChild>>,
774 handle: ProcessHandle,
775 pid: u32,
777 detached: bool,
778 terminate_on_drop: bool,
779 drop_reap_timeout: Duration,
780 tree_terminated: bool,
781 assignment_failures: Vec<JobAssignmentFailure>,
782 suspended: std::sync::atomic::AtomicBool,
784 stayed_in_parent_job: bool,
786 #[cfg(windows)]
789 job: Option<std::sync::Arc<crate::process_windows::KillOnCloseJob>>,
790}
791#[derive(Clone, Debug, Eq, PartialEq)]
793pub struct TreeTermination {
794 pub status: ExitStatus,
796 pub descendants: crate::SweepReport,
798}
799#[derive(Clone, Copy, Debug, Eq, PartialEq)]
800pub enum ReapOutcome {
801 Exited(ExitStatus),
802 TimedOut,
803}
804#[derive(Clone, Copy, Debug, Eq, PartialEq)]
805pub enum WaitOutcome {
806 Exited(ExitStatus),
807 Terminated(ExitStatus),
808}
809
810impl OwnedChild {
811 pub fn id(&self) -> u32 {
812 self.pid
813 }
814 pub fn stayed_in_parent_job(&self) -> bool {
819 self.stayed_in_parent_job
820 }
821 pub fn child(&self) -> MutexGuard<'_, PlatformChild> {
837 lock_child(&self.child)
838 }
839 pub fn resume(&self) -> io::Result<()> {
844 if self
845 .suspended
846 .swap(false, std::sync::atomic::Ordering::SeqCst)
847 {
848 #[cfg(windows)]
849 if let Err(error) = self.child().resume(self.detached) {
850 self.suspended
851 .store(true, std::sync::atomic::Ordering::SeqCst);
852 return Err(error);
853 }
854 }
855 Ok(())
856 }
857 pub fn is_suspended(&self) -> bool {
859 self.suspended.load(std::sync::atomic::Ordering::SeqCst)
860 }
861 pub fn while_unreaped<R>(&self, act: impl FnOnce(u32) -> R) -> io::Result<Option<R>> {
870 #[cfg(unix)]
871 {
872 let mut child = lock_child(&self.child);
873 if child.leader_state()? == LeaderState::Reaped {
874 return Ok(None);
875 }
876 Ok(Some(act(self.pid)))
877 }
878 #[cfg(windows)]
879 Ok(Some(act(self.pid)))
880 }
881 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
882 self.child().stdin.take()
883 }
884 pub fn close_stdin(&mut self) {
888 drop(self.child().stdin.take());
889 }
890 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
891 self.child().stdout.take()
892 }
893 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
894 self.child().stderr.take()
895 }
896 pub fn job_assignment_failures(&self) -> &[JobAssignmentFailure] {
897 &self.assignment_failures
898 }
899 pub fn process_handle(&self) -> io::Result<ProcessHandle> {
903 self.handle.try_clone()
904 }
905 pub fn creation_time(&self) -> Option<SystemTime> {
906 self.handle.creation_time()
907 }
908 #[cfg(windows)]
909 pub fn creation_time_ticks(&self) -> Option<u64> {
910 self.handle.creation_time_ticks()
911 }
912 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
913 self.handle.try_wait()
915 }
916 pub fn wait(&mut self) -> io::Result<ExitStatus> {
919 drop(self.child().stdin.take());
920 self.handle.wait()
921 }
922 pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
923 self.handle.wait_timeout(timeout)
924 }
925 pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
935 self.wait_or_kill_inner(grace, true)
936 }
937 pub fn wait_then_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
943 self.wait_or_kill_inner(grace, false)
944 }
945 fn wait_or_kill_inner(&mut self, grace: Duration, term_first: bool) -> io::Result<WaitOutcome> {
946 #[cfg(not(unix))]
947 let _ = term_first;
948 #[cfg(unix)]
949 {
950 if term_first && !self.tree_terminated {
951 self.signal(libc::SIGTERM)?;
952 }
953 if self.handle.wait_exited_timeout(grace)? {
955 self.force_kill(1)?;
960 return self.wait().map(WaitOutcome::Exited);
961 }
962 self.terminate_tree().map(WaitOutcome::Terminated)
963 }
964 #[cfg(windows)]
965 {
966 if let Some(status) = self.wait_timeout(grace)? {
967 self.force_kill(1)?;
971 return Ok(WaitOutcome::Exited(status));
972 }
973 self.terminate_tree().map(WaitOutcome::Terminated)
974 }
975 }
976 pub(crate) fn wait_exit_then_sweep(
982 &mut self,
983 timeout: Duration,
984 ) -> io::Result<Option<ExitStatus>> {
985 if self.handle.wait_exited_timeout(timeout)? {
986 self.force_kill(1)?;
987 return self.wait().map(Some);
988 }
989 Ok(None)
990 }
991 pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
996 self.force_kill(1)?;
997 self.wait()
998 }
999 pub fn terminate_tree_with_options(
1004 &mut self,
1005 options: TerminationOptions,
1006 ) -> io::Result<ExitStatus> {
1007 self.start_termination(options)?;
1008 self.wait()
1009 }
1010 pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
1011 self.terminate_tree_bounded_with_options(timeout, TerminationOptions::default())
1012 }
1013 pub fn terminate_tree_bounded_with_options(
1015 &mut self,
1016 timeout: Duration,
1017 mut options: TerminationOptions,
1018 ) -> io::Result<ReapOutcome> {
1019 let started = Instant::now();
1020 options.grace = options.grace.min(timeout);
1021 self.start_termination(options)?;
1022 #[cfg(test)]
1023 if cleanup_test_fault::refuse(timeout.saturating_sub(started.elapsed())) {
1024 return Ok(ReapOutcome::TimedOut);
1025 }
1026 Ok(
1027 match self.wait_timeout(timeout.saturating_sub(started.elapsed()))? {
1028 Some(status) => ReapOutcome::Exited(status),
1029 None => ReapOutcome::TimedOut,
1030 },
1031 )
1032 }
1033 pub fn terminate_tree_deep(
1057 &mut self,
1058 options: TerminationOptions,
1059 ) -> io::Result<TreeTermination> {
1060 let mut found = self.descendant_identities()?;
1061 found.reverse(); #[cfg(unix)]
1063 if !self.tree_terminated {
1064 signal_shared(&self.child, SignalTarget::Group, libc::SIGTERM)?;
1065 await_tree_shared(&self.child, &self.handle, options.grace, true)?;
1066 signal_shared(&self.child, SignalTarget::Group, libc::SIGKILL)?;
1067 self.tree_terminated = true;
1068 }
1069 #[cfg(windows)]
1070 self.force_kill(options.windows_exit_code)?;
1071 let status = self.wait()?;
1072 let descendants = crate::sweep(&found, options.grace)?;
1073 Ok(TreeTermination {
1074 status,
1075 descendants,
1076 })
1077 }
1078 fn start_termination(&mut self, options: TerminationOptions) -> io::Result<()> {
1079 #[cfg(unix)]
1080 if !self.tree_terminated {
1081 self.signal(libc::SIGTERM)?;
1082 self.await_tree(options.grace)?;
1086 }
1087 self.force_kill(options.windows_exit_code)
1088 }
1089 #[cfg(unix)]
1096 fn await_tree(&self, grace: Duration) -> io::Result<()> {
1097 await_tree_shared(&self.child, &self.handle, grace, !self.detached)
1099 }
1100 #[cfg(unix)]
1104 fn signal(&self, signal: i32) -> io::Result<()> {
1105 let target = if self.detached {
1108 SignalTarget::Leader
1109 } else {
1110 SignalTarget::Group
1111 };
1112 signal_shared(&self.child, target, signal).map(|_| ())
1113 }
1114 fn force_kill(&mut self, code: u32) -> io::Result<()> {
1115 if self.tree_terminated {
1116 return Ok(());
1117 }
1118 #[cfg(unix)]
1119 {
1120 let _ = code;
1121 self.signal(libc::SIGKILL)?;
1122 }
1123 #[cfg(windows)]
1124 crate::process_windows::terminate(&self.handle, self.job.as_deref(), code)?;
1125 self.tree_terminated = true;
1126 Ok(())
1127 }
1128}
1129impl OwnedChild {
1130 pub fn set_drop_reap_timeout(&mut self, timeout: Duration) -> &mut Self {
1137 self.drop_reap_timeout = timeout;
1138 self
1139 }
1140 pub fn drop_reap_timeout(&self) -> Duration {
1142 self.drop_reap_timeout
1143 }
1144 fn reap_in_background(&self) {
1146 #[cfg(unix)]
1147 if let Ok(reader) = self.handle.try_clone() {
1148 let _ = thread::Builder::new()
1149 .name("rightkit-drop-reap".into())
1150 .spawn(move || {
1151 let _ = reader.wait();
1152 });
1153 }
1154 }
1155 pub fn descendant_identities(&self) -> io::Result<Vec<crate::AdoptedIdentity>> {
1159 let root = self.while_unreaped(|pid| match NonZeroU32::new(pid) {
1160 Some(pid) => crate::AdoptedIdentity::capture(pid),
1161 None => Ok(None),
1162 })?;
1163 match root {
1164 Some(Ok(Some(root))) => root.descendants(),
1165 Some(Err(error)) => Err(error),
1166 _ => Ok(Vec::new()),
1167 }
1168 }
1169}
1170impl Drop for OwnedChild {
1171 fn drop(&mut self) {
1172 crate::registry::forget(&self.child);
1174 if self.terminate_on_drop {
1175 if self.force_kill(1).is_ok() {
1179 drop(self.child().stdin.take());
1180 if !matches!(
1181 self.handle.wait_timeout(self.drop_reap_timeout),
1182 Ok(Some(_))
1183 ) {
1184 self.reap_in_background();
1185 }
1186 }
1187 } else if self.detached {
1188 #[cfg(unix)]
1189 {
1190 {
1194 let mut child = self.child();
1195 drop(child.stdin.take());
1196 drop(child.stdout.take());
1197 drop(child.stderr.take());
1198 }
1199 if let Ok(reader) = self.handle.try_clone() {
1200 let _ = thread::Builder::new()
1201 .name("rightkit-detached-reap".into())
1202 .spawn(move || {
1203 let _ = reader.wait();
1204 });
1205 }
1206 }
1207 }
1208 }
1209}
1210
1211pub(crate) fn spawn_setup_failure(error: io::Error) -> io::Error {
1214 crate::run::failure(
1215 crate::RunnerPhase::Setup,
1216 crate::LeaderTermination::Unconfirmed,
1217 error,
1218 )
1219}
1220
1221pub(crate) fn spawn_cleanup_failure(error: io::Error, reaped: bool) -> io::Error {
1222 if reaped {
1223 spawn_setup_failure(error)
1224 } else {
1225 crate::run::failure(
1226 crate::RunnerPhase::Cleanup,
1227 crate::LeaderTermination::Unconfirmed,
1228 error,
1229 )
1230 }
1231}
1232
1233struct PartialChild {
1234 child: Option<OwnedChild>,
1235 timeout: Option<Duration>,
1236}
1237impl PartialChild {
1238 fn cleanup(&mut self) -> bool {
1239 if let Some(mut child) = self.child.take() {
1240 child.terminate_on_drop = false;
1241 let _ = child.force_kill(1);
1242 cleanup_direct(Arc::clone(&child.child), self.timeout)
1243 } else {
1244 true
1245 }
1246 }
1247
1248 fn fail(mut self, error: io::Error) -> io::Error {
1249 let reaped = self.cleanup();
1250 spawn_cleanup_failure(error, reaped)
1251 }
1252}
1253impl Drop for PartialChild {
1254 fn drop(&mut self) {
1255 self.cleanup();
1256 }
1257}
1258#[cfg(unix)]
1261const MID_EXIT_RETRY: Duration = Duration::from_millis(300);
1262
1263#[cfg(unix)]
1267pub(crate) fn signal_shared(
1268 shared: &Mutex<crate::unix_child::UnixChild>,
1269 target: SignalTarget,
1270 signal: i32,
1271) -> io::Result<SignalOutcome> {
1272 let started = Instant::now();
1273 loop {
1274 let (outcome, watch) = {
1275 let mut child = lock_child(shared);
1276 (child.signal(target, signal)?, child.exit_watch())
1277 };
1278 if outcome != SignalOutcome::MidExit {
1279 return Ok(outcome);
1280 }
1281 let remaining = MID_EXIT_RETRY.saturating_sub(started.elapsed());
1284 if remaining.is_zero() {
1285 return Err(io::Error::from_raw_os_error(libc::EPERM));
1286 }
1287 watch.pause(remaining);
1288 }
1289}
1290
1291#[cfg(unix)]
1293pub(crate) fn await_tree_shared(
1294 shared: &Mutex<crate::unix_child::UnixChild>,
1295 handle: &ProcessHandle,
1296 grace: Duration,
1297 whole_group: bool,
1298) -> io::Result<()> {
1299 use crate::{exit_watch::POLL_INTERVAL, unix_child::Members};
1300 let Some(deadline) = Instant::now().checked_add(grace) else {
1301 return Ok(());
1302 };
1303 let left = |deadline: Instant| deadline.saturating_duration_since(Instant::now());
1304 if !handle.wait_exited_timeout(left(deadline))? {
1305 return Ok(()); }
1307 if !whole_group {
1308 return Ok(());
1309 }
1310 loop {
1311 match lock_child(shared).group_other_members()? {
1312 Members::Pids(pids) if pids.is_empty() => return Ok(()),
1313 Members::Pids(_) => {}
1314 Members::Unknown => return wait_out(left(deadline)),
1315 }
1316 let remaining = left(deadline);
1317 if remaining.is_zero() {
1318 return Ok(()); }
1320 thread::sleep(POLL_INTERVAL.min(remaining));
1321 }
1322}
1323
1324#[cfg(unix)]
1327fn wait_out(remaining: Duration) -> io::Result<()> {
1328 thread::sleep(remaining);
1329 Ok(())
1330}
1331pub(crate) fn cleanup_direct(child: Arc<Mutex<PlatformChild>>, timeout: Option<Duration>) -> bool {
1338 let _ = lock_child(&child).kill();
1339 let Some(timeout) = timeout else {
1340 return lock_child(&child).wait().is_ok();
1342 };
1343 #[cfg(test)]
1344 let refused = cleanup_test_fault::refuse(timeout);
1345 #[cfg(not(test))]
1346 let refused = false;
1347 #[cfg(unix)]
1348 {
1349 let watch = lock_child(&child).exit_watch();
1352 let deadline = Instant::now().checked_add(timeout);
1353 if !refused
1354 && matches!(
1355 watch.wait_for(deadline, || lock_child(&child).try_wait()),
1356 Ok(Some(_))
1357 )
1358 {
1359 return true;
1360 }
1361 }
1362 #[cfg(windows)]
1363 if !refused {
1364 let started = Instant::now();
1365 loop {
1366 match lock_child(&child).try_wait() {
1367 Ok(Some(_)) => return true,
1368 Err(_) => break,
1369 Ok(None) => {}
1370 }
1371 if started.elapsed() >= timeout {
1372 break;
1373 }
1374 thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
1375 }
1376 }
1377 let _ = thread::Builder::new()
1381 .name("rightkit-partial-reap".into())
1382 .spawn(move || {
1383 let _ = lock_child(&child).wait();
1384 });
1385 false
1386}
1387
1388#[cfg(test)]
1389pub(crate) mod cleanup_test_fault {
1390 use std::{cell::Cell, time::Duration};
1391 thread_local! { static REFUSE_REAP: Cell<bool> = const { Cell::new(false) }; }
1392 pub(crate) fn arm() {
1393 REFUSE_REAP.with(|armed| armed.set(true));
1394 }
1395 pub(crate) fn refuse(timeout: Duration) -> bool {
1396 if REFUSE_REAP.with(|armed| armed.replace(false)) {
1397 std::thread::sleep(timeout);
1400 true
1401 } else {
1402 false
1403 }
1404 }
1405}
1406
1407#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1408pub struct AdoptedProcess {
1409 pid: NonZeroU32,
1410}
1411impl AdoptedProcess {
1412 pub fn new(pid: NonZeroU32) -> Self {
1413 Self { pid }
1414 }
1415 pub fn id(&self) -> u32 {
1416 self.pid.get()
1417 }
1418 pub fn is_running(&self) -> io::Result<bool> {
1422 process_is_running(self.pid)
1423 }
1424 pub fn identity(&self) -> io::Result<Option<crate::AdoptedIdentity>> {
1429 crate::AdoptedIdentity::capture(self.pid)
1430 }
1431}
1432#[cfg(test)]
1433fn spawn_for_test(
1434 command: Command,
1435 captured_pid: Arc<std::sync::atomic::AtomicU32>,
1436) -> io::Result<OwnedChild> {
1437 OwnedCommand::from_command(command).spawn_inner(false, true, Some(captured_pid))
1438}
1439
1440#[cfg(windows)]
1441fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1442 use windows::Win32::{
1443 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
1444 System::Threading::{
1445 OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
1446 PROCESS_SYNCHRONIZE,
1447 },
1448 };
1449
1450 let handle = unsafe {
1451 OpenProcess(
1452 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
1453 false,
1454 pid.get(),
1455 )
1456 };
1457 let handle = match handle {
1458 Ok(handle) => handle,
1459 Err(error) => {
1460 if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
1461 return Ok(false);
1462 }
1463 return Err(io::Error::other(error.to_string()));
1464 }
1465 };
1466 let wait = unsafe { WaitForSingleObject(handle, 0) };
1467 let close = unsafe { CloseHandle(handle) };
1468 close.map_err(|error| io::Error::other(error.to_string()))?;
1469 match wait.0 {
1470 0 => Ok(false),
1471 value if value == WAIT_TIMEOUT.0 => Ok(true),
1472 _ => Err(io::Error::last_os_error()),
1473 }
1474}
1475
1476#[cfg(unix)]
1477fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1478 let raw_pid = i32::try_from(pid.get()).map_err(io::Error::other)?;
1479 loop {
1480 if unsafe { libc::kill(raw_pid, 0) } == 0 {
1482 return Ok(!crate::foreign::process_is_zombie(pid.get()));
1485 }
1486 let error = io::Error::last_os_error();
1487 match error.raw_os_error() {
1488 Some(libc::EINTR) => continue,
1489 Some(libc::EPERM) => return Ok(!crate::foreign::process_is_zombie(pid.get())),
1490 Some(libc::ESRCH) => return Ok(false),
1491 _ => return Err(error),
1492 }
1493 }
1494}
1495
1496#[cfg(test)]
1497mod tests {
1498 #[test]
1499 fn option_defaults_preserve_existing_behavior() {
1500 let command = super::OwnedCommand::new("unused");
1501 assert_eq!(command.job_assignment, super::JobAssignmentMode::Strict);
1502 assert_eq!(
1503 command.unix_containment,
1504 super::UnixContainment::ProcessGroup
1505 );
1506 assert_eq!(command.partial_spawn_cleanup_timeout, None);
1507 assert_eq!(
1508 super::TerminationOptions::default().grace,
1509 std::time::Duration::ZERO
1510 );
1511 assert_eq!(super::TerminationOptions::default().windows_exit_code, 1);
1512 }
1513
1514 #[cfg(not(windows))]
1515 #[test]
1516 fn new_and_from_command_share_spawn_settings() {
1517 use super::OwnedCommand;
1518 use std::process::Command;
1519 let by_new = OwnedCommand::new("unused");
1521 let by_from = OwnedCommand::from_command(Command::new("unused"));
1522 assert_eq!(format!("{by_new:?}"), format!("{by_from:?}"));
1523 }
1524
1525 #[cfg(windows)]
1529 #[test]
1530 fn only_new_supplies_a_default_windows_spawn_config() {
1531 use super::OwnedCommand;
1532 use std::process::Command;
1533 let by_new = OwnedCommand::new("unused");
1534 let by_from = OwnedCommand::from_command(Command::new("unused"));
1535 assert!(by_new.windows_spawn_config.is_some() && by_new.windows_spawn_config_auto);
1536 assert!(by_from.windows_spawn_config.is_none() && !by_from.windows_spawn_config_auto);
1537 }
1538
1539 #[test]
1540 fn process_priority_defaults_to_normal_and_keeps_the_requested_value() {
1541 use super::{OwnedCommand, ProcessPriority};
1542 let mut command = OwnedCommand::new("unused");
1543 assert_eq!(command.process_priority, ProcessPriority::Normal);
1544 command.process_priority(ProcessPriority::BelowNormal);
1545 assert_eq!(command.process_priority, ProcessPriority::BelowNormal);
1546 command.process_priority(ProcessPriority::Idle);
1547 assert_eq!(command.process_priority, ProcessPriority::Idle);
1548 }
1549
1550 #[cfg(unix)]
1553 #[test]
1554 fn process_priority_maps_to_nice_floors() {
1555 use super::ProcessPriority;
1556 assert_eq!(ProcessPriority::Normal.nice_floor(), None);
1557 assert_eq!(ProcessPriority::BelowNormal.nice_floor(), Some(10));
1558 assert_eq!(ProcessPriority::Idle.nice_floor(), Some(19));
1559 assert_eq!(ProcessPriority::Efficient.nice_floor(), Some(10));
1560 }
1561
1562 #[cfg(windows)]
1564 #[test]
1565 fn below_normal_child_has_below_normal_priority_class() {
1566 use super::{OwnedCommand, ProcessPriority};
1567 use std::process::Stdio;
1568 use windows::Win32::{
1569 Foundation::CloseHandle,
1570 System::Threading::{
1571 GetPriorityClass, OpenProcess, BELOW_NORMAL_PRIORITY_CLASS,
1572 PROCESS_QUERY_LIMITED_INFORMATION,
1573 },
1574 };
1575 let mut command = OwnedCommand::new("ping");
1576 command
1577 .command_mut()
1578 .args(["-n", "6", "127.0.0.1"])
1579 .stdin(Stdio::null())
1580 .stdout(Stdio::null())
1581 .stderr(Stdio::null());
1582 command.process_priority(ProcessPriority::BelowNormal);
1583 let mut child = command.spawn().expect("spawn ping");
1584 let class = unsafe {
1586 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, child.id())
1587 .expect("open the child");
1588 let class = GetPriorityClass(handle);
1589 let _ = CloseHandle(handle);
1590 class
1591 };
1592 let _ = child.terminate_tree();
1593 assert_eq!(class, BELOW_NORMAL_PRIORITY_CLASS.0);
1594 }
1595
1596 #[cfg(unix)]
1598 #[test]
1599 fn lowered_priority_child_runs_at_or_above_the_nice_floor() {
1600 use super::{OwnedCommand, ProcessPriority};
1601 use std::{io::Read, process::Stdio};
1602 for (priority, floor) in [
1603 (ProcessPriority::BelowNormal, 10),
1604 (ProcessPriority::Idle, 19),
1605 ] {
1606 let mut command = OwnedCommand::new("/bin/sh");
1607 command
1608 .command_mut()
1609 .args(["-c", "ps -o ni= -p $$"])
1610 .stdin(Stdio::null())
1611 .stdout(Stdio::piped())
1612 .stderr(Stdio::null());
1613 command.process_priority(priority);
1614 let mut child = command.spawn().expect("spawn sh");
1615 let mut text = String::new();
1616 child
1617 .take_stdout()
1618 .expect("piped stdout")
1619 .read_to_string(&mut text)
1620 .expect("read ps output");
1621 let _ = child.wait();
1622 let nice: i32 = text.trim().parse().expect("ps printed a nice value");
1623 assert!(nice >= floor, "nice {nice} below floor {floor}");
1624 }
1625 }
1626
1627 #[test]
1628 fn breakaway_is_off_by_default_explicit_by_default_opt_in_and_silent_is_separate() {
1629 use super::BreakawayMode;
1630 let mut command = super::OwnedCommand::new("unused");
1631 assert_eq!(command.windows_breakaway, BreakawayMode::None);
1632 command.windows_allow_breakaway(true);
1633 assert_eq!(command.windows_breakaway, BreakawayMode::Explicit);
1634 command.windows_allow_silent_breakaway(true);
1635 assert_eq!(command.windows_breakaway, BreakawayMode::Silent);
1636 command.windows_allow_silent_breakaway(false);
1637 assert_eq!(command.windows_breakaway, BreakawayMode::None);
1638 }
1639
1640 #[test]
1641 fn kill_on_parent_exit_is_opt_in() {
1642 let mut command = super::OwnedCommand::new("unused");
1643 assert!(!command.kill_on_parent_exit);
1644 command.kill_on_parent_exit(true);
1645 assert!(command.kill_on_parent_exit);
1646 }
1647
1648 #[test]
1649 fn breakaway_denied_is_a_typed_recoverable_error() {
1650 let error =
1651 std::io::Error::new(std::io::ErrorKind::PermissionDenied, super::BreakawayDenied);
1652 assert!(error
1653 .get_ref()
1654 .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1655 .is_some());
1656 assert!(super::BreakawayDenied.to_string().contains("breakaway"));
1657 }
1658
1659 #[test]
1660 fn a_strict_detached_spawn_passes_other_failures_through_untyped() {
1661 let command = super::OwnedCommand::new("rightkit-no-such-program-for-strict-detach");
1662 let Err(error) = command.spawn_uncontained_detached_strict() else {
1663 panic!("a missing program must not spawn");
1664 };
1665 assert!(error
1666 .get_ref()
1667 .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1668 .is_none());
1669 }
1670
1671 #[test]
1672 fn a_strict_detached_spawn_still_rejects_kill_on_parent_exit() {
1673 let mut command = super::OwnedCommand::new("unused");
1674 command.kill_on_parent_exit(true);
1675 let Err(error) = command.spawn_uncontained_detached_strict() else {
1676 panic!("a detached spawn must not accept the parent-death tie");
1677 };
1678 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1679 }
1680
1681 #[test]
1682 fn a_detached_spawn_rejects_kill_on_parent_exit() {
1683 let mut command = super::OwnedCommand::new("unused");
1685 command.kill_on_parent_exit(true);
1686 let Err(error) = command.spawn_uncontained_detached() else {
1687 panic!("a detached spawn must not accept the parent-death tie");
1688 };
1689 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1690 }
1691
1692 #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
1695 #[test]
1696 fn a_host_without_a_parent_death_primitive_refuses_before_starting_anything() {
1697 let marker =
1698 std::env::temp_dir().join(format!("rightkit-process-untied-{}", std::process::id()));
1699 let _ = std::fs::remove_file(&marker);
1700 let mut command = super::OwnedCommand::new("/usr/bin/touch");
1701 command.command_mut().arg(&marker);
1702 command.kill_on_parent_exit(true);
1703 let Err(error) = command.spawn() else {
1704 panic!("kill_on_parent_exit must not run the child untied");
1705 };
1706 assert_eq!(error.kind(), std::io::ErrorKind::Unsupported);
1707 std::thread::sleep(std::time::Duration::from_millis(200));
1708 assert!(!marker.exists(), "the program ran");
1709 }
1710
1711 #[test]
1712 fn option_builders_keep_every_requested_value() {
1713 use super::{JobAssignmentMode, OwnedCommand, TerminationOptions, UnixContainment};
1714 use std::time::Duration;
1715 let mut command = OwnedCommand::new("unused");
1716 command
1717 .job_assignment_mode(JobAssignmentMode::BestEffort)
1718 .unix_containment(UnixContainment::Session)
1719 .partial_spawn_cleanup_timeout(Duration::from_secs(5));
1720 assert_eq!(command.job_assignment, JobAssignmentMode::BestEffort);
1721 assert_eq!(command.unix_containment, UnixContainment::Session);
1722 assert_eq!(
1723 command.partial_spawn_cleanup_timeout,
1724 Some(Duration::from_secs(5))
1725 );
1726 let options = TerminationOptions::default()
1727 .with_grace(Duration::from_millis(100))
1728 .with_windows_exit_code(1067);
1729 assert_eq!(options.grace, Duration::from_millis(100));
1730 assert_eq!(options.windows_exit_code, 1067);
1731 }
1732
1733 #[test]
1734 fn zero_bound_partial_spawn_failure_still_reaps_the_child() {
1735 use std::{
1736 num::NonZeroU32,
1737 process::Command,
1738 sync::{
1739 atomic::{AtomicU32, Ordering},
1740 Arc,
1741 },
1742 thread,
1743 time::{Duration, Instant},
1744 };
1745 let command = if cfg!(windows) {
1746 let mut command = Command::new("powershell.exe");
1747 command.args([
1748 "-NoLogo",
1749 "-NoProfile",
1750 "-NonInteractive",
1751 "-Command",
1752 "Start-Sleep -Seconds 30",
1753 ]);
1754 command
1755 } else {
1756 let mut command = Command::new("/bin/sh");
1757 command.args(["-c", "exec sleep 30"]);
1758 command
1759 };
1760 let mut command = super::OwnedCommand::from_command(command);
1761 command.partial_spawn_cleanup_timeout(Duration::ZERO);
1762 let captured = Arc::new(AtomicU32::new(0));
1763 let started = Instant::now();
1764 super::cleanup_test_fault::arm();
1765 let error = command
1766 .spawn_inner(false, true, Some(Arc::clone(&captured)))
1767 .err()
1768 .unwrap();
1769 let detail = crate::RunnerFailure::from_io(&error).unwrap();
1770 assert_eq!(detail.phase(), crate::RunnerPhase::Cleanup);
1771 assert_eq!(detail.termination(), crate::LeaderTermination::Unconfirmed);
1772 assert!(started.elapsed() < Duration::from_secs(3));
1773 let pid = captured.load(Ordering::SeqCst);
1774 assert_ne!(pid, 0);
1775 let process = super::AdoptedProcess::new(NonZeroU32::new(pid).unwrap());
1776 let started = Instant::now();
1777 while process.is_running().unwrap() {
1778 assert!(started.elapsed() < Duration::from_secs(5));
1779 thread::sleep(Duration::from_millis(5));
1780 }
1781 }
1782
1783 #[test]
1784 fn owned_commands_hide_their_console_window_by_default() {
1785 assert!(super::OwnedCommand::new("x").windows_hidden);
1786 assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
1787 }
1788
1789 #[cfg(windows)]
1791 fn host_job() -> std::os::windows::io::OwnedHandle {
1792 use std::os::windows::io::FromRawHandle;
1793 use windows::Win32::System::JobObjects::{
1794 CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject,
1795 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
1796 };
1797 let job = unsafe { CreateJobObjectW(None, None) }.unwrap();
1798 let job_handle = unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(job.0 as _) };
1799 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
1800 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
1801 unsafe {
1802 SetInformationJobObject(
1803 job,
1804 JobObjectExtendedLimitInformation,
1805 &info as *const _ as *const _,
1806 std::mem::size_of_val(&info) as u32,
1807 )
1808 }
1809 .unwrap();
1810 job_handle
1811 }
1812
1813 #[cfg(windows)]
1814 fn in_job(pid: u32, job: &std::os::windows::io::OwnedHandle) -> bool {
1815 use std::os::windows::io::AsRawHandle;
1816 #[link(name = "kernel32")]
1817 unsafe extern "system" {
1818 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> *mut std::ffi::c_void;
1819 fn IsProcessInJob(
1820 process: *mut std::ffi::c_void,
1821 job: *mut std::ffi::c_void,
1822 result: *mut i32,
1823 ) -> i32;
1824 fn CloseHandle(handle: *mut std::ffi::c_void) -> i32;
1825 }
1826 unsafe {
1827 let process = OpenProcess(0x1000, 0, pid); assert!(!process.is_null());
1829 let mut inside = 0;
1830 assert_ne!(
1831 IsProcessInJob(process, job.as_raw_handle() as _, &mut inside),
1832 0
1833 );
1834 CloseHandle(process);
1835 inside != 0
1836 }
1837 }
1838
1839 #[cfg(windows)]
1840 #[test]
1841 fn children_join_the_callers_job_and_die_when_the_caller_closes_it() {
1842 use std::os::windows::io::AsHandle;
1843 use std::time::{Duration, Instant};
1844 let job = host_job();
1845 let sleeper = || {
1846 let mut command = super::OwnedCommand::new("powershell.exe");
1847 command.command_mut().args([
1848 "-NoLogo",
1849 "-NoProfile",
1850 "-NonInteractive",
1851 "-Command",
1852 "Start-Sleep -Seconds 60",
1853 ]);
1854 command.windows_job(job.as_handle()).unwrap();
1855 command.spawn().unwrap()
1856 };
1857 let (first, second) = (sleeper(), sleeper());
1860 let pids = [first.id(), second.id()];
1861 assert!(
1862 pids.iter().all(|pid| in_job(*pid, &job)),
1863 "children must be in the caller's job"
1864 );
1865
1866 drop(job);
1868 let deadline = Instant::now() + Duration::from_secs(5);
1869 for pid in pids {
1870 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1871 while process.is_running().unwrap_or(false) {
1872 assert!(
1873 Instant::now() < deadline,
1874 "child {pid} survived its caller's job"
1875 );
1876 std::thread::sleep(Duration::from_millis(20));
1877 }
1878 }
1879 drop((first, second));
1880 }
1881
1882 #[test]
1883 fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
1884 use std::{
1885 process::Command,
1886 sync::{
1887 atomic::{AtomicU32, Ordering},
1888 Arc,
1889 },
1890 thread,
1891 time::{Duration, Instant},
1892 };
1893
1894 let command = if cfg!(windows) {
1896 let mut command = Command::new("powershell.exe");
1898 command.args([
1899 "-NoLogo",
1900 "-NoProfile",
1901 "-NonInteractive",
1902 "-Command",
1903 "Start-Sleep -Seconds 60",
1904 ]);
1905 command
1906 } else {
1907 let mut command = Command::new("sleep");
1909 command.arg("60");
1910 command
1911 };
1912 let spawned_pid = Arc::new(AtomicU32::new(0));
1913
1914 let error = match super::spawn_for_test(command, Arc::clone(&spawned_pid)) {
1915 Ok(_) => panic!("injected wrapping failure must fail after spawning"),
1916 Err(error) => error,
1917 };
1918 let detail = crate::RunnerFailure::from_io(&error).expect("partial-spawn provenance");
1919 assert_eq!(detail.phase(), crate::RunnerPhase::Setup);
1920 assert_eq!(detail.termination(), crate::LeaderTermination::Unconfirmed);
1921 let pid = spawned_pid.load(Ordering::SeqCst);
1922 assert_ne!(pid, 0, "test must fail after the OS process was spawned");
1923 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1924 let deadline = Instant::now() + Duration::from_secs(5);
1925 while process.is_running().unwrap_or(false) {
1926 assert!(
1927 Instant::now() < deadline,
1928 "partially spawned process leaked"
1929 );
1930 thread::sleep(Duration::from_millis(20));
1931 }
1932 }
1933}