1use crate::process_handle::{lock_child, ProcessHandle};
2#[cfg(unix)]
3use crate::unix_child::{LeaderState, SignalOutcome, SignalTarget};
4use std::{
5 ffi::OsStr,
6 io,
7 num::NonZeroU32,
8 process::{ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus},
9 sync::{Arc, Mutex, MutexGuard},
10 thread,
11 time::{Duration, Instant, SystemTime},
12};
13#[cfg(unix)]
14type PlatformChild = crate::unix_child::UnixChild;
15#[cfg(windows)]
16type PlatformChild = crate::spawn_windows::WindowsChild;
17
18#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
19pub enum JobAssignmentMode {
20 #[default]
21 Strict,
22 BestEffort,
23}
24
25#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
26pub enum UnixContainment {
27 #[default]
28 ProcessGroup,
29 Session,
30}
31
32#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
42#[non_exhaustive]
43pub enum ProcessPriority {
44 #[default]
45 Normal,
46 BelowNormal,
47 Idle,
48}
49
50impl ProcessPriority {
51 #[cfg(unix)]
53 pub(crate) fn nice_floor(self) -> Option<libc::c_int> {
54 match self {
55 Self::Normal => None,
56 Self::BelowNormal => Some(10),
57 Self::Idle => Some(19),
58 }
59 }
60}
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub struct TerminationOptions {
64 pub grace: Duration,
66 pub windows_exit_code: u32,
68}
69impl Default for TerminationOptions {
70 fn default() -> Self {
71 Self {
72 grace: Duration::ZERO,
73 windows_exit_code: 1,
74 }
75 }
76}
77impl TerminationOptions {
78 pub fn with_grace(mut self, grace: Duration) -> Self {
79 self.grace = grace;
80 self
81 }
82 pub fn with_windows_exit_code(mut self, code: u32) -> Self {
83 self.windows_exit_code = code;
84 self
85 }
86}
87
88#[derive(Debug)]
90pub struct JobAssignmentFailure {
91 pub stage: &'static str,
92 pub error: io::Error,
93}
94impl JobAssignmentFailure {
95 pub fn raw_os_error(&self) -> Option<i32> {
96 self.error.raw_os_error()
97 }
98}
99
100#[cfg(windows)]
102pub type RawHandleOrFd = std::os::windows::io::RawHandle;
103#[cfg(unix)]
104pub type RawHandleOrFd = std::os::fd::RawFd;
105#[cfg(windows)]
106type InheritedValue = usize;
107#[cfg(unix)]
108type InheritedValue = RawHandleOrFd;
109
110#[derive(Debug)]
111pub struct OwnedCommand {
112 command: Command,
113 windows_hidden: bool,
114 caller_job: CallerJob,
115 job_assignment: JobAssignmentMode,
116 unix_containment: UnixContainment,
117 process_priority: ProcessPriority,
118 kill_on_parent_exit: bool,
119 partial_spawn_cleanup_timeout: Option<Duration>,
121 inherited_handles: Option<Vec<InheritedValue>>,
122 #[cfg(windows)]
123 windows_spawn_config: Option<crate::WindowsSpawnConfig>,
124 #[cfg(windows)]
125 windows_spawn_config_auto: bool,
126 env_cleared: bool,
127}
128#[cfg(windows)]
129type CallerJob = Option<std::os::windows::io::OwnedHandle>;
130#[cfg(not(windows))]
131type CallerJob = Option<std::convert::Infallible>;
132
133impl OwnedCommand {
134 pub fn new(program: impl AsRef<OsStr>) -> Self {
135 let command = Self::from_command(Command::new(program));
136 #[cfg(windows)]
137 let command = {
138 let mut command = command;
139 command.windows_spawn_config = Some(crate::WindowsSpawnConfig::default());
142 command.windows_spawn_config_auto = true;
143 command
144 };
145 command
146 }
147 pub fn from_command(command: Command) -> Self {
153 Self {
154 command,
155 windows_hidden: true,
156 caller_job: None,
157 job_assignment: JobAssignmentMode::Strict,
158 unix_containment: UnixContainment::ProcessGroup,
159 process_priority: ProcessPriority::Normal,
160 kill_on_parent_exit: false,
161 partial_spawn_cleanup_timeout: None,
162 inherited_handles: None,
163 #[cfg(windows)]
164 windows_spawn_config: None,
165 #[cfg(windows)]
166 windows_spawn_config_auto: false,
167 env_cleared: false,
168 }
169 }
170 pub fn command_mut(&mut self) -> &mut Command {
171 #[cfg(windows)]
172 if self.windows_spawn_config_auto {
173 self.windows_spawn_config = None;
175 }
176 &mut self.command
177 }
178 pub fn inherit_handles_only(
180 &mut self,
181 handles: impl IntoIterator<Item = RawHandleOrFd>,
182 ) -> &mut Self {
183 #[cfg(windows)]
184 let handles = handles.into_iter().map(|handle| handle as usize).collect();
185 #[cfg(unix)]
186 let handles = handles.into_iter().collect();
187 self.inherited_handles = Some(handles);
188 self
189 }
190 #[cfg(windows)]
194 pub fn windows_spawn_config(&mut self, config: crate::WindowsSpawnConfig) -> &mut Self {
195 self.windows_spawn_config = Some(config);
196 self.windows_spawn_config_auto = false;
197 self
198 }
199 pub fn env_allowlist<I, S>(&mut self, keys: I) -> &mut Self
201 where
202 I: IntoIterator<Item = S>,
203 S: AsRef<OsStr>,
204 {
205 let kept: Vec<_> = keys
206 .into_iter()
207 .filter_map(|k| std::env::var_os(k.as_ref()).map(|v| (k.as_ref().to_os_string(), v)))
208 .collect();
209 self.command.env_clear();
210 self.env_cleared = true;
211 for (k, v) in kept {
212 self.command.env(k, v);
213 }
214 self
215 }
216 pub fn env_allowlist_with_defaults<I, S>(&mut self, extra: I) -> &mut Self
220 where
221 I: IntoIterator<Item = S>,
222 S: AsRef<OsStr>,
223 {
224 let keys: Vec<std::ffi::OsString> = DEFAULT_ENV_ALLOWLIST
225 .iter()
226 .map(std::ffi::OsString::from)
227 .chain(extra.into_iter().map(|k| k.as_ref().to_os_string()))
228 .collect();
229 self.env_allowlist(keys)
230 }
231 pub fn env_is_cleared(&self) -> bool {
236 self.env_cleared
237 }
238 pub fn env_strip<I, S>(&mut self, keys: I) -> &mut Self
239 where
240 I: IntoIterator<Item = S>,
241 S: AsRef<OsStr>,
242 {
243 for k in keys {
244 self.command.env_remove(k);
245 }
246 self
247 }
248 pub fn windows_hide(&mut self) -> &mut Self {
250 self.windows_hidden = true;
251 self
252 }
253 #[cfg(windows)]
255 pub fn windows_job(
256 &mut self,
257 job: std::os::windows::io::BorrowedHandle<'_>,
258 ) -> io::Result<&mut Self> {
259 self.caller_job = Some(job.try_clone_to_owned()?);
260 Ok(self)
261 }
262 pub fn job_assignment_mode(&mut self, mode: JobAssignmentMode) -> &mut Self {
264 self.job_assignment = mode;
265 self
266 }
267 pub fn unix_containment(&mut self, mode: UnixContainment) -> &mut Self {
269 self.unix_containment = mode;
270 self
271 }
272 pub fn process_priority(&mut self, priority: ProcessPriority) -> &mut Self {
276 self.process_priority = priority;
277 self
278 }
279 pub fn kill_on_parent_exit(&mut self, enabled: bool) -> &mut Self {
291 self.kill_on_parent_exit = enabled;
292 self
293 }
294 pub fn partial_spawn_cleanup_timeout(&mut self, timeout: Duration) -> &mut Self {
296 self.partial_spawn_cleanup_timeout = Some(timeout);
297 self
298 }
299 pub fn spawn(self) -> io::Result<OwnedChild> {
300 self.spawn_inner(false, false, None)
301 }
302 pub fn spawn_uncontained_detached(self) -> io::Result<OwnedChild> {
306 self.spawn_inner(true, false, None)
307 }
308 fn spawn_inner(
309 mut self,
310 detached: bool,
311 fail_after_spawn: bool,
312 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
313 ) -> io::Result<OwnedChild> {
314 if detached && self.caller_job.is_some() {
315 return Err(io::Error::new(
316 io::ErrorKind::InvalidInput,
317 "detached launch cannot join a caller job",
318 ));
319 }
320 if detached && self.kill_on_parent_exit {
321 return Err(io::Error::new(
322 io::ErrorKind::InvalidInput,
323 "kill_on_parent_exit cannot combine with a detached launch",
324 ));
325 }
326 #[cfg(all(unix, not(target_os = "linux")))]
327 if self.kill_on_parent_exit {
328 return Err(io::Error::new(
329 io::ErrorKind::Unsupported,
330 "kill_on_parent_exit is supported on Linux only",
331 ));
332 }
333 #[cfg(unix)]
334 {
335 use std::os::unix::process::CommandExt;
336 let session = detached || self.unix_containment == UnixContainment::Session;
337 let nice_floor = self.process_priority.nice_floor();
338 let inheritance = self
339 .inherited_handles
340 .take()
341 .map(crate::process_unix::Inheritance::new)
342 .transpose()?;
343 #[cfg(target_os = "linux")]
345 let supervisor = self
346 .kill_on_parent_exit
347 .then(|| unsafe { libc::getpid() });
349 unsafe {
351 self.command.pre_exec(move || {
352 let result = if session {
353 libc::setsid()
354 } else {
355 libc::setpgid(0, 0)
356 };
357 if result < 0 {
358 return Err(io::Error::last_os_error());
359 }
360 if let Some(floor) = nice_floor {
361 crate::process_unix::raise_nice_to(floor)?;
362 }
363 if let Some(inheritance) = &inheritance {
364 inheritance.apply()?;
365 }
366 #[cfg(target_os = "linux")]
367 if let Some(supervisor) = supervisor {
368 crate::process_unix::die_with_parent(supervisor)?;
369 }
370 Ok(())
371 });
372 }
373 }
374 #[cfg(windows)]
375 crate::process_windows::configure(
376 &mut self.command,
377 detached,
378 self.windows_hidden,
379 self.process_priority,
380 );
381 #[cfg(unix)]
382 let _ = (self.windows_hidden, self.job_assignment);
383 #[cfg(windows)]
384 let _ = (self.unix_containment, self.kill_on_parent_exit);
385 let spawned_at = SystemTime::now();
386 #[cfg(unix)]
387 let child = {
388 let mut child = crate::unix_child::UnixChild::new(self.command.spawn()?);
389 child.require_identity()?;
392 child
393 };
394 #[cfg(windows)]
395 let child: PlatformChild = match self.inherited_handles.as_deref() {
396 None => {
397 let _spawning = crate::spawn_windows::spawn_guard();
400 self.command.spawn()?.into()
401 }
402 Some(handles) => {
403 let config = self.windows_spawn_config.take().ok_or_else(|| {
404 io::Error::new(io::ErrorKind::InvalidInput,
405 "allowlisted Windows spawn requires windows_spawn_config for opaque Command settings")
406 })?;
407 crate::spawn_windows::spawn(
408 &self.command,
409 config,
410 handles,
411 detached,
412 self.windows_hidden,
413 self.process_priority,
414 self.env_cleared,
415 self.partial_spawn_cleanup_timeout,
416 )?
417 }
418 };
419 let pid = child.id();
420 if let Some(captured) = captured_pid {
421 captured.store(pid, std::sync::atomic::Ordering::SeqCst);
422 }
423 let child = Arc::new(Mutex::new(child));
424 let handle = match ProcessHandle::new(&child, spawned_at) {
425 Ok(handle) => handle,
426 Err(error) => {
427 cleanup_direct(child, self.partial_spawn_cleanup_timeout);
428 return Err(error);
429 }
430 };
431 let mut partial = PartialChild {
432 child: Some(OwnedChild {
433 child,
434 handle,
435 pid,
436 detached,
437 terminate_on_drop: !detached,
438 drop_reap_timeout: DEFAULT_DROP_REAP_TIMEOUT,
439 tree_terminated: false,
440 assignment_failures: Vec::new(),
441 #[cfg(windows)]
442 job: None,
443 }),
444 timeout: self.partial_spawn_cleanup_timeout,
445 };
446 #[cfg(windows)]
447 if !detached {
448 let owned = partial.child.as_mut().expect("partial child is present");
449 crate::process_windows::assign_jobs(
450 &owned.handle,
451 self.caller_job.take(),
452 self.job_assignment,
453 &mut owned.job,
454 &mut owned.assignment_failures,
455 )?;
456 }
457 #[cfg(windows)]
458 partial
459 .child
460 .as_ref()
461 .expect("partial child is present")
462 .child()
463 .resume(detached)?;
464 if fail_after_spawn {
465 return Err(io::Error::other("injected post-spawn wrapping failure"));
466 }
467 Ok(partial.child.take().expect("partial child is present"))
468 }
469}
470
471pub const DEFAULT_DROP_REAP_TIMEOUT: Duration = Duration::from_secs(2);
474
475pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
476 "PATH",
477 "HOME",
478 "USER",
479 "LOGNAME",
480 "LANG",
481 "LC_ALL",
482 "TMPDIR",
483 "TEMP",
484 "TMP",
485 "SystemRoot",
486 "SYSTEMROOT",
487 "USERPROFILE",
488 "APPDATA",
489 "LOCALAPPDATA",
490 "ProgramData",
491 "COMSPEC",
492 "PATHEXT",
493 "XDG_DATA_HOME",
494 "XDG_RUNTIME_DIR",
495];
496
497#[derive(Debug)]
498pub struct OwnedChild {
499 child: Arc<Mutex<PlatformChild>>,
500 handle: ProcessHandle,
501 pid: u32,
503 detached: bool,
504 terminate_on_drop: bool,
505 drop_reap_timeout: Duration,
506 tree_terminated: bool,
507 assignment_failures: Vec<JobAssignmentFailure>,
508 #[cfg(windows)]
509 job: Option<crate::process_windows::KillOnCloseJob>,
510}
511#[derive(Clone, Copy, Debug, Eq, PartialEq)]
512pub enum ReapOutcome {
513 Exited(ExitStatus),
514 TimedOut,
515}
516#[derive(Clone, Copy, Debug, Eq, PartialEq)]
517pub enum WaitOutcome {
518 Exited(ExitStatus),
519 Terminated(ExitStatus),
520}
521
522impl OwnedChild {
523 pub fn id(&self) -> u32 {
524 self.pid
525 }
526 pub fn child(&self) -> MutexGuard<'_, PlatformChild> {
542 lock_child(&self.child)
543 }
544 pub fn while_unreaped<R>(&self, act: impl FnOnce(u32) -> R) -> io::Result<Option<R>> {
553 #[cfg(unix)]
554 {
555 let mut child = lock_child(&self.child);
556 if child.leader_state()? == LeaderState::Reaped {
557 return Ok(None);
558 }
559 Ok(Some(act(self.pid)))
560 }
561 #[cfg(windows)]
562 Ok(Some(act(self.pid)))
563 }
564 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
565 self.child().stdin.take()
566 }
567 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
568 self.child().stdout.take()
569 }
570 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
571 self.child().stderr.take()
572 }
573 pub fn job_assignment_failures(&self) -> &[JobAssignmentFailure] {
574 &self.assignment_failures
575 }
576 pub fn process_handle(&self) -> io::Result<ProcessHandle> {
580 self.handle.try_clone()
581 }
582 pub fn creation_time(&self) -> Option<SystemTime> {
583 self.handle.creation_time()
584 }
585 #[cfg(windows)]
586 pub fn creation_time_ticks(&self) -> Option<u64> {
587 self.handle.creation_time_ticks()
588 }
589 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
590 self.handle.try_wait()
592 }
593 pub fn wait(&mut self) -> io::Result<ExitStatus> {
596 drop(self.child().stdin.take());
597 self.handle.wait()
598 }
599 pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
600 self.handle.wait_timeout(timeout)
601 }
602 pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
612 self.wait_or_kill_inner(grace, true)
613 }
614 pub fn wait_then_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
620 self.wait_or_kill_inner(grace, false)
621 }
622 fn wait_or_kill_inner(&mut self, grace: Duration, term_first: bool) -> io::Result<WaitOutcome> {
623 #[cfg(not(unix))]
624 let _ = term_first;
625 #[cfg(unix)]
626 {
627 if term_first && !self.tree_terminated {
628 self.signal(libc::SIGTERM)?;
629 }
630 if self.handle.wait_exited_timeout(grace)? {
632 self.force_kill(1)?;
637 return self.wait().map(WaitOutcome::Exited);
638 }
639 self.terminate_tree().map(WaitOutcome::Terminated)
640 }
641 #[cfg(windows)]
642 {
643 if let Some(status) = self.wait_timeout(grace)? {
644 self.force_kill(1)?;
648 return Ok(WaitOutcome::Exited(status));
649 }
650 self.terminate_tree().map(WaitOutcome::Terminated)
651 }
652 }
653 pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
658 self.force_kill(1)?;
659 self.wait()
660 }
661 pub fn terminate_tree_with_options(
666 &mut self,
667 options: TerminationOptions,
668 ) -> io::Result<ExitStatus> {
669 self.start_termination(options)?;
670 self.wait()
671 }
672 pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
673 self.terminate_tree_bounded_with_options(timeout, TerminationOptions::default())
674 }
675 pub fn terminate_tree_bounded_with_options(
677 &mut self,
678 timeout: Duration,
679 mut options: TerminationOptions,
680 ) -> io::Result<ReapOutcome> {
681 let started = Instant::now();
682 options.grace = options.grace.min(timeout);
683 self.start_termination(options)?;
684 Ok(
685 match self.wait_timeout(timeout.saturating_sub(started.elapsed()))? {
686 Some(status) => ReapOutcome::Exited(status),
687 None => ReapOutcome::TimedOut,
688 },
689 )
690 }
691 fn start_termination(&mut self, options: TerminationOptions) -> io::Result<()> {
692 #[cfg(unix)]
693 if !self.tree_terminated {
694 self.signal(libc::SIGTERM)?;
695 self.await_tree(options.grace)?;
699 }
700 self.force_kill(options.windows_exit_code)
701 }
702 #[cfg(unix)]
709 fn await_tree(&self, grace: Duration) -> io::Result<()> {
710 use crate::{exit_watch::POLL_INTERVAL, unix_child::Members};
711 let Some(deadline) = Instant::now().checked_add(grace) else {
712 return Ok(());
713 };
714 let left = |deadline: Instant| deadline.saturating_duration_since(Instant::now());
715 if !self.handle.wait_exited_timeout(left(deadline))? {
716 return Ok(()); }
718 if self.detached {
719 return Ok(()); }
721 loop {
722 match lock_child(&self.child).group_other_members()? {
723 Members::Pids(pids) if pids.is_empty() => return Ok(()),
724 Members::Pids(_) => {}
725 Members::Unknown => return wait_out(left(deadline)),
726 }
727 let remaining = left(deadline);
728 if remaining.is_zero() {
729 return Ok(()); }
731 thread::sleep(POLL_INTERVAL.min(remaining));
732 }
733 }
734 #[cfg(unix)]
738 fn signal(&self, signal: i32) -> io::Result<()> {
739 let target = if self.detached {
742 SignalTarget::Leader
743 } else {
744 SignalTarget::Group
745 };
746 let started = Instant::now();
747 loop {
748 let (outcome, watch) = {
749 let mut child = lock_child(&self.child);
750 (child.signal(target, signal)?, child.exit_watch())
751 };
752 if outcome != SignalOutcome::MidExit {
753 return Ok(());
754 }
755 let remaining = MID_EXIT_RETRY.saturating_sub(started.elapsed());
758 if remaining.is_zero() {
759 return Err(io::Error::from_raw_os_error(libc::EPERM));
760 }
761 watch.pause(remaining);
762 }
763 }
764 fn force_kill(&mut self, code: u32) -> io::Result<()> {
765 if self.tree_terminated {
766 return Ok(());
767 }
768 #[cfg(unix)]
769 {
770 let _ = code;
771 self.signal(libc::SIGKILL)?;
772 }
773 #[cfg(windows)]
774 crate::process_windows::terminate(&self.handle, self.job.as_ref(), code)?;
775 self.tree_terminated = true;
776 Ok(())
777 }
778}
779impl OwnedChild {
780 pub fn set_drop_reap_timeout(&mut self, timeout: Duration) -> &mut Self {
787 self.drop_reap_timeout = timeout;
788 self
789 }
790 pub fn drop_reap_timeout(&self) -> Duration {
792 self.drop_reap_timeout
793 }
794 fn reap_in_background(&self) {
796 #[cfg(unix)]
797 if let Ok(reader) = self.handle.try_clone() {
798 let _ = thread::Builder::new()
799 .name("rightkit-drop-reap".into())
800 .spawn(move || {
801 let _ = reader.wait();
802 });
803 }
804 }
805 pub fn descendant_identities(&self) -> io::Result<Vec<crate::AdoptedIdentity>> {
809 let root = self.while_unreaped(|pid| match NonZeroU32::new(pid) {
810 Some(pid) => crate::AdoptedIdentity::capture(pid),
811 None => Ok(None),
812 })?;
813 match root {
814 Some(Ok(Some(root))) => root.descendants(),
815 Some(Err(error)) => Err(error),
816 _ => Ok(Vec::new()),
817 }
818 }
819}
820impl Drop for OwnedChild {
821 fn drop(&mut self) {
822 if self.terminate_on_drop {
823 if self.force_kill(1).is_ok() {
827 drop(self.child().stdin.take());
828 if !matches!(
829 self.handle.wait_timeout(self.drop_reap_timeout),
830 Ok(Some(_))
831 ) {
832 self.reap_in_background();
833 }
834 }
835 } else if self.detached {
836 #[cfg(unix)]
837 {
838 {
842 let mut child = self.child();
843 drop(child.stdin.take());
844 drop(child.stdout.take());
845 drop(child.stderr.take());
846 }
847 if let Ok(reader) = self.handle.try_clone() {
848 let _ = thread::Builder::new()
849 .name("rightkit-detached-reap".into())
850 .spawn(move || {
851 let _ = reader.wait();
852 });
853 }
854 }
855 }
856 }
857}
858
859struct PartialChild {
860 child: Option<OwnedChild>,
861 timeout: Option<Duration>,
862}
863impl Drop for PartialChild {
864 fn drop(&mut self) {
865 if let Some(mut child) = self.child.take() {
866 child.terminate_on_drop = false;
868 let _ = child.force_kill(1);
869 cleanup_direct(Arc::clone(&child.child), self.timeout);
870 }
871 }
872}
873#[cfg(unix)]
876const MID_EXIT_RETRY: Duration = Duration::from_millis(300);
877
878#[cfg(unix)]
881fn wait_out(remaining: Duration) -> io::Result<()> {
882 thread::sleep(remaining);
883 Ok(())
884}
885pub(crate) fn cleanup_direct(child: Arc<Mutex<PlatformChild>>, timeout: Option<Duration>) {
892 let _ = lock_child(&child).kill();
893 let Some(timeout) = timeout else {
894 let _ = lock_child(&child).wait();
896 return;
897 };
898 #[cfg(unix)]
899 {
900 let watch = lock_child(&child).exit_watch();
903 let deadline = Instant::now().checked_add(timeout);
904 if matches!(
905 watch.wait_for(deadline, || lock_child(&child).try_wait()),
906 Ok(Some(_))
907 ) {
908 return;
909 }
910 }
911 #[cfg(windows)]
912 {
913 let started = Instant::now();
914 loop {
915 match lock_child(&child).try_wait() {
916 Ok(Some(_)) => return,
917 Err(_) => break,
918 Ok(None) => {}
919 }
920 if started.elapsed() >= timeout {
921 break;
922 }
923 thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
924 }
925 }
926 let _ = thread::Builder::new()
930 .name("rightkit-partial-reap".into())
931 .spawn(move || {
932 let _ = lock_child(&child).wait();
933 });
934}
935
936#[derive(Clone, Copy, Debug, Eq, PartialEq)]
937pub struct AdoptedProcess {
938 pid: NonZeroU32,
939}
940impl AdoptedProcess {
941 pub fn new(pid: NonZeroU32) -> Self {
942 Self { pid }
943 }
944 pub fn id(&self) -> u32 {
945 self.pid.get()
946 }
947 pub fn is_running(&self) -> io::Result<bool> {
948 process_is_running(self.pid)
949 }
950 pub fn identity(&self) -> io::Result<Option<crate::AdoptedIdentity>> {
955 crate::AdoptedIdentity::capture(self.pid)
956 }
957}
958#[cfg(test)]
959fn spawn_for_test(
960 command: Command,
961 captured_pid: Arc<std::sync::atomic::AtomicU32>,
962) -> io::Result<OwnedChild> {
963 OwnedCommand::from_command(command).spawn_inner(false, true, Some(captured_pid))
964}
965
966#[cfg(windows)]
967fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
968 use windows::Win32::{
969 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
970 System::Threading::{
971 OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
972 PROCESS_SYNCHRONIZE,
973 },
974 };
975
976 let handle = unsafe {
977 OpenProcess(
978 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
979 false,
980 pid.get(),
981 )
982 };
983 let handle = match handle {
984 Ok(handle) => handle,
985 Err(error) => {
986 if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
987 return Ok(false);
988 }
989 return Err(io::Error::other(error.to_string()));
990 }
991 };
992 let wait = unsafe { WaitForSingleObject(handle, 0) };
993 let close = unsafe { CloseHandle(handle) };
994 close.map_err(|error| io::Error::other(error.to_string()))?;
995 match wait.0 {
996 0 => Ok(false),
997 value if value == WAIT_TIMEOUT.0 => Ok(true),
998 _ => Err(io::Error::last_os_error()),
999 }
1000}
1001
1002#[cfg(unix)]
1003fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1004 let pid = i32::try_from(pid.get()).map_err(io::Error::other)?;
1005 loop {
1006 if unsafe { libc::kill(pid, 0) } == 0 {
1008 return Ok(true);
1009 }
1010 let error = io::Error::last_os_error();
1011 match error.raw_os_error() {
1012 Some(libc::EINTR) => continue,
1013 Some(libc::EPERM) => return Ok(true),
1014 Some(libc::ESRCH) => return Ok(false),
1015 _ => return Err(error),
1016 }
1017 }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 #[test]
1023 fn option_defaults_preserve_existing_behavior() {
1024 let command = super::OwnedCommand::new("unused");
1025 assert_eq!(command.job_assignment, super::JobAssignmentMode::Strict);
1026 assert_eq!(
1027 command.unix_containment,
1028 super::UnixContainment::ProcessGroup
1029 );
1030 assert_eq!(command.partial_spawn_cleanup_timeout, None);
1031 assert_eq!(
1032 super::TerminationOptions::default().grace,
1033 std::time::Duration::ZERO
1034 );
1035 assert_eq!(super::TerminationOptions::default().windows_exit_code, 1);
1036 }
1037
1038 #[cfg(not(windows))]
1039 #[test]
1040 fn new_and_from_command_share_spawn_settings() {
1041 use super::OwnedCommand;
1042 use std::process::Command;
1043 let by_new = OwnedCommand::new("unused");
1045 let by_from = OwnedCommand::from_command(Command::new("unused"));
1046 assert_eq!(format!("{by_new:?}"), format!("{by_from:?}"));
1047 }
1048
1049 #[cfg(windows)]
1053 #[test]
1054 fn only_new_supplies_a_default_windows_spawn_config() {
1055 use super::OwnedCommand;
1056 use std::process::Command;
1057 let by_new = OwnedCommand::new("unused");
1058 let by_from = OwnedCommand::from_command(Command::new("unused"));
1059 assert!(by_new.windows_spawn_config.is_some() && by_new.windows_spawn_config_auto);
1060 assert!(by_from.windows_spawn_config.is_none() && !by_from.windows_spawn_config_auto);
1061 }
1062
1063 #[test]
1064 fn process_priority_defaults_to_normal_and_keeps_the_requested_value() {
1065 use super::{OwnedCommand, ProcessPriority};
1066 let mut command = OwnedCommand::new("unused");
1067 assert_eq!(command.process_priority, ProcessPriority::Normal);
1068 command.process_priority(ProcessPriority::BelowNormal);
1069 assert_eq!(command.process_priority, ProcessPriority::BelowNormal);
1070 command.process_priority(ProcessPriority::Idle);
1071 assert_eq!(command.process_priority, ProcessPriority::Idle);
1072 }
1073
1074 #[cfg(unix)]
1077 #[test]
1078 fn process_priority_maps_to_nice_floors() {
1079 use super::ProcessPriority;
1080 assert_eq!(ProcessPriority::Normal.nice_floor(), None);
1081 assert_eq!(ProcessPriority::BelowNormal.nice_floor(), Some(10));
1082 assert_eq!(ProcessPriority::Idle.nice_floor(), Some(19));
1083 }
1084
1085 #[cfg(windows)]
1087 #[test]
1088 fn below_normal_child_has_below_normal_priority_class() {
1089 use super::{OwnedCommand, ProcessPriority};
1090 use std::process::Stdio;
1091 use windows::Win32::{
1092 Foundation::CloseHandle,
1093 System::Threading::{
1094 GetPriorityClass, OpenProcess, BELOW_NORMAL_PRIORITY_CLASS,
1095 PROCESS_QUERY_LIMITED_INFORMATION,
1096 },
1097 };
1098 let mut command = OwnedCommand::new("ping");
1099 command
1100 .command_mut()
1101 .args(["-n", "6", "127.0.0.1"])
1102 .stdin(Stdio::null())
1103 .stdout(Stdio::null())
1104 .stderr(Stdio::null());
1105 command.process_priority(ProcessPriority::BelowNormal);
1106 let mut child = command.spawn().expect("spawn ping");
1107 let class = unsafe {
1109 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, child.id())
1110 .expect("open the child");
1111 let class = GetPriorityClass(handle);
1112 let _ = CloseHandle(handle);
1113 class
1114 };
1115 let _ = child.terminate_tree();
1116 assert_eq!(class, BELOW_NORMAL_PRIORITY_CLASS.0);
1117 }
1118
1119 #[cfg(unix)]
1121 #[test]
1122 fn lowered_priority_child_runs_at_or_above_the_nice_floor() {
1123 use super::{OwnedCommand, ProcessPriority};
1124 use std::{io::Read, process::Stdio};
1125 for (priority, floor) in [
1126 (ProcessPriority::BelowNormal, 10),
1127 (ProcessPriority::Idle, 19),
1128 ] {
1129 let mut command = OwnedCommand::new("/bin/sh");
1130 command
1131 .command_mut()
1132 .args(["-c", "ps -o ni= -p $$"])
1133 .stdin(Stdio::null())
1134 .stdout(Stdio::piped())
1135 .stderr(Stdio::null());
1136 command.process_priority(priority);
1137 let mut child = command.spawn().expect("spawn sh");
1138 let mut text = String::new();
1139 child
1140 .take_stdout()
1141 .expect("piped stdout")
1142 .read_to_string(&mut text)
1143 .expect("read ps output");
1144 let _ = child.wait();
1145 let nice: i32 = text.trim().parse().expect("ps printed a nice value");
1146 assert!(nice >= floor, "nice {nice} below floor {floor}");
1147 }
1148 }
1149
1150 #[test]
1151 fn kill_on_parent_exit_is_opt_in() {
1152 let mut command = super::OwnedCommand::new("unused");
1153 assert!(!command.kill_on_parent_exit);
1154 command.kill_on_parent_exit(true);
1155 assert!(command.kill_on_parent_exit);
1156 }
1157
1158 #[test]
1159 fn a_detached_spawn_rejects_kill_on_parent_exit() {
1160 let mut command = super::OwnedCommand::new("unused");
1162 command.kill_on_parent_exit(true);
1163 let Err(error) = command.spawn_uncontained_detached() else {
1164 panic!("a detached spawn must not accept the parent-death tie");
1165 };
1166 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1167 }
1168
1169 #[cfg(all(unix, not(target_os = "linux")))]
1172 #[test]
1173 fn a_host_without_a_parent_death_primitive_refuses_before_starting_anything() {
1174 let marker =
1175 std::env::temp_dir().join(format!("rightkit-process-untied-{}", std::process::id()));
1176 let _ = std::fs::remove_file(&marker);
1177 let mut command = super::OwnedCommand::new("/usr/bin/touch");
1178 command.command_mut().arg(&marker);
1179 command.kill_on_parent_exit(true);
1180 let Err(error) = command.spawn() else {
1181 panic!("kill_on_parent_exit must not run the child untied");
1182 };
1183 assert_eq!(error.kind(), std::io::ErrorKind::Unsupported);
1184 std::thread::sleep(std::time::Duration::from_millis(200));
1185 assert!(!marker.exists(), "the program ran");
1186 }
1187
1188 #[test]
1189 fn option_builders_keep_every_requested_value() {
1190 use super::{JobAssignmentMode, OwnedCommand, TerminationOptions, UnixContainment};
1191 use std::time::Duration;
1192 let mut command = OwnedCommand::new("unused");
1193 command
1194 .job_assignment_mode(JobAssignmentMode::BestEffort)
1195 .unix_containment(UnixContainment::Session)
1196 .partial_spawn_cleanup_timeout(Duration::from_secs(5));
1197 assert_eq!(command.job_assignment, JobAssignmentMode::BestEffort);
1198 assert_eq!(command.unix_containment, UnixContainment::Session);
1199 assert_eq!(
1200 command.partial_spawn_cleanup_timeout,
1201 Some(Duration::from_secs(5))
1202 );
1203 let options = TerminationOptions::default()
1204 .with_grace(Duration::from_millis(100))
1205 .with_windows_exit_code(1067);
1206 assert_eq!(options.grace, Duration::from_millis(100));
1207 assert_eq!(options.windows_exit_code, 1067);
1208 }
1209
1210 #[test]
1211 fn zero_bound_partial_spawn_failure_still_reaps_the_child() {
1212 use std::{
1213 num::NonZeroU32,
1214 process::Command,
1215 sync::{
1216 atomic::{AtomicU32, Ordering},
1217 Arc,
1218 },
1219 thread,
1220 time::{Duration, Instant},
1221 };
1222 let command = if cfg!(windows) {
1223 let mut command = Command::new("powershell.exe");
1224 command.args([
1225 "-NoLogo",
1226 "-NoProfile",
1227 "-NonInteractive",
1228 "-Command",
1229 "Start-Sleep -Seconds 30",
1230 ]);
1231 command
1232 } else {
1233 let mut command = Command::new("/bin/sh");
1234 command.args(["-c", "exec sleep 30"]);
1235 command
1236 };
1237 let mut command = super::OwnedCommand::from_command(command);
1238 command.partial_spawn_cleanup_timeout(Duration::ZERO);
1239 let captured = Arc::new(AtomicU32::new(0));
1240 let started = Instant::now();
1241 assert!(command
1242 .spawn_inner(false, true, Some(Arc::clone(&captured)))
1243 .is_err());
1244 assert!(started.elapsed() < Duration::from_secs(3));
1245 let pid = captured.load(Ordering::SeqCst);
1246 assert_ne!(pid, 0);
1247 let process = super::AdoptedProcess::new(NonZeroU32::new(pid).unwrap());
1248 let started = Instant::now();
1249 while process.is_running().unwrap() {
1250 assert!(started.elapsed() < Duration::from_secs(5));
1251 thread::sleep(Duration::from_millis(5));
1252 }
1253 }
1254
1255 #[test]
1256 fn owned_commands_hide_their_console_window_by_default() {
1257 assert!(super::OwnedCommand::new("x").windows_hidden);
1258 assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
1259 }
1260
1261 #[cfg(windows)]
1263 fn host_job() -> std::os::windows::io::OwnedHandle {
1264 use std::os::windows::io::FromRawHandle;
1265 use windows::Win32::System::JobObjects::{
1266 CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject,
1267 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
1268 };
1269 let job = unsafe { CreateJobObjectW(None, None) }.unwrap();
1270 let job_handle = unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(job.0 as _) };
1271 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
1272 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
1273 unsafe {
1274 SetInformationJobObject(
1275 job,
1276 JobObjectExtendedLimitInformation,
1277 &info as *const _ as *const _,
1278 std::mem::size_of_val(&info) as u32,
1279 )
1280 }
1281 .unwrap();
1282 job_handle
1283 }
1284
1285 #[cfg(windows)]
1286 fn in_job(pid: u32, job: &std::os::windows::io::OwnedHandle) -> bool {
1287 use std::os::windows::io::AsRawHandle;
1288 #[link(name = "kernel32")]
1289 unsafe extern "system" {
1290 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> *mut std::ffi::c_void;
1291 fn IsProcessInJob(
1292 process: *mut std::ffi::c_void,
1293 job: *mut std::ffi::c_void,
1294 result: *mut i32,
1295 ) -> i32;
1296 fn CloseHandle(handle: *mut std::ffi::c_void) -> i32;
1297 }
1298 unsafe {
1299 let process = OpenProcess(0x1000, 0, pid); assert!(!process.is_null());
1301 let mut inside = 0;
1302 assert_ne!(
1303 IsProcessInJob(process, job.as_raw_handle() as _, &mut inside),
1304 0
1305 );
1306 CloseHandle(process);
1307 inside != 0
1308 }
1309 }
1310
1311 #[cfg(windows)]
1312 #[test]
1313 fn children_join_the_callers_job_and_die_when_the_caller_closes_it() {
1314 use std::os::windows::io::AsHandle;
1315 use std::time::{Duration, Instant};
1316 let job = host_job();
1317 let sleeper = || {
1318 let mut command = super::OwnedCommand::new("powershell.exe");
1319 command.command_mut().args([
1320 "-NoLogo",
1321 "-NoProfile",
1322 "-NonInteractive",
1323 "-Command",
1324 "Start-Sleep -Seconds 60",
1325 ]);
1326 command.windows_job(job.as_handle()).unwrap();
1327 command.spawn().unwrap()
1328 };
1329 let (first, second) = (sleeper(), sleeper());
1332 let pids = [first.id(), second.id()];
1333 assert!(
1334 pids.iter().all(|pid| in_job(*pid, &job)),
1335 "children must be in the caller's job"
1336 );
1337
1338 drop(job);
1340 let deadline = Instant::now() + Duration::from_secs(5);
1341 for pid in pids {
1342 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1343 while process.is_running().unwrap_or(false) {
1344 assert!(
1345 Instant::now() < deadline,
1346 "child {pid} survived its caller's job"
1347 );
1348 std::thread::sleep(Duration::from_millis(20));
1349 }
1350 }
1351 drop((first, second));
1352 }
1353
1354 #[test]
1355 fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
1356 use std::{
1357 process::Command,
1358 sync::{
1359 atomic::{AtomicU32, Ordering},
1360 Arc,
1361 },
1362 thread,
1363 time::{Duration, Instant},
1364 };
1365
1366 let command = if cfg!(windows) {
1368 let mut command = Command::new("powershell.exe");
1370 command.args([
1371 "-NoLogo",
1372 "-NoProfile",
1373 "-NonInteractive",
1374 "-Command",
1375 "Start-Sleep -Seconds 60",
1376 ]);
1377 command
1378 } else {
1379 let mut command = Command::new("sleep");
1381 command.arg("60");
1382 command
1383 };
1384 let spawned_pid = Arc::new(AtomicU32::new(0));
1385
1386 assert!(super::spawn_for_test(command, Arc::clone(&spawned_pid)).is_err());
1387 let pid = spawned_pid.load(Ordering::SeqCst);
1388 assert_ne!(pid, 0, "test must fail after the OS process was spawned");
1389 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1390 let deadline = Instant::now() + Duration::from_secs(5);
1391 while process.is_running().unwrap_or(false) {
1392 assert!(
1393 Instant::now() < deadline,
1394 "partially spawned process leaked"
1395 );
1396 thread::sleep(Duration::from_millis(20));
1397 }
1398 }
1399}