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, Eq, PartialEq)]
33pub struct TerminationOptions {
34 pub grace: Duration,
36 pub windows_exit_code: u32,
38}
39impl Default for TerminationOptions {
40 fn default() -> Self {
41 Self {
42 grace: Duration::ZERO,
43 windows_exit_code: 1,
44 }
45 }
46}
47impl TerminationOptions {
48 pub fn with_grace(mut self, grace: Duration) -> Self {
49 self.grace = grace;
50 self
51 }
52 pub fn with_windows_exit_code(mut self, code: u32) -> Self {
53 self.windows_exit_code = code;
54 self
55 }
56}
57
58#[derive(Debug)]
60pub struct JobAssignmentFailure {
61 pub stage: &'static str,
62 pub error: io::Error,
63}
64impl JobAssignmentFailure {
65 pub fn raw_os_error(&self) -> Option<i32> {
66 self.error.raw_os_error()
67 }
68}
69
70#[cfg(windows)]
72pub type RawHandleOrFd = std::os::windows::io::RawHandle;
73#[cfg(unix)]
74pub type RawHandleOrFd = std::os::fd::RawFd;
75#[cfg(windows)]
76type InheritedValue = usize;
77#[cfg(unix)]
78type InheritedValue = RawHandleOrFd;
79
80#[derive(Debug)]
81pub struct OwnedCommand {
82 command: Command,
83 windows_hidden: bool,
84 caller_job: CallerJob,
85 job_assignment: JobAssignmentMode,
86 unix_containment: UnixContainment,
87 kill_on_parent_exit: bool,
88 partial_spawn_cleanup_timeout: Option<Duration>,
90 inherited_handles: Option<Vec<InheritedValue>>,
91 #[cfg(windows)]
92 windows_spawn_config: Option<crate::WindowsSpawnConfig>,
93 #[cfg(windows)]
94 windows_spawn_config_auto: bool,
95 env_cleared: bool,
96}
97#[cfg(windows)]
98type CallerJob = Option<std::os::windows::io::OwnedHandle>;
99#[cfg(not(windows))]
100type CallerJob = Option<std::convert::Infallible>;
101
102impl OwnedCommand {
103 pub fn new(program: impl AsRef<OsStr>) -> Self {
104 let command = Self::from_command(Command::new(program));
105 #[cfg(windows)]
106 let command = {
107 let mut command = command;
108 command.windows_spawn_config = Some(crate::WindowsSpawnConfig::default());
111 command.windows_spawn_config_auto = true;
112 command
113 };
114 command
115 }
116 pub fn from_command(command: Command) -> Self {
122 Self {
123 command,
124 windows_hidden: true,
125 caller_job: None,
126 job_assignment: JobAssignmentMode::Strict,
127 unix_containment: UnixContainment::ProcessGroup,
128 kill_on_parent_exit: false,
129 partial_spawn_cleanup_timeout: None,
130 inherited_handles: None,
131 #[cfg(windows)]
132 windows_spawn_config: None,
133 #[cfg(windows)]
134 windows_spawn_config_auto: false,
135 env_cleared: false,
136 }
137 }
138 pub fn command_mut(&mut self) -> &mut Command {
139 #[cfg(windows)]
140 if self.windows_spawn_config_auto {
141 self.windows_spawn_config = None;
143 }
144 &mut self.command
145 }
146 pub fn inherit_handles_only(
148 &mut self,
149 handles: impl IntoIterator<Item = RawHandleOrFd>,
150 ) -> &mut Self {
151 #[cfg(windows)]
152 let handles = handles.into_iter().map(|handle| handle as usize).collect();
153 #[cfg(unix)]
154 let handles = handles.into_iter().collect();
155 self.inherited_handles = Some(handles);
156 self
157 }
158 #[cfg(windows)]
162 pub fn windows_spawn_config(&mut self, config: crate::WindowsSpawnConfig) -> &mut Self {
163 self.windows_spawn_config = Some(config);
164 self.windows_spawn_config_auto = false;
165 self
166 }
167 pub fn env_allowlist<I, S>(&mut self, keys: I) -> &mut Self
169 where
170 I: IntoIterator<Item = S>,
171 S: AsRef<OsStr>,
172 {
173 let kept: Vec<_> = keys
174 .into_iter()
175 .filter_map(|k| std::env::var_os(k.as_ref()).map(|v| (k.as_ref().to_os_string(), v)))
176 .collect();
177 self.command.env_clear();
178 self.env_cleared = true;
179 for (k, v) in kept {
180 self.command.env(k, v);
181 }
182 self
183 }
184 pub fn env_allowlist_with_defaults<I, S>(&mut self, extra: I) -> &mut Self
188 where
189 I: IntoIterator<Item = S>,
190 S: AsRef<OsStr>,
191 {
192 let keys: Vec<std::ffi::OsString> = DEFAULT_ENV_ALLOWLIST
193 .iter()
194 .map(std::ffi::OsString::from)
195 .chain(extra.into_iter().map(|k| k.as_ref().to_os_string()))
196 .collect();
197 self.env_allowlist(keys)
198 }
199 pub fn env_is_cleared(&self) -> bool {
204 self.env_cleared
205 }
206 pub fn env_strip<I, S>(&mut self, keys: I) -> &mut Self
207 where
208 I: IntoIterator<Item = S>,
209 S: AsRef<OsStr>,
210 {
211 for k in keys {
212 self.command.env_remove(k);
213 }
214 self
215 }
216 pub fn windows_hide(&mut self) -> &mut Self {
218 self.windows_hidden = true;
219 self
220 }
221 #[cfg(windows)]
223 pub fn windows_job(
224 &mut self,
225 job: std::os::windows::io::BorrowedHandle<'_>,
226 ) -> io::Result<&mut Self> {
227 self.caller_job = Some(job.try_clone_to_owned()?);
228 Ok(self)
229 }
230 pub fn job_assignment_mode(&mut self, mode: JobAssignmentMode) -> &mut Self {
232 self.job_assignment = mode;
233 self
234 }
235 pub fn unix_containment(&mut self, mode: UnixContainment) -> &mut Self {
237 self.unix_containment = mode;
238 self
239 }
240 pub fn kill_on_parent_exit(&mut self, enabled: bool) -> &mut Self {
252 self.kill_on_parent_exit = enabled;
253 self
254 }
255 pub fn partial_spawn_cleanup_timeout(&mut self, timeout: Duration) -> &mut Self {
257 self.partial_spawn_cleanup_timeout = Some(timeout);
258 self
259 }
260 pub fn spawn(self) -> io::Result<OwnedChild> {
261 self.spawn_inner(false, false, None)
262 }
263 pub fn spawn_uncontained_detached(self) -> io::Result<OwnedChild> {
267 self.spawn_inner(true, false, None)
268 }
269 fn spawn_inner(
270 mut self,
271 detached: bool,
272 fail_after_spawn: bool,
273 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
274 ) -> io::Result<OwnedChild> {
275 if detached && self.caller_job.is_some() {
276 return Err(io::Error::new(
277 io::ErrorKind::InvalidInput,
278 "detached launch cannot join a caller job",
279 ));
280 }
281 if detached && self.kill_on_parent_exit {
282 return Err(io::Error::new(
283 io::ErrorKind::InvalidInput,
284 "kill_on_parent_exit cannot combine with a detached launch",
285 ));
286 }
287 #[cfg(all(unix, not(target_os = "linux")))]
288 if self.kill_on_parent_exit {
289 return Err(io::Error::new(
290 io::ErrorKind::Unsupported,
291 "kill_on_parent_exit is supported on Linux only",
292 ));
293 }
294 #[cfg(unix)]
295 {
296 use std::os::unix::process::CommandExt;
297 let session = detached || self.unix_containment == UnixContainment::Session;
298 let inheritance = self
299 .inherited_handles
300 .take()
301 .map(crate::process_unix::Inheritance::new)
302 .transpose()?;
303 #[cfg(target_os = "linux")]
305 let supervisor = self
306 .kill_on_parent_exit
307 .then(|| unsafe { libc::getpid() });
309 unsafe {
311 self.command.pre_exec(move || {
312 let result = if session {
313 libc::setsid()
314 } else {
315 libc::setpgid(0, 0)
316 };
317 if result < 0 {
318 return Err(io::Error::last_os_error());
319 }
320 if let Some(inheritance) = &inheritance {
321 inheritance.apply()?;
322 }
323 #[cfg(target_os = "linux")]
324 if let Some(supervisor) = supervisor {
325 crate::process_unix::die_with_parent(supervisor)?;
326 }
327 Ok(())
328 });
329 }
330 }
331 #[cfg(windows)]
332 crate::process_windows::configure(&mut self.command, detached, self.windows_hidden);
333 #[cfg(unix)]
334 let _ = (self.windows_hidden, self.job_assignment);
335 #[cfg(windows)]
336 let _ = (self.unix_containment, self.kill_on_parent_exit);
337 let spawned_at = SystemTime::now();
338 #[cfg(unix)]
339 let child = {
340 let mut child = crate::unix_child::UnixChild::new(self.command.spawn()?);
341 child.require_identity()?;
344 child
345 };
346 #[cfg(windows)]
347 let child: PlatformChild = match self.inherited_handles.as_deref() {
348 None => {
349 let _spawning = crate::spawn_windows::spawn_guard();
352 self.command.spawn()?.into()
353 }
354 Some(handles) => {
355 let config = self.windows_spawn_config.take().ok_or_else(|| {
356 io::Error::new(io::ErrorKind::InvalidInput,
357 "allowlisted Windows spawn requires windows_spawn_config for opaque Command settings")
358 })?;
359 crate::spawn_windows::spawn(
360 &self.command,
361 config,
362 handles,
363 detached,
364 self.windows_hidden,
365 self.env_cleared,
366 self.partial_spawn_cleanup_timeout,
367 )?
368 }
369 };
370 let pid = child.id();
371 if let Some(captured) = captured_pid {
372 captured.store(pid, std::sync::atomic::Ordering::SeqCst);
373 }
374 let child = Arc::new(Mutex::new(child));
375 let handle = match ProcessHandle::new(&child, spawned_at) {
376 Ok(handle) => handle,
377 Err(error) => {
378 cleanup_direct(child, self.partial_spawn_cleanup_timeout);
379 return Err(error);
380 }
381 };
382 let mut partial = PartialChild {
383 child: Some(OwnedChild {
384 child,
385 handle,
386 pid,
387 detached,
388 terminate_on_drop: !detached,
389 drop_reap_timeout: DEFAULT_DROP_REAP_TIMEOUT,
390 tree_terminated: false,
391 assignment_failures: Vec::new(),
392 #[cfg(windows)]
393 job: None,
394 }),
395 timeout: self.partial_spawn_cleanup_timeout,
396 };
397 #[cfg(windows)]
398 if !detached {
399 let owned = partial.child.as_mut().expect("partial child is present");
400 crate::process_windows::assign_jobs(
401 &owned.handle,
402 self.caller_job.take(),
403 self.job_assignment,
404 &mut owned.job,
405 &mut owned.assignment_failures,
406 )?;
407 }
408 #[cfg(windows)]
409 partial
410 .child
411 .as_ref()
412 .expect("partial child is present")
413 .child()
414 .resume(detached)?;
415 if fail_after_spawn {
416 return Err(io::Error::other("injected post-spawn wrapping failure"));
417 }
418 Ok(partial.child.take().expect("partial child is present"))
419 }
420}
421
422pub const DEFAULT_DROP_REAP_TIMEOUT: Duration = Duration::from_secs(2);
425
426pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
427 "PATH",
428 "HOME",
429 "USER",
430 "LOGNAME",
431 "LANG",
432 "LC_ALL",
433 "TMPDIR",
434 "TEMP",
435 "TMP",
436 "SystemRoot",
437 "SYSTEMROOT",
438 "USERPROFILE",
439 "APPDATA",
440 "LOCALAPPDATA",
441 "ProgramData",
442 "COMSPEC",
443 "PATHEXT",
444 "XDG_DATA_HOME",
445 "XDG_RUNTIME_DIR",
446];
447
448#[derive(Debug)]
449pub struct OwnedChild {
450 child: Arc<Mutex<PlatformChild>>,
451 handle: ProcessHandle,
452 pid: u32,
454 detached: bool,
455 terminate_on_drop: bool,
456 drop_reap_timeout: Duration,
457 tree_terminated: bool,
458 assignment_failures: Vec<JobAssignmentFailure>,
459 #[cfg(windows)]
460 job: Option<crate::process_windows::KillOnCloseJob>,
461}
462#[derive(Clone, Copy, Debug, Eq, PartialEq)]
463pub enum ReapOutcome {
464 Exited(ExitStatus),
465 TimedOut,
466}
467#[derive(Clone, Copy, Debug, Eq, PartialEq)]
468pub enum WaitOutcome {
469 Exited(ExitStatus),
470 Terminated(ExitStatus),
471}
472
473impl OwnedChild {
474 pub fn id(&self) -> u32 {
475 self.pid
476 }
477 pub fn child(&self) -> MutexGuard<'_, PlatformChild> {
493 lock_child(&self.child)
494 }
495 pub fn while_unreaped<R>(&self, act: impl FnOnce(u32) -> R) -> io::Result<Option<R>> {
504 #[cfg(unix)]
505 {
506 let mut child = lock_child(&self.child);
507 if child.leader_state()? == LeaderState::Reaped {
508 return Ok(None);
509 }
510 Ok(Some(act(self.pid)))
511 }
512 #[cfg(windows)]
513 Ok(Some(act(self.pid)))
514 }
515 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
516 self.child().stdin.take()
517 }
518 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
519 self.child().stdout.take()
520 }
521 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
522 self.child().stderr.take()
523 }
524 pub fn job_assignment_failures(&self) -> &[JobAssignmentFailure] {
525 &self.assignment_failures
526 }
527 pub fn process_handle(&self) -> io::Result<ProcessHandle> {
531 self.handle.try_clone()
532 }
533 pub fn creation_time(&self) -> Option<SystemTime> {
534 self.handle.creation_time()
535 }
536 #[cfg(windows)]
537 pub fn creation_time_ticks(&self) -> Option<u64> {
538 self.handle.creation_time_ticks()
539 }
540 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
541 self.handle.try_wait()
543 }
544 pub fn wait(&mut self) -> io::Result<ExitStatus> {
547 drop(self.child().stdin.take());
548 self.handle.wait()
549 }
550 pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
551 self.handle.wait_timeout(timeout)
552 }
553 pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
563 #[cfg(unix)]
564 {
565 if !self.tree_terminated {
566 self.signal(libc::SIGTERM)?;
567 }
568 if self.handle.wait_exited_timeout(grace)? {
570 self.force_kill(1)?;
575 return self.wait().map(WaitOutcome::Exited);
576 }
577 self.terminate_tree().map(WaitOutcome::Terminated)
578 }
579 #[cfg(windows)]
580 {
581 if let Some(status) = self.wait_timeout(grace)? {
582 self.force_kill(1)?;
586 return Ok(WaitOutcome::Exited(status));
587 }
588 self.terminate_tree().map(WaitOutcome::Terminated)
589 }
590 }
591 pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
596 self.force_kill(1)?;
597 self.wait()
598 }
599 pub fn terminate_tree_with_options(
604 &mut self,
605 options: TerminationOptions,
606 ) -> io::Result<ExitStatus> {
607 self.start_termination(options)?;
608 self.wait()
609 }
610 pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
611 self.terminate_tree_bounded_with_options(timeout, TerminationOptions::default())
612 }
613 pub fn terminate_tree_bounded_with_options(
615 &mut self,
616 timeout: Duration,
617 mut options: TerminationOptions,
618 ) -> io::Result<ReapOutcome> {
619 let started = Instant::now();
620 options.grace = options.grace.min(timeout);
621 self.start_termination(options)?;
622 Ok(
623 match self.wait_timeout(timeout.saturating_sub(started.elapsed()))? {
624 Some(status) => ReapOutcome::Exited(status),
625 None => ReapOutcome::TimedOut,
626 },
627 )
628 }
629 fn start_termination(&mut self, options: TerminationOptions) -> io::Result<()> {
630 #[cfg(unix)]
631 if !self.tree_terminated {
632 self.signal(libc::SIGTERM)?;
633 self.await_tree(options.grace)?;
637 }
638 self.force_kill(options.windows_exit_code)
639 }
640 #[cfg(unix)]
647 fn await_tree(&self, grace: Duration) -> io::Result<()> {
648 use crate::{exit_watch::POLL_INTERVAL, unix_child::Members};
649 let Some(deadline) = Instant::now().checked_add(grace) else {
650 return Ok(());
651 };
652 let left = |deadline: Instant| deadline.saturating_duration_since(Instant::now());
653 if !self.handle.wait_exited_timeout(left(deadline))? {
654 return Ok(()); }
656 if self.detached {
657 return Ok(()); }
659 loop {
660 match lock_child(&self.child).group_other_members()? {
661 Members::Pids(pids) if pids.is_empty() => return Ok(()),
662 Members::Pids(_) => {}
663 Members::Unknown => return wait_out(left(deadline)),
664 }
665 let remaining = left(deadline);
666 if remaining.is_zero() {
667 return Ok(()); }
669 thread::sleep(POLL_INTERVAL.min(remaining));
670 }
671 }
672 #[cfg(unix)]
676 fn signal(&self, signal: i32) -> io::Result<()> {
677 let target = if self.detached {
680 SignalTarget::Leader
681 } else {
682 SignalTarget::Group
683 };
684 let started = Instant::now();
685 loop {
686 let (outcome, watch) = {
687 let mut child = lock_child(&self.child);
688 (child.signal(target, signal)?, child.exit_watch())
689 };
690 if outcome != SignalOutcome::MidExit {
691 return Ok(());
692 }
693 let remaining = MID_EXIT_RETRY.saturating_sub(started.elapsed());
696 if remaining.is_zero() {
697 return Err(io::Error::from_raw_os_error(libc::EPERM));
698 }
699 watch.pause(remaining);
700 }
701 }
702 fn force_kill(&mut self, code: u32) -> io::Result<()> {
703 if self.tree_terminated {
704 return Ok(());
705 }
706 #[cfg(unix)]
707 {
708 let _ = code;
709 self.signal(libc::SIGKILL)?;
710 }
711 #[cfg(windows)]
712 crate::process_windows::terminate(&self.handle, self.job.as_ref(), code)?;
713 self.tree_terminated = true;
714 Ok(())
715 }
716}
717impl OwnedChild {
718 pub fn set_drop_reap_timeout(&mut self, timeout: Duration) -> &mut Self {
725 self.drop_reap_timeout = timeout;
726 self
727 }
728 pub fn drop_reap_timeout(&self) -> Duration {
730 self.drop_reap_timeout
731 }
732 fn reap_in_background(&self) {
734 #[cfg(unix)]
735 if let Ok(reader) = self.handle.try_clone() {
736 let _ = thread::Builder::new()
737 .name("rightkit-drop-reap".into())
738 .spawn(move || {
739 let _ = reader.wait();
740 });
741 }
742 }
743 pub fn descendant_identities(&self) -> io::Result<Vec<crate::AdoptedIdentity>> {
747 let root = self.while_unreaped(|pid| match NonZeroU32::new(pid) {
748 Some(pid) => crate::AdoptedIdentity::capture(pid),
749 None => Ok(None),
750 })?;
751 match root {
752 Some(Ok(Some(root))) => root.descendants(),
753 Some(Err(error)) => Err(error),
754 _ => Ok(Vec::new()),
755 }
756 }
757}
758impl Drop for OwnedChild {
759 fn drop(&mut self) {
760 if self.terminate_on_drop {
761 if self.force_kill(1).is_ok() {
765 drop(self.child().stdin.take());
766 if !matches!(
767 self.handle.wait_timeout(self.drop_reap_timeout),
768 Ok(Some(_))
769 ) {
770 self.reap_in_background();
771 }
772 }
773 } else if self.detached {
774 #[cfg(unix)]
775 {
776 {
780 let mut child = self.child();
781 drop(child.stdin.take());
782 drop(child.stdout.take());
783 drop(child.stderr.take());
784 }
785 if let Ok(reader) = self.handle.try_clone() {
786 let _ = thread::Builder::new()
787 .name("rightkit-detached-reap".into())
788 .spawn(move || {
789 let _ = reader.wait();
790 });
791 }
792 }
793 }
794 }
795}
796
797struct PartialChild {
798 child: Option<OwnedChild>,
799 timeout: Option<Duration>,
800}
801impl Drop for PartialChild {
802 fn drop(&mut self) {
803 if let Some(mut child) = self.child.take() {
804 child.terminate_on_drop = false;
806 let _ = child.force_kill(1);
807 cleanup_direct(Arc::clone(&child.child), self.timeout);
808 }
809 }
810}
811#[cfg(unix)]
814const MID_EXIT_RETRY: Duration = Duration::from_millis(300);
815
816#[cfg(unix)]
819fn wait_out(remaining: Duration) -> io::Result<()> {
820 thread::sleep(remaining);
821 Ok(())
822}
823pub(crate) fn cleanup_direct(child: Arc<Mutex<PlatformChild>>, timeout: Option<Duration>) {
830 let _ = lock_child(&child).kill();
831 let Some(timeout) = timeout else {
832 let _ = lock_child(&child).wait();
834 return;
835 };
836 #[cfg(unix)]
837 {
838 let watch = lock_child(&child).exit_watch();
841 let deadline = Instant::now().checked_add(timeout);
842 if matches!(
843 watch.wait_for(deadline, || lock_child(&child).try_wait()),
844 Ok(Some(_))
845 ) {
846 return;
847 }
848 }
849 #[cfg(windows)]
850 {
851 let started = Instant::now();
852 loop {
853 match lock_child(&child).try_wait() {
854 Ok(Some(_)) => return,
855 Err(_) => break,
856 Ok(None) => {}
857 }
858 if started.elapsed() >= timeout {
859 break;
860 }
861 thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
862 }
863 }
864 let _ = thread::Builder::new()
868 .name("rightkit-partial-reap".into())
869 .spawn(move || {
870 let _ = lock_child(&child).wait();
871 });
872}
873
874#[derive(Clone, Copy, Debug, Eq, PartialEq)]
875pub struct AdoptedProcess {
876 pid: NonZeroU32,
877}
878impl AdoptedProcess {
879 pub fn new(pid: NonZeroU32) -> Self {
880 Self { pid }
881 }
882 pub fn id(&self) -> u32 {
883 self.pid.get()
884 }
885 pub fn is_running(&self) -> io::Result<bool> {
886 process_is_running(self.pid)
887 }
888 pub fn identity(&self) -> io::Result<Option<crate::AdoptedIdentity>> {
893 crate::AdoptedIdentity::capture(self.pid)
894 }
895}
896#[cfg(test)]
897fn spawn_for_test(
898 command: Command,
899 captured_pid: Arc<std::sync::atomic::AtomicU32>,
900) -> io::Result<OwnedChild> {
901 OwnedCommand::from_command(command).spawn_inner(false, true, Some(captured_pid))
902}
903
904#[cfg(windows)]
905fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
906 use windows::Win32::{
907 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
908 System::Threading::{
909 OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
910 PROCESS_SYNCHRONIZE,
911 },
912 };
913
914 let handle = unsafe {
915 OpenProcess(
916 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
917 false,
918 pid.get(),
919 )
920 };
921 let handle = match handle {
922 Ok(handle) => handle,
923 Err(error) => {
924 if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
925 return Ok(false);
926 }
927 return Err(io::Error::other(error.to_string()));
928 }
929 };
930 let wait = unsafe { WaitForSingleObject(handle, 0) };
931 let close = unsafe { CloseHandle(handle) };
932 close.map_err(|error| io::Error::other(error.to_string()))?;
933 match wait.0 {
934 0 => Ok(false),
935 value if value == WAIT_TIMEOUT.0 => Ok(true),
936 _ => Err(io::Error::last_os_error()),
937 }
938}
939
940#[cfg(unix)]
941fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
942 let pid = i32::try_from(pid.get()).map_err(io::Error::other)?;
943 loop {
944 if unsafe { libc::kill(pid, 0) } == 0 {
946 return Ok(true);
947 }
948 let error = io::Error::last_os_error();
949 match error.raw_os_error() {
950 Some(libc::EINTR) => continue,
951 Some(libc::EPERM) => return Ok(true),
952 Some(libc::ESRCH) => return Ok(false),
953 _ => return Err(error),
954 }
955 }
956}
957
958#[cfg(test)]
959mod tests {
960 #[test]
961 fn option_defaults_preserve_existing_behavior() {
962 let command = super::OwnedCommand::new("unused");
963 assert_eq!(command.job_assignment, super::JobAssignmentMode::Strict);
964 assert_eq!(
965 command.unix_containment,
966 super::UnixContainment::ProcessGroup
967 );
968 assert_eq!(command.partial_spawn_cleanup_timeout, None);
969 assert_eq!(
970 super::TerminationOptions::default().grace,
971 std::time::Duration::ZERO
972 );
973 assert_eq!(super::TerminationOptions::default().windows_exit_code, 1);
974 }
975
976 #[cfg(not(windows))]
977 #[test]
978 fn new_and_from_command_share_spawn_settings() {
979 use super::OwnedCommand;
980 use std::process::Command;
981 let by_new = OwnedCommand::new("unused");
983 let by_from = OwnedCommand::from_command(Command::new("unused"));
984 assert_eq!(format!("{by_new:?}"), format!("{by_from:?}"));
985 }
986
987 #[cfg(windows)]
991 #[test]
992 fn only_new_supplies_a_default_windows_spawn_config() {
993 use super::OwnedCommand;
994 use std::process::Command;
995 let by_new = OwnedCommand::new("unused");
996 let by_from = OwnedCommand::from_command(Command::new("unused"));
997 assert!(by_new.windows_spawn_config.is_some() && by_new.windows_spawn_config_auto);
998 assert!(by_from.windows_spawn_config.is_none() && !by_from.windows_spawn_config_auto);
999 }
1000
1001 #[test]
1002 fn kill_on_parent_exit_is_opt_in() {
1003 let mut command = super::OwnedCommand::new("unused");
1004 assert!(!command.kill_on_parent_exit);
1005 command.kill_on_parent_exit(true);
1006 assert!(command.kill_on_parent_exit);
1007 }
1008
1009 #[test]
1010 fn a_detached_spawn_rejects_kill_on_parent_exit() {
1011 let mut command = super::OwnedCommand::new("unused");
1013 command.kill_on_parent_exit(true);
1014 let Err(error) = command.spawn_uncontained_detached() else {
1015 panic!("a detached spawn must not accept the parent-death tie");
1016 };
1017 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1018 }
1019
1020 #[cfg(all(unix, not(target_os = "linux")))]
1023 #[test]
1024 fn a_host_without_a_parent_death_primitive_refuses_before_starting_anything() {
1025 let marker =
1026 std::env::temp_dir().join(format!("rightkit-process-untied-{}", std::process::id()));
1027 let _ = std::fs::remove_file(&marker);
1028 let mut command = super::OwnedCommand::new("/usr/bin/touch");
1029 command.command_mut().arg(&marker);
1030 command.kill_on_parent_exit(true);
1031 let Err(error) = command.spawn() else {
1032 panic!("kill_on_parent_exit must not run the child untied");
1033 };
1034 assert_eq!(error.kind(), std::io::ErrorKind::Unsupported);
1035 std::thread::sleep(std::time::Duration::from_millis(200));
1036 assert!(!marker.exists(), "the program ran");
1037 }
1038
1039 #[test]
1040 fn option_builders_keep_every_requested_value() {
1041 use super::{JobAssignmentMode, OwnedCommand, TerminationOptions, UnixContainment};
1042 use std::time::Duration;
1043 let mut command = OwnedCommand::new("unused");
1044 command
1045 .job_assignment_mode(JobAssignmentMode::BestEffort)
1046 .unix_containment(UnixContainment::Session)
1047 .partial_spawn_cleanup_timeout(Duration::from_secs(5));
1048 assert_eq!(command.job_assignment, JobAssignmentMode::BestEffort);
1049 assert_eq!(command.unix_containment, UnixContainment::Session);
1050 assert_eq!(
1051 command.partial_spawn_cleanup_timeout,
1052 Some(Duration::from_secs(5))
1053 );
1054 let options = TerminationOptions::default()
1055 .with_grace(Duration::from_millis(100))
1056 .with_windows_exit_code(1067);
1057 assert_eq!(options.grace, Duration::from_millis(100));
1058 assert_eq!(options.windows_exit_code, 1067);
1059 }
1060
1061 #[test]
1062 fn zero_bound_partial_spawn_failure_still_reaps_the_child() {
1063 use std::{
1064 num::NonZeroU32,
1065 process::Command,
1066 sync::{
1067 atomic::{AtomicU32, Ordering},
1068 Arc,
1069 },
1070 thread,
1071 time::{Duration, Instant},
1072 };
1073 let command = if cfg!(windows) {
1074 let mut command = Command::new("powershell.exe");
1075 command.args([
1076 "-NoLogo",
1077 "-NoProfile",
1078 "-NonInteractive",
1079 "-Command",
1080 "Start-Sleep -Seconds 30",
1081 ]);
1082 command
1083 } else {
1084 let mut command = Command::new("/bin/sh");
1085 command.args(["-c", "exec sleep 30"]);
1086 command
1087 };
1088 let mut command = super::OwnedCommand::from_command(command);
1089 command.partial_spawn_cleanup_timeout(Duration::ZERO);
1090 let captured = Arc::new(AtomicU32::new(0));
1091 let started = Instant::now();
1092 assert!(command
1093 .spawn_inner(false, true, Some(Arc::clone(&captured)))
1094 .is_err());
1095 assert!(started.elapsed() < Duration::from_secs(3));
1096 let pid = captured.load(Ordering::SeqCst);
1097 assert_ne!(pid, 0);
1098 let process = super::AdoptedProcess::new(NonZeroU32::new(pid).unwrap());
1099 let started = Instant::now();
1100 while process.is_running().unwrap() {
1101 assert!(started.elapsed() < Duration::from_secs(5));
1102 thread::sleep(Duration::from_millis(5));
1103 }
1104 }
1105
1106 #[test]
1107 fn owned_commands_hide_their_console_window_by_default() {
1108 assert!(super::OwnedCommand::new("x").windows_hidden);
1109 assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
1110 }
1111
1112 #[cfg(windows)]
1114 fn host_job() -> std::os::windows::io::OwnedHandle {
1115 use std::os::windows::io::FromRawHandle;
1116 use windows::Win32::System::JobObjects::{
1117 CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject,
1118 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
1119 };
1120 let job = unsafe { CreateJobObjectW(None, None) }.unwrap();
1121 let job_handle = unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(job.0 as _) };
1122 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
1123 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
1124 unsafe {
1125 SetInformationJobObject(
1126 job,
1127 JobObjectExtendedLimitInformation,
1128 &info as *const _ as *const _,
1129 std::mem::size_of_val(&info) as u32,
1130 )
1131 }
1132 .unwrap();
1133 job_handle
1134 }
1135
1136 #[cfg(windows)]
1137 fn in_job(pid: u32, job: &std::os::windows::io::OwnedHandle) -> bool {
1138 use std::os::windows::io::AsRawHandle;
1139 #[link(name = "kernel32")]
1140 unsafe extern "system" {
1141 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> *mut std::ffi::c_void;
1142 fn IsProcessInJob(
1143 process: *mut std::ffi::c_void,
1144 job: *mut std::ffi::c_void,
1145 result: *mut i32,
1146 ) -> i32;
1147 fn CloseHandle(handle: *mut std::ffi::c_void) -> i32;
1148 }
1149 unsafe {
1150 let process = OpenProcess(0x1000, 0, pid); assert!(!process.is_null());
1152 let mut inside = 0;
1153 assert_ne!(
1154 IsProcessInJob(process, job.as_raw_handle() as _, &mut inside),
1155 0
1156 );
1157 CloseHandle(process);
1158 inside != 0
1159 }
1160 }
1161
1162 #[cfg(windows)]
1163 #[test]
1164 fn children_join_the_callers_job_and_die_when_the_caller_closes_it() {
1165 use std::os::windows::io::AsHandle;
1166 use std::time::{Duration, Instant};
1167 let job = host_job();
1168 let sleeper = || {
1169 let mut command = super::OwnedCommand::new("powershell.exe");
1170 command.command_mut().args([
1171 "-NoLogo",
1172 "-NoProfile",
1173 "-NonInteractive",
1174 "-Command",
1175 "Start-Sleep -Seconds 60",
1176 ]);
1177 command.windows_job(job.as_handle()).unwrap();
1178 command.spawn().unwrap()
1179 };
1180 let (first, second) = (sleeper(), sleeper());
1183 let pids = [first.id(), second.id()];
1184 assert!(
1185 pids.iter().all(|pid| in_job(*pid, &job)),
1186 "children must be in the caller's job"
1187 );
1188
1189 drop(job);
1191 let deadline = Instant::now() + Duration::from_secs(5);
1192 for pid in pids {
1193 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1194 while process.is_running().unwrap_or(false) {
1195 assert!(
1196 Instant::now() < deadline,
1197 "child {pid} survived its caller's job"
1198 );
1199 std::thread::sleep(Duration::from_millis(20));
1200 }
1201 }
1202 drop((first, second));
1203 }
1204
1205 #[test]
1206 fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
1207 use std::{
1208 process::Command,
1209 sync::{
1210 atomic::{AtomicU32, Ordering},
1211 Arc,
1212 },
1213 thread,
1214 time::{Duration, Instant},
1215 };
1216
1217 let command = if cfg!(windows) {
1219 let mut command = Command::new("powershell.exe");
1221 command.args([
1222 "-NoLogo",
1223 "-NoProfile",
1224 "-NonInteractive",
1225 "-Command",
1226 "Start-Sleep -Seconds 60",
1227 ]);
1228 command
1229 } else {
1230 let mut command = Command::new("sleep");
1232 command.arg("60");
1233 command
1234 };
1235 let spawned_pid = Arc::new(AtomicU32::new(0));
1236
1237 assert!(super::spawn_for_test(command, Arc::clone(&spawned_pid)).is_err());
1238 let pid = spawned_pid.load(Ordering::SeqCst);
1239 assert_ne!(pid, 0, "test must fail after the OS process was spawned");
1240 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1241 let deadline = Instant::now() + Duration::from_secs(5);
1242 while process.is_running().unwrap_or(false) {
1243 assert!(
1244 Instant::now() < deadline,
1245 "partially spawned process leaked"
1246 );
1247 thread::sleep(Duration::from_millis(20));
1248 }
1249 }
1250}