1use std::{
2 ffi::OsStr,
3 io,
4 num::NonZeroU32,
5 process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus},
6 sync::{
7 atomic::{AtomicU32, Ordering},
8 Arc,
9 },
10 thread,
11 time::{Duration, Instant},
12};
13
14use process_wrap::std::{StdChildWrapper, StdCommandWrap, StdCommandWrapper};
15
16#[derive(Debug)]
17pub struct OwnedCommand {
18 command: Command,
19 windows_hidden: bool,
20 caller_job: CallerJob,
21}
22
23#[cfg(windows)]
25type CallerJob = Option<std::os::windows::io::OwnedHandle>;
26#[cfg(not(windows))]
27type CallerJob = Option<std::convert::Infallible>;
28
29impl OwnedCommand {
30 pub fn new(program: impl AsRef<OsStr>) -> Self {
31 Self::from_command(Command::new(program))
33 }
34
35 pub fn from_command(command: Command) -> Self {
38 Self {
39 command,
40 windows_hidden: true,
41 caller_job: Default::default(),
42 }
43 }
44
45 pub fn command_mut(&mut self) -> &mut Command {
46 &mut self.command
47 }
48
49 pub fn env_allowlist<I, S>(&mut self, keys: I) -> &mut Self
55 where
56 I: IntoIterator<Item = S>,
57 S: AsRef<OsStr>,
58 {
59 let kept: Vec<_> = keys
60 .into_iter()
61 .filter_map(|k| std::env::var_os(k.as_ref()).map(|v| (k.as_ref().to_os_string(), v)))
62 .collect();
63 self.command.env_clear();
64 for (k, v) in kept {
65 self.command.env(k, v);
66 }
67 self
68 }
69
70 pub fn env_strip<I, S>(&mut self, keys: I) -> &mut Self
73 where
74 I: IntoIterator<Item = S>,
75 S: AsRef<OsStr>,
76 {
77 for k in keys {
78 self.command.env_remove(k);
79 }
80 self
81 }
82
83 pub fn windows_hide(&mut self) -> &mut Self {
85 self.windows_hidden = true;
86 self
87 }
88
89 #[cfg(windows)]
95 pub fn windows_job(
96 &mut self,
97 job: std::os::windows::io::BorrowedHandle<'_>,
98 ) -> io::Result<&mut Self> {
99 self.caller_job = Some(job.try_clone_to_owned()?);
100 Ok(self)
101 }
102
103 pub fn spawn(self) -> io::Result<OwnedChild> {
104 spawn_owned(
105 self.command,
106 self.windows_hidden,
107 false,
108 None,
109 self.caller_job,
110 )
111 }
112}
113
114pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
117 "PATH",
118 "HOME",
119 "USER",
120 "LOGNAME",
121 "LANG",
122 "LC_ALL",
123 "TMPDIR",
124 "TEMP",
125 "TMP",
126 "SystemRoot",
127 "SYSTEMROOT",
128 "USERPROFILE",
129 "APPDATA",
130 "LOCALAPPDATA",
131 "ProgramData",
132 "COMSPEC",
133 "PATHEXT",
134 "XDG_DATA_HOME",
135 "XDG_RUNTIME_DIR",
136];
137
138#[derive(Debug)]
139pub struct OwnedChild {
140 child: Option<Box<dyn StdChildWrapper>>,
141}
142
143#[derive(Clone, Copy, Debug, Eq, PartialEq)]
145pub enum ReapOutcome {
146 Exited(ExitStatus),
148 TimedOut,
152}
153
154#[derive(Clone, Copy, Debug, Eq, PartialEq)]
155pub enum WaitOutcome {
156 Exited(ExitStatus),
157 Terminated(ExitStatus),
158}
159
160impl OwnedChild {
161 pub fn id(&self) -> u32 {
162 self.child().id()
163 }
164
165 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
166 self.child_mut().stdin().take()
167 }
168
169 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
170 self.child_mut().stdout().take()
171 }
172
173 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
174 self.child_mut().stderr().take()
175 }
176
177 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
178 self.child_mut().try_wait()
179 }
180
181 pub fn wait(&mut self) -> io::Result<ExitStatus> {
182 self.child_mut().wait()
183 }
184
185 pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
186 let started = Instant::now();
187 loop {
188 if let Some(status) = self.try_wait()? {
189 return Ok(Some(status));
190 }
191 if started.elapsed() >= timeout {
192 return Ok(None);
193 }
194 thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
195 }
196 }
197
198 pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
199 #[cfg(unix)]
200 self.child().signal(nix::libc::SIGTERM)?;
201
202 if let Some(status) = self.wait_timeout(grace)? {
203 return Ok(WaitOutcome::Exited(status));
204 }
205 self.terminate_tree().map(WaitOutcome::Terminated)
206 }
207
208 pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
209 if let Some(status) = self.try_wait()? {
210 return Ok(status);
211 }
212 self.child_mut().start_kill()?;
213 self.child_mut().wait()
214 }
215
216 pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
221 if let Some(status) = self.try_wait()? {
222 return Ok(ReapOutcome::Exited(status));
223 }
224 self.child_mut().start_kill()?;
225 Ok(match self.wait_timeout(timeout)? {
226 Some(status) => ReapOutcome::Exited(status),
227 None => ReapOutcome::TimedOut,
228 })
229 }
230
231 fn child(&self) -> &dyn StdChildWrapper {
232 self.child
233 .as_deref()
234 .expect("owned child is always present")
235 }
236
237 fn child_mut(&mut self) -> &mut dyn StdChildWrapper {
238 self.child
239 .as_deref_mut()
240 .expect("owned child is always present")
241 }
242}
243
244impl Drop for OwnedChild {
245 fn drop(&mut self) {
246 let _ = self.terminate_tree();
247 }
248}
249
250#[derive(Clone, Copy, Debug, Eq, PartialEq)]
251pub struct AdoptedProcess {
252 pid: NonZeroU32,
253}
254
255impl AdoptedProcess {
256 pub fn new(pid: NonZeroU32) -> Self {
257 Self { pid }
258 }
259
260 pub fn id(&self) -> u32 {
261 self.pid.get()
262 }
263
264 pub fn is_running(&self) -> io::Result<bool> {
265 process_is_running(self.pid)
266 }
267}
268
269#[derive(Debug)]
270struct CleanupWrapper {
271 captured_pid: Option<Arc<AtomicU32>>,
272 caller_job: CallerJob,
273}
274
275impl StdCommandWrapper for CleanupWrapper {
276 fn post_spawn(&mut self, child: &mut Child, _core: &StdCommandWrap) -> io::Result<()> {
277 if let Some(pid) = &self.captured_pid {
278 pid.store(child.id(), Ordering::SeqCst);
279 }
280 Ok(())
281 }
282
283 fn wrap_child(
284 &mut self,
285 #[cfg_attr(not(windows), allow(unused_mut))] mut child: Box<dyn StdChildWrapper>,
286 _core: &StdCommandWrap,
287 ) -> io::Result<Box<dyn StdChildWrapper>> {
288 #[cfg(not(windows))]
294 let _ = self.caller_job.take();
295 #[cfg(windows)]
296 let kill_on_close = {
297 let bound = match self.caller_job.take() {
298 Some(job) => assign_to_caller_job(&job, child.inner()),
299 None => Ok(()),
300 }
301 .and_then(|()| KillOnCloseJob::assign(child.inner()));
302 match bound {
303 Ok(job) => Some(job),
304 Err(error) => {
305 let _ = child.start_kill();
306 let _ = child.wait();
307 return Err(error);
308 }
309 }
310 };
311 Ok(Box::new(CleanupChild {
312 child: Some(child),
313 #[cfg(windows)]
314 _kill_on_close: kill_on_close,
315 }))
316 }
317}
318
319#[cfg(windows)]
320fn assign_to_caller_job(job: &std::os::windows::io::OwnedHandle, child: &Child) -> io::Result<()> {
321 use std::os::windows::io::AsRawHandle;
322 use windows::Win32::{Foundation::HANDLE, System::JobObjects::AssignProcessToJobObject};
323 unsafe {
324 AssignProcessToJobObject(
325 HANDLE(job.as_raw_handle() as _),
326 HANDLE(child.as_raw_handle() as _),
327 )
328 }
329 .map_err(|e| io::Error::other(format!("bind child to the caller's job object: {e}")))
330}
331
332#[cfg(windows)]
337#[derive(Debug)]
338struct KillOnCloseJob(windows::Win32::Foundation::HANDLE);
339
340#[cfg(windows)]
341impl KillOnCloseJob {
342 fn assign(child: &Child) -> io::Result<Self> {
343 use std::os::windows::io::AsRawHandle;
344 use windows::Win32::{
345 Foundation::HANDLE,
346 System::JobObjects::{
347 AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
348 SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
349 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
350 },
351 };
352 let job = Self(unsafe { CreateJobObjectW(None, None) }.map_err(io::Error::other)?);
353 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
354 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
355 unsafe {
356 SetInformationJobObject(
357 job.0,
358 JobObjectExtendedLimitInformation,
359 &info as *const _ as *const _,
360 std::mem::size_of_val(&info) as u32,
361 )
362 }
363 .map_err(io::Error::other)?;
364 unsafe { AssignProcessToJobObject(job.0, HANDLE(child.as_raw_handle() as _)) }
365 .map_err(io::Error::other)?;
366 Ok(job)
367 }
368}
369
370#[cfg(windows)]
371impl Drop for KillOnCloseJob {
372 fn drop(&mut self) {
373 let _ = unsafe { windows::Win32::Foundation::CloseHandle(self.0) };
374 }
375}
376
377#[cfg(windows)]
379unsafe impl Send for KillOnCloseJob {}
380#[cfg(windows)]
381unsafe impl Sync for KillOnCloseJob {}
382
383#[derive(Debug)]
384struct CleanupChild {
385 child: Option<Box<dyn StdChildWrapper>>,
386 #[cfg(windows)]
388 _kill_on_close: Option<KillOnCloseJob>,
389}
390
391impl CleanupChild {
392 fn child(&self) -> &dyn StdChildWrapper {
393 self.child.as_deref().expect("cleanup child is present")
394 }
395
396 fn child_mut(&mut self) -> &mut dyn StdChildWrapper {
397 self.child.as_deref_mut().expect("cleanup child is present")
398 }
399}
400
401impl StdChildWrapper for CleanupChild {
402 fn inner(&self) -> &Child {
403 self.child().inner()
404 }
405
406 fn inner_mut(&mut self) -> &mut Child {
407 self.child_mut().inner_mut()
408 }
409
410 fn into_inner(mut self: Box<Self>) -> Child {
411 self.child
412 .take()
413 .expect("cleanup child is present")
414 .into_inner()
415 }
416
417 fn stdin(&mut self) -> &mut Option<ChildStdin> {
418 self.child_mut().stdin()
419 }
420
421 fn stdout(&mut self) -> &mut Option<ChildStdout> {
422 self.child_mut().stdout()
423 }
424
425 fn stderr(&mut self) -> &mut Option<ChildStderr> {
426 self.child_mut().stderr()
427 }
428
429 fn id(&self) -> u32 {
430 self.child().id()
431 }
432
433 fn start_kill(&mut self) -> io::Result<()> {
434 self.child_mut().start_kill()
435 }
436
437 fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
438 self.child_mut().try_wait()
439 }
440
441 fn wait(&mut self) -> io::Result<ExitStatus> {
442 self.child_mut().wait()
443 }
444
445 #[cfg(unix)]
446 fn signal(&self, signal: i32) -> io::Result<()> {
447 self.child().signal(signal)
448 }
449}
450
451impl Drop for CleanupChild {
452 fn drop(&mut self) {
453 let Some(child) = self.child.as_deref_mut() else {
454 return;
455 };
456 if !matches!(child.try_wait(), Ok(Some(_))) {
457 let _ = child.start_kill();
458 let _ = child.wait();
459 }
460 }
461}
462
463#[derive(Debug)]
464struct FailAfterSpawn;
465
466impl StdCommandWrapper for FailAfterSpawn {
467 fn wrap_child(
468 &mut self,
469 _child: Box<dyn StdChildWrapper>,
470 _core: &StdCommandWrap,
471 ) -> io::Result<Box<dyn StdChildWrapper>> {
472 Err(io::Error::other("injected post-spawn wrapping failure"))
473 }
474}
475
476fn spawn_owned(
477 command: Command,
478 windows_hidden: bool,
479 fail_after_spawn: bool,
480 captured_pid: Option<Arc<AtomicU32>>,
481 caller_job: CallerJob,
482) -> io::Result<OwnedChild> {
483 let mut command = StdCommandWrap::from(command);
484 command.wrap(CleanupWrapper {
485 captured_pid,
486 caller_job,
487 });
488
489 #[cfg(windows)]
490 {
491 use process_wrap::std::{CreationFlags, JobObject};
492 use windows::Win32::System::Threading::CREATE_NO_WINDOW;
493
494 if windows_hidden {
495 command.wrap(CreationFlags(CREATE_NO_WINDOW));
496 }
497 command.wrap(JobObject);
498 }
499
500 #[cfg(unix)]
501 {
502 use process_wrap::std::ProcessGroup;
503 let _ = windows_hidden;
504 command.wrap(ProcessGroup::leader());
505 }
506
507 if fail_after_spawn {
508 command.wrap(FailAfterSpawn);
509 }
510
511 command
512 .spawn()
513 .map(|child| OwnedChild { child: Some(child) })
514}
515
516#[cfg(test)]
517fn spawn_for_test(command: Command, captured_pid: Arc<AtomicU32>) -> io::Result<OwnedChild> {
518 spawn_owned(command, true, true, Some(captured_pid), Default::default())
519}
520
521#[cfg(windows)]
522fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
523 use windows::Win32::{
524 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
525 System::Threading::{
526 OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
527 PROCESS_SYNCHRONIZE,
528 },
529 };
530
531 let handle = unsafe {
532 OpenProcess(
533 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
534 false,
535 pid.get(),
536 )
537 };
538 let handle = match handle {
539 Ok(handle) => handle,
540 Err(error) => {
541 if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
542 return Ok(false);
543 }
544 return Err(io::Error::other(error.to_string()));
545 }
546 };
547 let wait = unsafe { WaitForSingleObject(handle, 0) };
548 let close = unsafe { CloseHandle(handle) };
549 close.map_err(|error| io::Error::other(error.to_string()))?;
550 match wait.0 {
551 0 => Ok(false),
552 value if value == WAIT_TIMEOUT.0 => Ok(true),
553 _ => Err(io::Error::last_os_error()),
554 }
555}
556
557#[cfg(unix)]
558fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
559 use nix::{errno::Errno, sys::signal::kill, unistd::Pid};
560
561 let pid = i32::try_from(pid.get())
562 .map(Pid::from_raw)
563 .map_err(io::Error::other)?;
564 match kill(pid, None) {
565 Ok(()) | Err(Errno::EPERM) => Ok(true),
566 Err(Errno::ESRCH) => Ok(false),
567 Err(error) => Err(io::Error::from(error)),
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 #[test]
574 fn owned_commands_hide_their_console_window_by_default() {
575 assert!(super::OwnedCommand::new("x").windows_hidden);
576 assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
577 }
578
579 #[cfg(windows)]
581 fn host_job() -> std::os::windows::io::OwnedHandle {
582 use std::os::windows::io::FromRawHandle;
583 use windows::Win32::System::JobObjects::{
584 CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject,
585 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
586 };
587 let job = unsafe { CreateJobObjectW(None, None) }.unwrap();
588 let job_handle = unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(job.0 as _) };
589 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
590 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
591 unsafe {
592 SetInformationJobObject(
593 job,
594 JobObjectExtendedLimitInformation,
595 &info as *const _ as *const _,
596 std::mem::size_of_val(&info) as u32,
597 )
598 }
599 .unwrap();
600 job_handle
601 }
602
603 #[cfg(windows)]
604 fn in_job(pid: u32, job: &std::os::windows::io::OwnedHandle) -> bool {
605 use std::os::windows::io::AsRawHandle;
606 #[link(name = "kernel32")]
607 unsafe extern "system" {
608 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> *mut std::ffi::c_void;
609 fn IsProcessInJob(
610 process: *mut std::ffi::c_void,
611 job: *mut std::ffi::c_void,
612 result: *mut i32,
613 ) -> i32;
614 fn CloseHandle(handle: *mut std::ffi::c_void) -> i32;
615 }
616 unsafe {
617 let process = OpenProcess(0x1000, 0, pid); assert!(!process.is_null());
619 let mut inside = 0;
620 assert_ne!(
621 IsProcessInJob(process, job.as_raw_handle() as _, &mut inside),
622 0
623 );
624 CloseHandle(process);
625 inside != 0
626 }
627 }
628
629 #[cfg(windows)]
630 #[test]
631 fn children_join_the_callers_job_and_die_when_the_caller_closes_it() {
632 use std::os::windows::io::AsHandle;
633 use std::time::{Duration, Instant};
634 let job = host_job();
635 let sleeper = || {
636 let mut command = super::OwnedCommand::new("powershell.exe");
637 command.command_mut().args([
638 "-NoLogo",
639 "-NoProfile",
640 "-NonInteractive",
641 "-Command",
642 "Start-Sleep -Seconds 60",
643 ]);
644 command.windows_job(job.as_handle()).unwrap();
645 command.spawn().unwrap()
646 };
647 let (first, second) = (sleeper(), sleeper());
650 let pids = [first.id(), second.id()];
651 assert!(
652 pids.iter().all(|pid| in_job(*pid, &job)),
653 "children must be in the caller's job"
654 );
655
656 drop(job);
658 let deadline = Instant::now() + Duration::from_secs(5);
659 for pid in pids {
660 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
661 while process.is_running().unwrap_or(false) {
662 assert!(
663 Instant::now() < deadline,
664 "child {pid} survived its caller's job"
665 );
666 std::thread::sleep(Duration::from_millis(20));
667 }
668 }
669 drop((first, second));
670 }
671
672 #[test]
673 fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
674 use std::{
675 process::Command,
676 sync::{
677 atomic::{AtomicU32, Ordering},
678 Arc,
679 },
680 thread,
681 time::{Duration, Instant},
682 };
683
684 let command = if cfg!(windows) {
686 let mut command = Command::new("powershell.exe");
688 command.args([
689 "-NoLogo",
690 "-NoProfile",
691 "-NonInteractive",
692 "-Command",
693 "Start-Sleep -Seconds 60",
694 ]);
695 command
696 } else {
697 let mut command = Command::new("sleep");
699 command.arg("60");
700 command
701 };
702 let spawned_pid = Arc::new(AtomicU32::new(0));
703
704 assert!(super::spawn_for_test(command, Arc::clone(&spawned_pid)).is_err());
705 let pid = spawned_pid.load(Ordering::SeqCst);
706 assert_ne!(pid, 0, "test must fail after the OS process was spawned");
707 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
708 let deadline = Instant::now() + Duration::from_secs(5);
709 while process.is_running().unwrap_or(false) {
710 assert!(
711 Instant::now() < deadline,
712 "partially spawned process leaked"
713 );
714 thread::sleep(Duration::from_millis(20));
715 }
716 }
717}