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}
21
22impl OwnedCommand {
23 pub fn new(program: impl AsRef<OsStr>) -> Self {
24 Self::from_command(Command::new(program))
26 }
27
28 pub fn from_command(command: Command) -> Self {
31 Self {
32 command,
33 windows_hidden: true,
34 }
35 }
36
37 pub fn command_mut(&mut self) -> &mut Command {
38 &mut self.command
39 }
40
41 pub fn env_allowlist<I, S>(&mut self, keys: I) -> &mut Self
47 where
48 I: IntoIterator<Item = S>,
49 S: AsRef<OsStr>,
50 {
51 let kept: Vec<_> = keys
52 .into_iter()
53 .filter_map(|k| std::env::var_os(k.as_ref()).map(|v| (k.as_ref().to_os_string(), v)))
54 .collect();
55 self.command.env_clear();
56 for (k, v) in kept {
57 self.command.env(k, v);
58 }
59 self
60 }
61
62 pub fn env_strip<I, S>(&mut self, keys: I) -> &mut Self
65 where
66 I: IntoIterator<Item = S>,
67 S: AsRef<OsStr>,
68 {
69 for k in keys {
70 self.command.env_remove(k);
71 }
72 self
73 }
74
75 pub fn windows_hide(&mut self) -> &mut Self {
77 self.windows_hidden = true;
78 self
79 }
80
81 pub fn spawn(self) -> io::Result<OwnedChild> {
82 spawn_owned(self.command, self.windows_hidden, false, None)
83 }
84}
85
86pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
89 "PATH",
90 "HOME",
91 "USER",
92 "LOGNAME",
93 "LANG",
94 "LC_ALL",
95 "TMPDIR",
96 "TEMP",
97 "TMP",
98 "SystemRoot",
99 "SYSTEMROOT",
100 "USERPROFILE",
101 "APPDATA",
102 "LOCALAPPDATA",
103 "ProgramData",
104 "COMSPEC",
105 "PATHEXT",
106 "XDG_DATA_HOME",
107 "XDG_RUNTIME_DIR",
108];
109
110#[derive(Debug)]
111pub struct OwnedChild {
112 child: Option<Box<dyn StdChildWrapper>>,
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub enum ReapOutcome {
118 Exited(ExitStatus),
120 TimedOut,
124}
125
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub enum WaitOutcome {
128 Exited(ExitStatus),
129 Terminated(ExitStatus),
130}
131
132impl OwnedChild {
133 pub fn id(&self) -> u32 {
134 self.child().id()
135 }
136
137 pub fn take_stdin(&mut self) -> Option<ChildStdin> {
138 self.child_mut().stdin().take()
139 }
140
141 pub fn take_stdout(&mut self) -> Option<ChildStdout> {
142 self.child_mut().stdout().take()
143 }
144
145 pub fn take_stderr(&mut self) -> Option<ChildStderr> {
146 self.child_mut().stderr().take()
147 }
148
149 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
150 self.child_mut().try_wait()
151 }
152
153 pub fn wait(&mut self) -> io::Result<ExitStatus> {
154 self.child_mut().wait()
155 }
156
157 pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
158 let started = Instant::now();
159 loop {
160 if let Some(status) = self.try_wait()? {
161 return Ok(Some(status));
162 }
163 if started.elapsed() >= timeout {
164 return Ok(None);
165 }
166 thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
167 }
168 }
169
170 pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
171 #[cfg(unix)]
172 self.child().signal(nix::libc::SIGTERM)?;
173
174 if let Some(status) = self.wait_timeout(grace)? {
175 return Ok(WaitOutcome::Exited(status));
176 }
177 self.terminate_tree().map(WaitOutcome::Terminated)
178 }
179
180 pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
181 if let Some(status) = self.try_wait()? {
182 return Ok(status);
183 }
184 self.child_mut().start_kill()?;
185 self.child_mut().wait()
186 }
187
188 pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
193 if let Some(status) = self.try_wait()? {
194 return Ok(ReapOutcome::Exited(status));
195 }
196 self.child_mut().start_kill()?;
197 Ok(match self.wait_timeout(timeout)? {
198 Some(status) => ReapOutcome::Exited(status),
199 None => ReapOutcome::TimedOut,
200 })
201 }
202
203 fn child(&self) -> &dyn StdChildWrapper {
204 self.child
205 .as_deref()
206 .expect("owned child is always present")
207 }
208
209 fn child_mut(&mut self) -> &mut dyn StdChildWrapper {
210 self.child
211 .as_deref_mut()
212 .expect("owned child is always present")
213 }
214}
215
216impl Drop for OwnedChild {
217 fn drop(&mut self) {
218 let _ = self.terminate_tree();
219 }
220}
221
222#[derive(Clone, Copy, Debug, Eq, PartialEq)]
223pub struct AdoptedProcess {
224 pid: NonZeroU32,
225}
226
227impl AdoptedProcess {
228 pub fn new(pid: NonZeroU32) -> Self {
229 Self { pid }
230 }
231
232 pub fn id(&self) -> u32 {
233 self.pid.get()
234 }
235
236 pub fn is_running(&self) -> io::Result<bool> {
237 process_is_running(self.pid)
238 }
239}
240
241#[derive(Debug)]
242struct CleanupWrapper {
243 captured_pid: Option<Arc<AtomicU32>>,
244}
245
246impl StdCommandWrapper for CleanupWrapper {
247 fn post_spawn(&mut self, child: &mut Child, _core: &StdCommandWrap) -> io::Result<()> {
248 if let Some(pid) = &self.captured_pid {
249 pid.store(child.id(), Ordering::SeqCst);
250 }
251 Ok(())
252 }
253
254 fn wrap_child(
255 &mut self,
256 child: Box<dyn StdChildWrapper>,
257 _core: &StdCommandWrap,
258 ) -> io::Result<Box<dyn StdChildWrapper>> {
259 #[cfg(windows)]
262 let kill_on_close = Some(KillOnCloseJob::assign(child.inner())?);
263 Ok(Box::new(CleanupChild {
264 child: Some(child),
265 #[cfg(windows)]
266 _kill_on_close: kill_on_close,
267 }))
268 }
269}
270
271#[cfg(windows)]
276#[derive(Debug)]
277struct KillOnCloseJob(windows::Win32::Foundation::HANDLE);
278
279#[cfg(windows)]
280impl KillOnCloseJob {
281 fn assign(child: &Child) -> io::Result<Self> {
282 use std::os::windows::io::AsRawHandle;
283 use windows::Win32::{
284 Foundation::HANDLE,
285 System::JobObjects::{
286 AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
287 SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
288 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
289 },
290 };
291 let job = Self(unsafe { CreateJobObjectW(None, None) }.map_err(io::Error::other)?);
292 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
293 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
294 unsafe {
295 SetInformationJobObject(
296 job.0,
297 JobObjectExtendedLimitInformation,
298 &info as *const _ as *const _,
299 std::mem::size_of_val(&info) as u32,
300 )
301 }
302 .map_err(io::Error::other)?;
303 unsafe { AssignProcessToJobObject(job.0, HANDLE(child.as_raw_handle() as _)) }
304 .map_err(io::Error::other)?;
305 Ok(job)
306 }
307}
308
309#[cfg(windows)]
310impl Drop for KillOnCloseJob {
311 fn drop(&mut self) {
312 let _ = unsafe { windows::Win32::Foundation::CloseHandle(self.0) };
313 }
314}
315
316#[cfg(windows)]
318unsafe impl Send for KillOnCloseJob {}
319#[cfg(windows)]
320unsafe impl Sync for KillOnCloseJob {}
321
322#[derive(Debug)]
323struct CleanupChild {
324 child: Option<Box<dyn StdChildWrapper>>,
325 #[cfg(windows)]
327 _kill_on_close: Option<KillOnCloseJob>,
328}
329
330impl CleanupChild {
331 fn child(&self) -> &dyn StdChildWrapper {
332 self.child.as_deref().expect("cleanup child is present")
333 }
334
335 fn child_mut(&mut self) -> &mut dyn StdChildWrapper {
336 self.child.as_deref_mut().expect("cleanup child is present")
337 }
338}
339
340impl StdChildWrapper for CleanupChild {
341 fn inner(&self) -> &Child {
342 self.child().inner()
343 }
344
345 fn inner_mut(&mut self) -> &mut Child {
346 self.child_mut().inner_mut()
347 }
348
349 fn into_inner(mut self: Box<Self>) -> Child {
350 self.child
351 .take()
352 .expect("cleanup child is present")
353 .into_inner()
354 }
355
356 fn stdin(&mut self) -> &mut Option<ChildStdin> {
357 self.child_mut().stdin()
358 }
359
360 fn stdout(&mut self) -> &mut Option<ChildStdout> {
361 self.child_mut().stdout()
362 }
363
364 fn stderr(&mut self) -> &mut Option<ChildStderr> {
365 self.child_mut().stderr()
366 }
367
368 fn id(&self) -> u32 {
369 self.child().id()
370 }
371
372 fn start_kill(&mut self) -> io::Result<()> {
373 self.child_mut().start_kill()
374 }
375
376 fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
377 self.child_mut().try_wait()
378 }
379
380 fn wait(&mut self) -> io::Result<ExitStatus> {
381 self.child_mut().wait()
382 }
383
384 #[cfg(unix)]
385 fn signal(&self, signal: i32) -> io::Result<()> {
386 self.child().signal(signal)
387 }
388}
389
390impl Drop for CleanupChild {
391 fn drop(&mut self) {
392 let Some(child) = self.child.as_deref_mut() else {
393 return;
394 };
395 if !matches!(child.try_wait(), Ok(Some(_))) {
396 let _ = child.start_kill();
397 let _ = child.wait();
398 }
399 }
400}
401
402#[derive(Debug)]
403struct FailAfterSpawn;
404
405impl StdCommandWrapper for FailAfterSpawn {
406 fn wrap_child(
407 &mut self,
408 _child: Box<dyn StdChildWrapper>,
409 _core: &StdCommandWrap,
410 ) -> io::Result<Box<dyn StdChildWrapper>> {
411 Err(io::Error::other("injected post-spawn wrapping failure"))
412 }
413}
414
415fn spawn_owned(
416 command: Command,
417 windows_hidden: bool,
418 fail_after_spawn: bool,
419 captured_pid: Option<Arc<AtomicU32>>,
420) -> io::Result<OwnedChild> {
421 let mut command = StdCommandWrap::from(command);
422 command.wrap(CleanupWrapper { captured_pid });
423
424 #[cfg(windows)]
425 {
426 use process_wrap::std::{CreationFlags, JobObject};
427 use windows::Win32::System::Threading::CREATE_NO_WINDOW;
428
429 if windows_hidden {
430 command.wrap(CreationFlags(CREATE_NO_WINDOW));
431 }
432 command.wrap(JobObject);
433 }
434
435 #[cfg(unix)]
436 {
437 use process_wrap::std::ProcessGroup;
438 let _ = windows_hidden;
439 command.wrap(ProcessGroup::leader());
440 }
441
442 if fail_after_spawn {
443 command.wrap(FailAfterSpawn);
444 }
445
446 command
447 .spawn()
448 .map(|child| OwnedChild { child: Some(child) })
449}
450
451#[cfg(test)]
452fn spawn_for_test(command: Command, captured_pid: Arc<AtomicU32>) -> io::Result<OwnedChild> {
453 spawn_owned(command, true, true, Some(captured_pid))
454}
455
456#[cfg(windows)]
457fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
458 use windows::Win32::{
459 Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
460 System::Threading::{
461 OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
462 PROCESS_SYNCHRONIZE,
463 },
464 };
465
466 let handle = unsafe {
467 OpenProcess(
468 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
469 false,
470 pid.get(),
471 )
472 };
473 let handle = match handle {
474 Ok(handle) => handle,
475 Err(error) => {
476 if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
477 return Ok(false);
478 }
479 return Err(io::Error::other(error.to_string()));
480 }
481 };
482 let wait = unsafe { WaitForSingleObject(handle, 0) };
483 let close = unsafe { CloseHandle(handle) };
484 close.map_err(|error| io::Error::other(error.to_string()))?;
485 match wait.0 {
486 0 => Ok(false),
487 value if value == WAIT_TIMEOUT.0 => Ok(true),
488 _ => Err(io::Error::last_os_error()),
489 }
490}
491
492#[cfg(unix)]
493fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
494 use nix::{errno::Errno, sys::signal::kill, unistd::Pid};
495
496 let pid = i32::try_from(pid.get())
497 .map(Pid::from_raw)
498 .map_err(io::Error::other)?;
499 match kill(pid, None) {
500 Ok(()) | Err(Errno::EPERM) => Ok(true),
501 Err(Errno::ESRCH) => Ok(false),
502 Err(error) => Err(io::Error::from(error)),
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 #[test]
509 fn owned_commands_hide_their_console_window_by_default() {
510 assert!(super::OwnedCommand::new("x").windows_hidden);
511 assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
512 }
513
514 #[test]
515 fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
516 use std::{
517 process::Command,
518 sync::{
519 atomic::{AtomicU32, Ordering},
520 Arc,
521 },
522 thread,
523 time::{Duration, Instant},
524 };
525
526 let command = if cfg!(windows) {
528 let mut command = Command::new("powershell.exe");
529 command.args([
530 "-NoLogo",
531 "-NoProfile",
532 "-NonInteractive",
533 "-Command",
534 "Start-Sleep -Seconds 60",
535 ]);
536 command
537 } else {
538 let mut command = Command::new("sleep");
539 command.arg("60");
540 command
541 };
542 let spawned_pid = Arc::new(AtomicU32::new(0));
543
544 assert!(super::spawn_for_test(command, Arc::clone(&spawned_pid)).is_err());
545 let pid = spawned_pid.load(Ordering::SeqCst);
546 assert_ne!(pid, 0, "test must fail after the OS process was spawned");
547 let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
548 let deadline = Instant::now() + Duration::from_secs(5);
549 while process.is_running().unwrap_or(false) {
550 assert!(
551 Instant::now() < deadline,
552 "partially spawned process leaked"
553 );
554 thread::sleep(Duration::from_millis(20));
555 }
556 }
557}