1use crate::process_handle::{lock_child, ProcessHandle};
2use std::{
3 ffi::OsStr,
4 io,
5 num::NonZeroU32,
6 process::{ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus},
7 sync::{Arc, Mutex, MutexGuard},
8 thread,
9 time::{Duration, Instant, SystemTime},
10};
11#[cfg(unix)]
12type PlatformChild = std::process::Child;
13#[cfg(windows)]
14type PlatformChild = crate::spawn_windows::WindowsChild;
15
16#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
17pub enum JobAssignmentMode {
18 #[default]
19 Strict,
20 BestEffort,
21}
22
23#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
24pub enum UnixContainment {
25 #[default]
26 ProcessGroup,
27 Session,
28}
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub struct TerminationOptions {
32 pub grace: Duration,
34 pub windows_exit_code: u32,
36}
37impl Default for TerminationOptions {
38 fn default() -> Self {
39 Self {
40 grace: Duration::ZERO,
41 windows_exit_code: 1,
42 }
43 }
44}
45impl TerminationOptions {
46 pub fn with_grace(mut self, grace: Duration) -> Self {
47 self.grace = grace;
48 self
49 }
50 pub fn with_windows_exit_code(mut self, code: u32) -> Self {
51 self.windows_exit_code = code;
52 self
53 }
54}
55
56#[derive(Debug)]
58pub struct JobAssignmentFailure {
59 pub stage: &'static str,
60 pub error: io::Error,
61}
62impl JobAssignmentFailure {
63 pub fn raw_os_error(&self) -> Option<i32> {
64 self.error.raw_os_error()
65 }
66}
67
68#[cfg(windows)]
70pub type RawHandleOrFd = std::os::windows::io::RawHandle;
71#[cfg(unix)]
72pub type RawHandleOrFd = std::os::fd::RawFd;
73#[cfg(windows)]
74type InheritedValue = usize;
75#[cfg(unix)]
76type InheritedValue = RawHandleOrFd;
77
78#[derive(Debug)]
79pub struct OwnedCommand {
80 command: Command,
81 windows_hidden: bool,
82 caller_job: CallerJob,
83 job_assignment: JobAssignmentMode,
84 unix_containment: UnixContainment,
85 partial_spawn_cleanup_timeout: Option<Duration>,
87 inherited_handles: Option<Vec<InheritedValue>>,
88 #[cfg(windows)]
89 windows_spawn_config: Option<crate::WindowsSpawnConfig>,
90 #[cfg(windows)]
91 windows_spawn_config_auto: bool,
92 #[cfg(windows)]
93 env_cleared: bool,
94}
95#[cfg(windows)]
96type CallerJob = Option<std::os::windows::io::OwnedHandle>;
97#[cfg(not(windows))]
98type CallerJob = Option<std::convert::Infallible>;
99
100impl OwnedCommand {
101 pub fn new(program: impl AsRef<OsStr>) -> Self {
102 let command = Self::from_command(Command::new(program));
103 #[cfg(windows)]
104 let command = {
105 let mut command = command;
106 command.windows_spawn_config = Some(crate::WindowsSpawnConfig::default());
108 command.windows_spawn_config_auto = true;
109 command
110 };
111 command
112 }
113 pub fn from_command(command: Command) -> Self {
115 Self {
116 command,
117 windows_hidden: true,
118 caller_job: None,
119 job_assignment: JobAssignmentMode::Strict,
120 unix_containment: UnixContainment::ProcessGroup,
121 partial_spawn_cleanup_timeout: None,
122 inherited_handles: None,
123 #[cfg(windows)]
124 windows_spawn_config: None,
125 #[cfg(windows)]
126 windows_spawn_config_auto: false,
127 #[cfg(windows)]
128 env_cleared: false,
129 }
130 }
131 pub fn command_mut(&mut self) -> &mut Command {
132 #[cfg(windows)]
133 if self.windows_spawn_config_auto {
134 self.windows_spawn_config = None;
136 }
137 &mut self.command
138 }
139 pub fn inherit_handles_only(
141 &mut self,
142 handles: impl IntoIterator<Item = RawHandleOrFd>,
143 ) -> &mut Self {
144 #[cfg(windows)]
145 let handles = handles.into_iter().map(|handle| handle as usize).collect();
146 #[cfg(unix)]
147 let handles = handles.into_iter().collect();
148 self.inherited_handles = Some(handles);
149 self
150 }
151 #[cfg(windows)]
155 pub fn windows_spawn_config(&mut self, config: crate::WindowsSpawnConfig) -> &mut Self {
156 self.windows_spawn_config = Some(config);
157 self.windows_spawn_config_auto = false;
158 self
159 }
160 pub fn env_allowlist<I, S>(&mut self, keys: I) -> &mut Self
162 where
163 I: IntoIterator<Item = S>,
164 S: AsRef<OsStr>,
165 {
166 let kept: Vec<_> = keys
167 .into_iter()
168 .filter_map(|k| std::env::var_os(k.as_ref()).map(|v| (k.as_ref().to_os_string(), v)))
169 .collect();
170 self.command.env_clear();
171 #[cfg(windows)]
172 {
173 self.env_cleared = true;
174 }
175 for (k, v) in kept {
176 self.command.env(k, v);
177 }
178 self
179 }
180 pub fn env_strip<I, S>(&mut self, keys: I) -> &mut Self
181 where
182 I: IntoIterator<Item = S>,
183 S: AsRef<OsStr>,
184 {
185 for k in keys {
186 self.command.env_remove(k);
187 }
188 self
189 }
190 pub fn windows_hide(&mut self) -> &mut Self {
192 self.windows_hidden = true;
193 self
194 }
195 #[cfg(windows)]
197 pub fn windows_job(
198 &mut self,
199 job: std::os::windows::io::BorrowedHandle<'_>,
200 ) -> io::Result<&mut Self> {
201 self.caller_job = Some(job.try_clone_to_owned()?);
202 Ok(self)
203 }
204 pub fn job_assignment_mode(&mut self, mode: JobAssignmentMode) -> &mut Self {
206 self.job_assignment = mode;
207 self
208 }
209 pub fn unix_containment(&mut self, mode: UnixContainment) -> &mut Self {
211 self.unix_containment = mode;
212 self
213 }
214 pub fn partial_spawn_cleanup_timeout(&mut self, timeout: Duration) -> &mut Self {
216 self.partial_spawn_cleanup_timeout = Some(timeout);
217 self
218 }
219 pub fn spawn(self) -> io::Result<OwnedChild> {
220 self.spawn_inner(false, false, None)
221 }
222 pub fn spawn_uncontained_detached(self) -> io::Result<OwnedChild> {
226 self.spawn_inner(true, false, None)
227 }
228 fn spawn_inner(
229 mut self,
230 detached: bool,
231 fail_after_spawn: bool,
232 captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
233 ) -> io::Result<OwnedChild> {
234 if detached && self.caller_job.is_some() {
235 return Err(io::Error::new(
236 io::ErrorKind::InvalidInput,
237 "detached launch cannot join a caller job",
238 ));
239 }
240 #[cfg(unix)]
241 {
242 use std::os::unix::process::CommandExt;
243 let session = detached || self.unix_containment == UnixContainment::Session;
244 let inheritance = self
245 .inherited_handles
246 .take()
247 .map(crate::process_unix::Inheritance::new)
248 .transpose()?;
249 unsafe {
251 self.command.pre_exec(move || {
252 let result = if session {
253 nix::libc::setsid()
254 } else {
255 nix::libc::setpgid(0, 0)
256 };
257 if result < 0 {
258 return Err(io::Error::last_os_error());
259 }
260 if let Some(inheritance) = &inheritance {
261 inheritance.apply()?;
262 }
263 Ok(())
264 });
265 }
266 }
267 #[cfg(windows)]
268 crate::process_windows::configure(&mut self.command, detached, self.windows_hidden);
269 #[cfg(unix)]
270 let _ = (self.windows_hidden, self.job_assignment);
271 #[cfg(windows)]
272 let _ = self.unix_containment;
273 let spawned_at = SystemTime::now();
274 #[cfg(unix)]
275 let child = self.command.spawn()?;
276 #[cfg(windows)]
277 let child: PlatformChild = match self.inherited_handles.as_deref() {
278 None => self.command.spawn()?.into(),
279 Some(handles) => {
280 let config = self.windows_spawn_config.take().ok_or_else(|| {
281 io::Error::new(io::ErrorKind::InvalidInput,
282 "allowlisted Windows spawn requires windows_spawn_config for opaque Command settings")
283 })?;
284 crate::spawn_windows::spawn(
285 &self.command,
286 config,
287 handles,
288 detached,
289 self.windows_hidden,
290 self.env_cleared,
291 self.partial_spawn_cleanup_timeout,
292 )?
293 }
294 };
295 let pid = child.id();
296 if let Some(captured) = captured_pid {
297 captured.store(pid, std::sync::atomic::Ordering::SeqCst);
298 }
299 let child = Arc::new(Mutex::new(child));
300 let handle = match ProcessHandle::new(&child, spawned_at) {
301 Ok(handle) => handle,
302 Err(error) => {
303 cleanup_direct(child, self.partial_spawn_cleanup_timeout);
304 return Err(error);
305 }
306 };
307 let mut partial = PartialChild {
308 child: Some(OwnedChild {
309 child,
310 handle,
311 pid,
312 detached,
313 terminate_on_drop: !detached,
314 tree_terminated: false,
315 assignment_failures: Vec::new(),
316 #[cfg(windows)]
317 job: None,
318 }),
319 timeout: self.partial_spawn_cleanup_timeout,
320 };
321 #[cfg(windows)]
322 if !detached {
323 let owned = partial.child.as_mut().expect("partial child is present");
324 crate::process_windows::assign_jobs(
325 &owned.handle,
326 self.caller_job.take(),
327 self.job_assignment,
328 &mut owned.job,
329 &mut owned.assignment_failures,
330 )?;
331 }
332 #[cfg(windows)]
333 partial
334 .child
335 .as_ref()
336 .expect("partial child is present")
337 .child()
338 .resume(detached)?;
339 if fail_after_spawn {
340 return Err(io::Error::other("injected post-spawn wrapping failure"));
341 }
342 Ok(partial.child.take().expect("partial child is present"))
343 }
344}
345
346pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
347 "PATH",
348 "HOME",
349 "USER",
350 "LOGNAME",
351 "LANG",
352 "LC_ALL",
353 "TMPDIR",
354 "TEMP",
355 "TMP",
356 "SystemRoot",
357 "SYSTEMROOT",
358 "USERPROFILE",
359 "APPDATA",
360 "LOCALAPPDATA",
361 "ProgramData",
362 "COMSPEC",
363 "PATHEXT",
364 "XDG_DATA_HOME",
365 "XDG_RUNTIME_DIR",
366];
367
368#[derive(Debug)]
369pub struct OwnedChild {
370 child: Arc<Mutex<PlatformChild>>,
371 handle: ProcessHandle,
372 pid: u32,
374 detached: bool,
375 terminate_on_drop: bool,
376 tree_terminated: bool,
377 assignment_failures: Vec<JobAssignmentFailure>,
378 #[cfg(windows)]
379 job: Option<crate::process_windows::KillOnCloseJob>,
380}
381#[derive(Clone, Copy, Debug, Eq, PartialEq)]
382pub enum ReapOutcome {
383 Exited(ExitStatus),
384 TimedOut,
385}
386#[derive(Clone, Copy, Debug, Eq, PartialEq)]
387pub enum WaitOutcome {
388 Exited(ExitStatus),
389 Terminated(ExitStatus),
390}
391
392impl OwnedChild {
393 pub fn id(&self) -> u32 {
394 self.pid
395 }
396 pub fn child(&self) -> MutexGuard<'_, PlatformChild> {
400 lock_child(&self.child)
401 }
402 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
403 self.child().stdin.take()
404 }
405 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
406 self.child().stdout.take()
407 }
408 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
409 self.child().stderr.take()
410 }
411 pub fn job_assignment_failures(&self) -> &[JobAssignmentFailure] {
412 &self.assignment_failures
413 }
414 pub fn process_handle(&self) -> io::Result<ProcessHandle> {
415 self.handle.try_clone()
416 }
417 pub fn creation_time(&self) -> Option<SystemTime> {
418 self.handle.creation_time()
419 }
420 #[cfg(windows)]
421 pub fn creation_time_ticks(&self) -> Option<u64> {
422 self.handle.creation_time_ticks()
423 }
424 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
425 self.child().try_wait()
426 }
427 pub fn wait(&mut self) -> io::Result<ExitStatus> {
430 drop(self.child().stdin.take());
431 self.handle.wait()
432 }
433 pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
434 self.handle.wait_timeout(timeout)
435 }
436 pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
437 #[cfg(unix)]
438 if !self.tree_terminated {
439 self.signal(nix::libc::SIGTERM)?;
440 }
441 if let Some(status) = self.wait_timeout(grace)? {
442 return Ok(WaitOutcome::Exited(status));
443 }
444 self.terminate_tree().map(WaitOutcome::Terminated)
445 }
446 pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
449 self.force_kill(1)?;
450 self.wait()
451 }
452 pub fn terminate_tree_with_options(
455 &mut self,
456 options: TerminationOptions,
457 ) -> io::Result<ExitStatus> {
458 self.start_termination(options)?;
459 self.wait()
460 }
461 pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
462 self.terminate_tree_bounded_with_options(timeout, TerminationOptions::default())
463 }
464 pub fn terminate_tree_bounded_with_options(
466 &mut self,
467 timeout: Duration,
468 mut options: TerminationOptions,
469 ) -> io::Result<ReapOutcome> {
470 let started = Instant::now();
471 options.grace = options.grace.min(timeout);
472 self.start_termination(options)?;
473 Ok(
474 match self.wait_timeout(timeout.saturating_sub(started.elapsed()))? {
475 Some(status) => ReapOutcome::Exited(status),
476 None => ReapOutcome::TimedOut,
477 },
478 )
479 }
480 fn start_termination(&mut self, options: TerminationOptions) -> io::Result<()> {
481 #[cfg(unix)]
482 if !self.tree_terminated {
483 self.signal(nix::libc::SIGTERM)?;
484 let started = Instant::now();
485 while started.elapsed() < options.grace {
486 let _ = self.handle.try_wait()?;
489 if !self.tree_exists()? {
490 break;
491 }
492 thread::sleep(
493 Duration::from_millis(5).min(options.grace.saturating_sub(started.elapsed())),
494 );
495 }
496 }
497 self.force_kill(options.windows_exit_code)
498 }
499 #[cfg(unix)]
500 fn signal(&self, signal: i32) -> io::Result<()> {
501 if self.detached && self.handle.try_wait()?.is_some() {
504 return Ok(());
505 }
506 let target = if self.detached {
507 self.pid as i32
508 } else {
509 -(self.pid as i32)
510 };
511 if unsafe { nix::libc::kill(target, signal) } == 0 {
512 return Ok(());
513 }
514 let error = io::Error::last_os_error();
515 if error.raw_os_error() == Some(nix::libc::ESRCH) || self.group_eperm_after_exit(&error)? {
516 Ok(())
517 } else {
518 Err(error)
519 }
520 }
521 #[cfg(unix)]
522 fn tree_exists(&self) -> io::Result<bool> {
523 if self.detached && self.handle.try_wait()?.is_some() {
524 return Ok(false);
525 }
526 let target = if self.detached {
527 self.pid as i32
528 } else {
529 -(self.pid as i32)
530 };
531 if unsafe { nix::libc::kill(target, 0) } == 0 {
532 return Ok(true);
533 }
534 let error = io::Error::last_os_error();
535 if error.raw_os_error() == Some(nix::libc::ESRCH) || self.group_eperm_after_exit(&error)? {
536 Ok(false)
537 } else {
538 Err(error)
539 }
540 }
541 #[cfg(unix)]
544 fn group_eperm_after_exit(&self, error: &io::Error) -> io::Result<bool> {
545 Ok(!self.detached
546 && error.raw_os_error() == Some(nix::libc::EPERM)
547 && self.handle.try_wait()?.is_some())
548 }
549 fn force_kill(&mut self, code: u32) -> io::Result<()> {
550 if self.tree_terminated {
551 return Ok(());
552 }
553 #[cfg(unix)]
554 {
555 let _ = code;
556 self.signal(nix::libc::SIGKILL)?;
557 }
558 #[cfg(windows)]
559 crate::process_windows::terminate(&self.handle, self.job.as_ref(), code)?;
560 self.tree_terminated = true;
561 Ok(())
562 }
563}
564impl Drop for OwnedChild {
565 fn drop(&mut self) {
566 if self.terminate_on_drop {
567 let _ = self.terminate_tree();
568 } else if self.detached {
569 #[cfg(unix)]
570 {
571 {
574 let mut child = self.child();
575 drop(child.stdin.take());
576 drop(child.stdout.take());
577 drop(child.stderr.take());
578 }
579 if let Ok(reader) = self.handle.try_clone() {
580 let _ = thread::Builder::new()
581 .name("rightkit-detached-reap".into())
582 .spawn(move || {
583 let _ = reader.wait();
584 });
585 }
586 }
587 }
588 }
589}
590
591struct PartialChild {
592 child: Option<OwnedChild>,
593 timeout: Option<Duration>,
594}
595impl Drop for PartialChild {
596 fn drop(&mut self) {
597 if let Some(mut child) = self.child.take() {
598 child.terminate_on_drop = false;
600 let _ = child.force_kill(1);
601 cleanup_direct(Arc::clone(&child.child), self.timeout);
602 }
603 }
604}
605pub(crate) fn cleanup_direct(child: Arc<Mutex<PlatformChild>>, timeout: Option<Duration>) {
606 let _ = lock_child(&child).kill();
607 let Some(timeout) = timeout else {
608 let _ = lock_child(&child).wait();
609 return;
610 };
611 let started = Instant::now();
612 loop {
613 match lock_child(&child).try_wait() {
614 Ok(Some(_)) => return,
615 Err(_) => break,
616 Ok(None) => {}
617 }
618 if started.elapsed() >= timeout {
619 break;
620 }
621 thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
622 }
623 let _ = thread::Builder::new()
625 .name("rightkit-partial-reap".into())
626 .spawn(move || {
627 let _ = lock_child(&child).wait();
628 });
629}
630
631#[derive(Clone, Copy, Debug, Eq, PartialEq)]
632pub struct AdoptedProcess {
633 pid: NonZeroU32,
634}
635impl AdoptedProcess {
636 pub fn new(pid: NonZeroU32) -> Self {
637 Self { pid }
638 }
639 pub fn id(&self) -> u32 {
640 self.pid.get()
641 }
642 pub fn is_running(&self) -> io::Result<bool> {
643 process_is_running(self.pid)
644 }
645}
646#[cfg(test)]
647fn spawn_for_test(
648 command: Command,
649 captured_pid: Arc<std::sync::atomic::AtomicU32>,
650) -> io::Result<OwnedChild> {
651 OwnedCommand::from_command(command).spawn_inner(false, true, Some(captured_pid))
652}
653
654#[cfg(windows)]
655fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
656 use windows::Win32::{
657 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
658 System::Threading::{
659 OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
660 PROCESS_SYNCHRONIZE,
661 },
662 };
663
664 let handle = unsafe {
665 OpenProcess(
666 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
667 false,
668 pid.get(),
669 )
670 };
671 let handle = match handle {
672 Ok(handle) => handle,
673 Err(error) => {
674 if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
675 return Ok(false);
676 }
677 return Err(io::Error::other(error.to_string()));
678 }
679 };
680 let wait = unsafe { WaitForSingleObject(handle, 0) };
681 let close = unsafe { CloseHandle(handle) };
682 close.map_err(|error| io::Error::other(error.to_string()))?;
683 match wait.0 {
684 0 => Ok(false),
685 value if value == WAIT_TIMEOUT.0 => Ok(true),
686 _ => Err(io::Error::last_os_error()),
687 }
688}
689
690#[cfg(unix)]
691fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
692 use nix::{errno::Errno, sys::signal::kill, unistd::Pid};
693
694 let pid = i32::try_from(pid.get())
695 .map(Pid::from_raw)
696 .map_err(io::Error::other)?;
697 match kill(pid, None) {
698 Ok(()) | Err(Errno::EPERM) => Ok(true),
699 Err(Errno::ESRCH) => Ok(false),
700 Err(error) => Err(io::Error::from(error)),
701 }
702}
703
704#[cfg(test)]
705mod tests {
706 #[test]
707 fn option_defaults_preserve_existing_behavior() {
708 let command = super::OwnedCommand::new("unused");
709 assert_eq!(command.job_assignment, super::JobAssignmentMode::Strict);
710 assert_eq!(
711 command.unix_containment,
712 super::UnixContainment::ProcessGroup
713 );
714 assert_eq!(command.partial_spawn_cleanup_timeout, None);
715 assert_eq!(
716 super::TerminationOptions::default().grace,
717 std::time::Duration::ZERO
718 );
719 assert_eq!(super::TerminationOptions::default().windows_exit_code, 1);
720 }
721
722 #[test]
723 fn option_builders_keep_every_requested_value() {
724 use super::{JobAssignmentMode, OwnedCommand, TerminationOptions, UnixContainment};
725 use std::time::Duration;
726 let mut command = OwnedCommand::new("unused");
727 command
728 .job_assignment_mode(JobAssignmentMode::BestEffort)
729 .unix_containment(UnixContainment::Session)
730 .partial_spawn_cleanup_timeout(Duration::from_secs(5));
731 assert_eq!(command.job_assignment, JobAssignmentMode::BestEffort);
732 assert_eq!(command.unix_containment, UnixContainment::Session);
733 assert_eq!(
734 command.partial_spawn_cleanup_timeout,
735 Some(Duration::from_secs(5))
736 );
737 let options = TerminationOptions::default()
738 .with_grace(Duration::from_millis(100))
739 .with_windows_exit_code(1067);
740 assert_eq!(options.grace, Duration::from_millis(100));
741 assert_eq!(options.windows_exit_code, 1067);
742 }
743
744 #[test]
745 fn zero_bound_partial_spawn_failure_still_reaps_the_child() {
746 use std::{
747 num::NonZeroU32,
748 process::Command,
749 sync::{
750 atomic::{AtomicU32, Ordering},
751 Arc,
752 },
753 thread,
754 time::{Duration, Instant},
755 };
756 let command = if cfg!(windows) {
757 let mut command = Command::new("powershell.exe");
758 command.args([
759 "-NoLogo",
760 "-NoProfile",
761 "-NonInteractive",
762 "-Command",
763 "Start-Sleep -Seconds 30",
764 ]);
765 command
766 } else {
767 let mut command = Command::new("/bin/sh");
768 command.args(["-c", "exec sleep 30"]);
769 command
770 };
771 let mut command = super::OwnedCommand::from_command(command);
772 command.partial_spawn_cleanup_timeout(Duration::ZERO);
773 let captured = Arc::new(AtomicU32::new(0));
774 let started = Instant::now();
775 assert!(command
776 .spawn_inner(false, true, Some(Arc::clone(&captured)))
777 .is_err());
778 assert!(started.elapsed() < Duration::from_secs(3));
779 let pid = captured.load(Ordering::SeqCst);
780 assert_ne!(pid, 0);
781 let process = super::AdoptedProcess::new(NonZeroU32::new(pid).unwrap());
782 let started = Instant::now();
783 while process.is_running().unwrap() {
784 assert!(started.elapsed() < Duration::from_secs(5));
785 thread::sleep(Duration::from_millis(5));
786 }
787 }
788
789 #[test]
790 fn owned_commands_hide_their_console_window_by_default() {
791 assert!(super::OwnedCommand::new("x").windows_hidden);
792 assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
793 }
794
795 #[cfg(windows)]
797 fn host_job() -> std::os::windows::io::OwnedHandle {
798 use std::os::windows::io::FromRawHandle;
799 use windows::Win32::System::JobObjects::{
800 CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject,
801 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
802 };
803 let job = unsafe { CreateJobObjectW(None, None) }.unwrap();
804 let job_handle = unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(job.0 as _) };
805 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
806 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
807 unsafe {
808 SetInformationJobObject(
809 job,
810 JobObjectExtendedLimitInformation,
811 &info as *const _ as *const _,
812 std::mem::size_of_val(&info) as u32,
813 )
814 }
815 .unwrap();
816 job_handle
817 }
818
819 #[cfg(windows)]
820 fn in_job(pid: u32, job: &std::os::windows::io::OwnedHandle) -> bool {
821 use std::os::windows::io::AsRawHandle;
822 #[link(name = "kernel32")]
823 unsafe extern "system" {
824 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> *mut std::ffi::c_void;
825 fn IsProcessInJob(
826 process: *mut std::ffi::c_void,
827 job: *mut std::ffi::c_void,
828 result: *mut i32,
829 ) -> i32;
830 fn CloseHandle(handle: *mut std::ffi::c_void) -> i32;
831 }
832 unsafe {
833 let process = OpenProcess(0x1000, 0, pid); assert!(!process.is_null());
835 let mut inside = 0;
836 assert_ne!(
837 IsProcessInJob(process, job.as_raw_handle() as _, &mut inside),
838 0
839 );
840 CloseHandle(process);
841 inside != 0
842 }
843 }
844
845 #[cfg(windows)]
846 #[test]
847 fn children_join_the_callers_job_and_die_when_the_caller_closes_it() {
848 use std::os::windows::io::AsHandle;
849 use std::time::{Duration, Instant};
850 let job = host_job();
851 let sleeper = || {
852 let mut command = super::OwnedCommand::new("powershell.exe");
853 command.command_mut().args([
854 "-NoLogo",
855 "-NoProfile",
856 "-NonInteractive",
857 "-Command",
858 "Start-Sleep -Seconds 60",
859 ]);
860 command.windows_job(job.as_handle()).unwrap();
861 command.spawn().unwrap()
862 };
863 let (first, second) = (sleeper(), sleeper());
866 let pids = [first.id(), second.id()];
867 assert!(
868 pids.iter().all(|pid| in_job(*pid, &job)),
869 "children must be in the caller's job"
870 );
871
872 drop(job);
874 let deadline = Instant::now() + Duration::from_secs(5);
875 for pid in pids {
876 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
877 while process.is_running().unwrap_or(false) {
878 assert!(
879 Instant::now() < deadline,
880 "child {pid} survived its caller's job"
881 );
882 std::thread::sleep(Duration::from_millis(20));
883 }
884 }
885 drop((first, second));
886 }
887
888 #[test]
889 fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
890 use std::{
891 process::Command,
892 sync::{
893 atomic::{AtomicU32, Ordering},
894 Arc,
895 },
896 thread,
897 time::{Duration, Instant},
898 };
899
900 let command = if cfg!(windows) {
902 let mut command = Command::new("powershell.exe");
904 command.args([
905 "-NoLogo",
906 "-NoProfile",
907 "-NonInteractive",
908 "-Command",
909 "Start-Sleep -Seconds 60",
910 ]);
911 command
912 } else {
913 let mut command = Command::new("sleep");
915 command.arg("60");
916 command
917 };
918 let spawned_pid = Arc::new(AtomicU32::new(0));
919
920 assert!(super::spawn_for_test(command, Arc::clone(&spawned_pid)).is_err());
921 let pid = spawned_pid.load(Ordering::SeqCst);
922 assert_ne!(pid, 0, "test must fail after the OS process was spawned");
923 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
924 let deadline = Instant::now() + Duration::from_secs(5);
925 while process.is_running().unwrap_or(false) {
926 assert!(
927 Instant::now() < deadline,
928 "partially spawned process leaked"
929 );
930 thread::sleep(Duration::from_millis(20));
931 }
932 }
933}