Skip to main content

pal_async/
process.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Process wait functionality.
5//!
6//! Provides async primitives for waiting on child process exit without
7//! consuming the child object. On Linux and Windows, [`PolledChild`]
8//! constructs the wait directly from existing [`Driver`]
9//! primitives (fd readiness and waitable handles). On macOS,
10//! `Driver::new_dyn_process_wait` dispatches to the kqueue `EVFILT_PROC`
11//! mechanism via the `ProcessWaitDriver` trait.
12
13use crate::driver::Driver;
14use std::future::Future;
15use std::future::poll_fn;
16use std::io;
17#[cfg(target_os = "linux")]
18use std::os::fd::OwnedFd;
19use std::task::Context;
20use std::task::Poll;
21use std::task::ready;
22
23/// macOS-specific process wait types.
24#[cfg(target_os = "macos")]
25pub mod macos {
26    pub use crate::sys::process::macos::NoProcessWait;
27    pub use crate::sys::process::macos::PollProcessWait;
28    pub use crate::sys::process::macos::ProcessWaitDriver;
29    pub use crate::sys::process::macos::ProcessWaitImpl;
30}
31
32/// An owned child process with an asynchronous exit wait.
33///
34/// The wait field is declared before `child` so that the backend wait
35/// registration is dropped before the child's underlying handle or fd.
36pub struct PolledChild<C> {
37    wait: Option<crate::sys::process::WaitInner>,
38    // Drop order: after `wait`, which may have a RawFd copy of this.
39    #[cfg(target_os = "linux")]
40    _owned_pidfd: Option<OwnedFd>,
41    child: C,
42}
43
44impl<C> PolledChild<C> {
45    /// Returns the inner child, dropping the wait registration.
46    pub fn into_inner(self) -> C {
47        self.child
48    }
49
50    /// Gets a reference to the inner child.
51    pub fn get(&self) -> &C {
52        &self.child
53    }
54
55    /// Gets a mutable reference to the inner child.
56    pub fn get_mut(&mut self) -> &mut C {
57        &mut self.child
58    }
59}
60
61/// Polls the wait backend for the exit notification, then reaps the child.
62///
63/// `reap` performs a *blocking* wait. On Linux (pidfd) and Windows (handle)
64/// the backend only signals once the child is reapable, so the wait returns
65/// immediately. On macOS the kqueue `NOTE_EXIT` notification can arrive a few
66/// microseconds before `waitpid` can reap the child, so the wait blocks for
67/// that brief, kernel-bounded window (XNU delivers `NOTE_EXIT` while the
68/// process is exiting, with only a few non-blocking teardown steps remaining)
69/// rather than spinning on a non-blocking `try_wait`.
70///
71/// This is the shared implementation of `PolledChild::poll_wait` for all
72/// child-process types.
73fn poll_child_exit(
74    cx: &mut Context<'_>,
75    wait: &mut Option<crate::sys::process::WaitInner>,
76    reap: impl FnOnce() -> io::Result<std::process::ExitStatus>,
77) -> Poll<io::Result<std::process::ExitStatus>> {
78    if let Some(w) = wait {
79        ready!(w.poll_exit(cx))?;
80    }
81    Poll::Ready(reap())
82}
83
84// --- std::process::Child ---
85
86impl PolledChild<std::process::Child> {
87    /// Creates a new `PolledChild` wrapping a [`std::process::Child`].
88    pub fn new(driver: &(impl ?Sized + Driver), child: std::process::Child) -> io::Result<Self> {
89        Self::new_inner(driver, child)
90    }
91
92    /// Polls for the child process to exit.
93    pub fn poll_wait(
94        &mut self,
95        cx: &mut Context<'_>,
96    ) -> Poll<io::Result<std::process::ExitStatus>> {
97        poll_child_exit(cx, &mut self.wait, || self.child.wait())
98    }
99
100    /// Waits for the child process to exit.
101    pub fn wait(
102        &mut self,
103    ) -> impl '_ + Unpin + Future<Output = io::Result<std::process::ExitStatus>> {
104        poll_fn(move |cx| self.poll_wait(cx))
105    }
106}
107
108// --- pal::unix::process::Child ---
109
110#[cfg(unix)]
111impl PolledChild<pal::unix::process::Child> {
112    /// Creates a new `PolledChild` wrapping a [`pal::unix::process::Child`].
113    pub fn new(
114        driver: &(impl ?Sized + Driver),
115        child: pal::unix::process::Child,
116    ) -> io::Result<Self> {
117        Self::new_inner(driver, child)
118    }
119
120    /// Polls for the child process to exit.
121    pub fn poll_wait(
122        &mut self,
123        cx: &mut Context<'_>,
124    ) -> Poll<io::Result<std::process::ExitStatus>> {
125        poll_child_exit(cx, &mut self.wait, || self.child.wait())
126    }
127
128    /// Waits for the child process to exit.
129    pub fn wait(
130        &mut self,
131    ) -> impl '_ + Unpin + Future<Output = io::Result<std::process::ExitStatus>> {
132        poll_fn(move |cx| self.poll_wait(cx))
133    }
134}
135
136// --- pal::windows::Process ---
137
138/// An owned process handle with an asynchronous exit wait.
139///
140/// Unlike [`PolledChild`], this type wraps a cloneable process handle
141/// (not a child with cached exit status). The exit code is returned
142/// as a `u32` matching the API of [`pal::windows::Process`].
143///
144/// The `wait` field is declared before `process` so that the backend
145/// wait registration is dropped before the process handle.
146#[cfg(windows)]
147pub struct PolledProcess {
148    wait: Option<crate::sys::process::WaitInner>,
149    process: pal::windows::Process,
150}
151
152#[cfg(windows)]
153impl PolledProcess {
154    /// Creates a new `PolledProcess` wrapping a [`pal::windows::Process`].
155    ///
156    /// Waits on the process handle to detect exit.
157    pub fn new(
158        driver: &(impl ?Sized + Driver),
159        process: pal::windows::Process,
160    ) -> io::Result<Self> {
161        use crate::sys::process::HandleProcessWait;
162        use std::os::windows::prelude::*;
163
164        let handle = process.as_handle().as_raw_handle();
165        let wait = driver.new_dyn_wait(handle)?;
166        Ok(Self {
167            wait: Some(HandleProcessWait::new(wait)),
168            process,
169        })
170    }
171
172    /// Returns the inner process, dropping the wait registration.
173    pub fn into_inner(self) -> pal::windows::Process {
174        self.process
175    }
176
177    /// Gets a reference to the inner process.
178    pub fn get(&self) -> &pal::windows::Process {
179        &self.process
180    }
181
182    /// Polls for the process to exit, returning its exit code.
183    pub fn poll_wait(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<u32>> {
184        if let Some(wait) = &mut self.wait {
185            ready!(wait.poll_exit(cx))?;
186        }
187        Poll::Ready(Ok(self.process.exit_code()))
188    }
189
190    /// Waits for the process to exit, returning its exit code.
191    pub fn wait(&mut self) -> impl '_ + Unpin + Future<Output = io::Result<u32>> {
192        poll_fn(move |cx| self.poll_wait(cx))
193    }
194}
195
196impl<C> PolledChild<C> {
197    #[cfg_attr(windows, expect(dead_code))]
198    /// Creates a `PolledChild` for an already-exited child.
199    fn exited(child: C) -> Self {
200        Self {
201            wait: None,
202            #[cfg(target_os = "linux")]
203            _owned_pidfd: None,
204            child,
205        }
206    }
207}
208
209/// Linux: open a pidfd and poll fd readiness.
210#[cfg(target_os = "linux")]
211mod linux {
212    // UNSAFETY: Needed for the pidfd_open syscall.
213    #![expect(unsafe_code)]
214
215    use super::*;
216    use crate::sys::process::linux::FdProcessWait;
217    use std::os::unix::prelude::*;
218
219    /// Opens a pidfd for an existing process.
220    fn pidfd_open(pid: i32) -> io::Result<OwnedFd> {
221        // SAFETY: pidfd_open is a simple syscall that creates a new file
222        // descriptor for monitoring the given pid.
223        let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0 as libc::c_int) };
224        if fd < 0 {
225            return Err(io::Error::last_os_error());
226        }
227        // SAFETY: pidfd_open returned a valid file descriptor on success.
228        Ok(unsafe { OwnedFd::from_raw_fd(fd as RawFd) })
229    }
230
231    impl PolledChild<std::process::Child> {
232        pub(super) fn new_inner(
233            driver: &(impl ?Sized + Driver),
234            mut child: std::process::Child,
235        ) -> io::Result<Self> {
236            // If the caller already reaped the child, don't try to register
237            // notifications on its pid.
238            if child.try_wait()?.is_some() {
239                return Ok(Self::exited(child));
240            }
241            let pidfd = pidfd_open(child.id() as i32)?;
242            let fd_ready = driver.new_dyn_fd_ready(pidfd.as_fd().as_raw_fd())?;
243            Ok(Self {
244                wait: Some(FdProcessWait::new(fd_ready)),
245                _owned_pidfd: Some(pidfd),
246                child,
247            })
248        }
249    }
250
251    impl PolledChild<pal::unix::process::Child> {
252        pub(super) fn new_inner(
253            driver: &(impl ?Sized + Driver),
254            child: pal::unix::process::Child,
255        ) -> io::Result<Self> {
256            let fd_ready = driver.new_dyn_fd_ready(child.as_fd().as_raw_fd())?;
257            Ok(Self {
258                wait: Some(FdProcessWait::new(fd_ready)),
259                _owned_pidfd: None,
260                child,
261            })
262        }
263    }
264}
265
266/// macOS: use kqueue EVFILT_PROC via `Driver::new_dyn_process_wait`.
267#[cfg(target_os = "macos")]
268mod macos_impl {
269    use super::*;
270    use crate::sys::process::WaitInner;
271
272    impl PolledChild<std::process::Child> {
273        pub(super) fn new_inner(
274            driver: &(impl ?Sized + Driver),
275            mut child: std::process::Child,
276        ) -> io::Result<Self> {
277            // If the caller already reaped the child, don't try to register
278            // notifications on its pid.
279            if child.try_wait()?.is_some() {
280                return Ok(Self::exited(child));
281            }
282            let wait = driver.new_dyn_process_wait(child.id() as i32)?;
283            Ok(Self {
284                wait: Some(WaitInner::new(wait)),
285                child,
286            })
287        }
288    }
289
290    impl PolledChild<pal::unix::process::Child> {
291        pub(super) fn new_inner(
292            driver: &(impl ?Sized + Driver),
293            mut child: pal::unix::process::Child,
294        ) -> io::Result<Self> {
295            // If the caller already reaped the child, don't try to register
296            // notifications on its pid.
297            if child.try_wait()?.is_some() {
298                return Ok(Self::exited(child));
299            }
300            let wait = driver.new_dyn_process_wait(child.id())?;
301            Ok(Self {
302                wait: Some(WaitInner::new(wait)),
303                child,
304            })
305        }
306    }
307}
308
309/// Windows: wait on process handle via `Driver::new_dyn_wait`.
310#[cfg(windows)]
311mod windows {
312    use super::*;
313    use crate::sys::process::HandleProcessWait;
314    use std::os::windows::prelude::*;
315
316    impl PolledChild<std::process::Child> {
317        pub(super) fn new_inner(
318            driver: &(impl ?Sized + Driver),
319            child: std::process::Child,
320        ) -> io::Result<Self> {
321            let handle = child.as_handle().as_raw_handle();
322            let wait = driver.new_dyn_wait(handle)?;
323            Ok(Self {
324                wait: Some(HandleProcessWait::new(wait)),
325                child,
326            })
327        }
328    }
329}