Skip to main content

rightkit_process/
process_handle.rs

1use std::{
2    io,
3    process::ExitStatus,
4    sync::{Arc, Mutex, MutexGuard},
5    time::{Duration, SystemTime},
6};
7#[cfg(unix)]
8type PlatformChild = std::process::Child;
9#[cfg(windows)]
10type PlatformChild = crate::spawn_windows::WindowsChild;
11#[cfg(unix)]
12use std::{thread, time::Instant};
13
14/// An independent exit reader. On Unix it shares std's cached reap state;
15/// on Windows it owns a duplicated process handle, never a borrowed handle value.
16/// Keeping this reader alive does not keep an owned process tree alive.
17#[derive(Debug)]
18pub struct ProcessHandle {
19    #[cfg(unix)]
20    child: Arc<Mutex<PlatformChild>>,
21    #[cfg(unix)]
22    pid: u32,
23    #[cfg(windows)]
24    handle: std::os::windows::io::OwnedHandle,
25    creation_time: Option<SystemTime>,
26    #[cfg(windows)]
27    creation_time_ticks: Option<u64>,
28}
29
30pub(crate) fn lock_child<T>(child: &Mutex<T>) -> MutexGuard<'_, T> {
31    child.lock().unwrap_or_else(|poison| poison.into_inner())
32}
33
34impl ProcessHandle {
35    pub(crate) fn new(
36        child: &Arc<Mutex<PlatformChild>>,
37        spawned_at: SystemTime,
38    ) -> io::Result<Self> {
39        #[cfg(unix)]
40        {
41            #[cfg(target_os = "macos")]
42            let creation_time = macos_creation_time(lock_child(child).id());
43            #[cfg(not(target_os = "macos"))]
44            let creation_time = Some(spawned_at);
45            let _ = spawned_at;
46            Ok(Self {
47                child: Arc::clone(child),
48                pid: lock_child(child).id(),
49                creation_time,
50            })
51        }
52        #[cfg(windows)]
53        {
54            use std::os::windows::io::AsHandle;
55            let handle = lock_child(child).as_handle().try_clone_to_owned()?;
56            let ticks = windows_creation_time(&handle);
57            let creation_time = ticks.and_then(|ticks| {
58                // FILETIME epoch precedes Unix by 11,644,473,600 seconds.
59                let ticks = ticks.checked_sub(116_444_736_000_000_000)?;
60                SystemTime::UNIX_EPOCH.checked_add(Duration::new(
61                    ticks / 10_000_000,
62                    ((ticks % 10_000_000) * 100) as u32,
63                ))
64            });
65            let _ = spawned_at;
66            Ok(Self {
67                handle,
68                creation_time,
69                creation_time_ticks: ticks,
70            })
71        }
72    }
73
74    pub fn try_clone(&self) -> io::Result<Self> {
75        Ok(Self {
76            #[cfg(unix)]
77            child: Arc::clone(&self.child),
78            #[cfg(unix)]
79            pid: self.pid,
80            #[cfg(windows)]
81            handle: self.handle.try_clone()?,
82            creation_time: self.creation_time,
83            #[cfg(windows)]
84            creation_time_ticks: self.creation_time_ticks,
85        })
86    }
87
88    /// Native creation time on Windows/macOS; spawn observation time on other Unix hosts.
89    pub fn creation_time(&self) -> Option<SystemTime> {
90        self.creation_time
91    }
92
93    /// Native Windows FILETIME, in 100 ns ticks since 1601-01-01 UTC.
94    #[cfg(windows)]
95    pub fn creation_time_ticks(&self) -> Option<u64> {
96        self.creation_time_ticks
97    }
98
99    pub fn try_wait(&self) -> io::Result<Option<ExitStatus>> {
100        #[cfg(unix)]
101        {
102            lock_child(&self.child).try_wait()
103        }
104        #[cfg(windows)]
105        {
106            self.windows_wait(0)
107        }
108    }
109
110    pub fn wait(&self) -> io::Result<ExitStatus> {
111        #[cfg(unix)]
112        {
113            #[cfg(any(
114                target_os = "linux",
115                target_os = "android",
116                target_os = "macos",
117                target_os = "ios",
118                target_os = "freebsd",
119                target_os = "openbsd",
120                target_os = "netbsd",
121                target_os = "dragonfly",
122                target_os = "solaris",
123                target_os = "illumos"
124            ))]
125            {
126                if let Some(status) = self.try_wait()? {
127                    return Ok(status);
128                }
129                loop {
130                    let mut info = std::mem::MaybeUninit::<nix::libc::siginfo_t>::uninit();
131                    // Block without reaping or holding the std-child lock. std's
132                    // waitpid below remains the sole reap/cache owner. This lets
133                    // concurrent timeouts & detached-drop stdio cleanup proceed.
134                    if unsafe {
135                        nix::libc::waitid(
136                            nix::libc::P_PID,
137                            self.pid as nix::libc::id_t,
138                            info.as_mut_ptr(),
139                            nix::libc::WEXITED | nix::libc::WNOWAIT,
140                        )
141                    } == 0
142                    {
143                        return self.try_wait()?.ok_or_else(|| {
144                            io::Error::other("completed process wait had no exit status")
145                        });
146                    }
147                    let error = io::Error::last_os_error();
148                    if error.raw_os_error() == Some(nix::libc::EINTR) {
149                        continue;
150                    }
151                    // Another reader may have reaped & cached status while waitid
152                    // was waking, causing ECHILD despite a valid cached result.
153                    if let Some(status) = self.try_wait()? {
154                        return Ok(status);
155                    }
156                    return Err(error);
157                }
158            }
159            #[cfg(not(any(
160                target_os = "linux",
161                target_os = "android",
162                target_os = "macos",
163                target_os = "ios",
164                target_os = "freebsd",
165                target_os = "openbsd",
166                target_os = "netbsd",
167                target_os = "dragonfly",
168                target_os = "solaris",
169                target_os = "illumos"
170            )))]
171            {
172                // Other Unix hosts use std's blocking waitpid & shared cache.
173                let mut child = lock_child(&self.child);
174                // An independent reader must not close owner stdin. OwnedChild::wait
175                // already closes it explicitly, matching std Child::wait.
176                let stdin = child.stdin.take();
177                let result = child.wait();
178                child.stdin = stdin;
179                result
180            }
181        }
182        #[cfg(windows)]
183        {
184            self.windows_wait(u32::MAX)?
185                .ok_or_else(|| io::Error::other("infinite process wait unexpectedly timed out"))
186        }
187    }
188
189    pub fn wait_timeout(&self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
190        #[cfg(windows)]
191        {
192            let started = std::time::Instant::now();
193            loop {
194                let remaining = timeout.saturating_sub(started.elapsed());
195                // Round up sub-millisecond waits; reserve INFINITE for wait().
196                let millis = remaining.as_millis()
197                    + u128::from(!remaining.subsec_nanos().is_multiple_of(1_000_000));
198                let millis = millis.min(u128::from(u32::MAX - 1)) as u32;
199                if let Some(status) = self.windows_wait(millis)? {
200                    return Ok(Some(status));
201                }
202                if started.elapsed() >= timeout {
203                    return Ok(None);
204                }
205            }
206        }
207        #[cfg(unix)]
208        {
209            let started = Instant::now();
210            loop {
211                if let Some(status) = self.try_wait()? {
212                    return Ok(Some(status));
213                }
214                if started.elapsed() >= timeout {
215                    return Ok(None);
216                }
217                thread::sleep(
218                    Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())),
219                );
220            }
221        }
222    }
223
224    #[cfg(windows)]
225    fn windows_wait(&self, millis: u32) -> io::Result<Option<ExitStatus>> {
226        use std::os::windows::{io::AsRawHandle, process::ExitStatusExt};
227        use windows::Win32::{
228            Foundation::{HANDLE, WAIT_OBJECT_0, WAIT_TIMEOUT},
229            System::Threading::{GetExitCodeProcess, WaitForSingleObject},
230        };
231        let handle = HANDLE(self.handle.as_raw_handle() as _);
232        match unsafe { WaitForSingleObject(handle, millis) } {
233            WAIT_TIMEOUT => Ok(None),
234            WAIT_OBJECT_0 => {
235                let mut code = 0;
236                unsafe { GetExitCodeProcess(handle, &mut code) }
237                    .map_err(crate::process_windows::os_error)?;
238                Ok(Some(ExitStatus::from_raw(code)))
239            }
240            _ => Err(io::Error::last_os_error()),
241        }
242    }
243}
244
245#[cfg(windows)]
246impl std::os::windows::io::AsHandle for ProcessHandle {
247    fn as_handle(&self) -> std::os::windows::io::BorrowedHandle<'_> {
248        std::os::windows::io::AsHandle::as_handle(&self.handle)
249    }
250}
251
252#[cfg(windows)]
253fn windows_creation_time(handle: &std::os::windows::io::OwnedHandle) -> Option<u64> {
254    use std::os::windows::io::AsRawHandle;
255    use windows::Win32::{
256        Foundation::{FILETIME, HANDLE},
257        System::Threading::GetProcessTimes,
258    };
259    let (mut creation, mut exit, mut kernel, mut user) = (
260        FILETIME::default(),
261        FILETIME::default(),
262        FILETIME::default(),
263        FILETIME::default(),
264    );
265    unsafe {
266        GetProcessTimes(
267            HANDLE(handle.as_raw_handle() as _),
268            &mut creation,
269            &mut exit,
270            &mut kernel,
271            &mut user,
272        )
273    }
274    .ok()?;
275    Some((u64::from(creation.dwHighDateTime) << 32) | u64::from(creation.dwLowDateTime))
276}
277
278#[cfg(target_os = "macos")]
279fn macos_creation_time(pid: u32) -> Option<SystemTime> {
280    use nix::libc;
281    let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::zeroed();
282    let size = std::mem::size_of::<libc::proc_bsdinfo>();
283    // libproc writes exactly proc_bsdinfo for this flavor; check size before reading it.
284    let written = unsafe {
285        libc::proc_pidinfo(
286            pid as i32,
287            libc::PROC_PIDTBSDINFO,
288            0,
289            info.as_mut_ptr().cast(),
290            size as i32,
291        )
292    };
293    if written != size as i32 {
294        return None;
295    }
296    let info = unsafe { info.assume_init() };
297    SystemTime::UNIX_EPOCH
298        .checked_add(Duration::from_secs(info.pbi_start_tvsec))?
299        .checked_add(Duration::from_micros(info.pbi_start_tvusec))
300}