1#[cfg(unix)]
8pub fn is_process_alive(pid: u32) -> bool {
9 let Ok(pid) = i32::try_from(pid) else {
10 return false;
11 };
12 let result = unsafe { libc::kill(pid, 0) };
13 result == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
14}
15
16#[cfg(windows)]
17pub fn is_process_alive(pid: u32) -> bool {
18 use windows_sys::Win32::Foundation::{CloseHandle, STILL_ACTIVE};
19 use windows_sys::Win32::System::Threading::{
20 GetExitCodeProcess, OpenProcess, PROCESS_QUERY_INFORMATION,
21 };
22
23 unsafe {
24 let handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
25 if handle == 0 {
26 return false;
27 }
28 let mut exit_code = 0u32;
29 let ok = GetExitCodeProcess(handle, &mut exit_code);
30 CloseHandle(handle);
31 ok != 0 && exit_code == STILL_ACTIVE as u32
32 }
33}
34
35#[cfg(not(any(unix, windows)))]
36pub fn is_process_alive(_pid: u32) -> bool {
37 false
38}
39
40#[cfg(target_os = "linux")]
46pub fn pid_start_time(pid: u32) -> Option<u64> {
47 let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
48 linux_process_identity_from_stat(&stat).map(|(_, start_time)| start_time)
49}
50
51#[cfg(target_os = "macos")]
52pub fn pid_start_time(pid: u32) -> Option<u64> {
53 macos_process_identity(pid).map(|identity| identity.start_time)
54}
55
56#[cfg(not(any(target_os = "linux", target_os = "macos")))]
57pub fn pid_start_time(_pid: u32) -> Option<u64> {
58 None
59}
60
61pub fn is_process_alive_with_identity(pid: u32, expected_start_time: Option<u64>) -> bool {
66 if !is_process_alive(pid) {
67 return false;
68 }
69
70 match expected_start_time {
71 Some(expected) => pid_start_time(pid) == Some(expected),
72 None => true,
73 }
74}
75
76#[cfg(target_os = "linux")]
83pub fn is_process_running_with_identity(pid: u32, expected_start_time: Option<u64>) -> bool {
84 let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
85 return false;
86 };
87 linux_process_identity_from_stat(&stat).is_some_and(|(state, start_time)| {
88 is_linux_process_state_running(state)
89 && expected_start_time
90 .map(|expected| expected == start_time)
91 .unwrap_or(true)
92 })
93}
94
95#[cfg(target_os = "macos")]
96pub fn is_process_running_with_identity(pid: u32, expected_start_time: Option<u64>) -> bool {
97 match macos_process_identity(pid) {
98 Some(identity) => {
99 identity.running
100 && expected_start_time
101 .map(|expected| expected == identity.start_time)
102 .unwrap_or(true)
103 }
104 None => expected_start_time.is_none() && is_process_alive(pid),
108 }
109}
110
111#[cfg(not(any(target_os = "linux", target_os = "macos")))]
112pub fn is_process_running_with_identity(pid: u32, expected_start_time: Option<u64>) -> bool {
113 is_process_alive_with_identity(pid, expected_start_time)
114}
115
116#[cfg(target_os = "macos")]
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118struct MacosProcessIdentity {
119 start_time: u64,
120 running: bool,
121}
122
123#[cfg(target_os = "macos")]
124fn macos_process_identity(pid: u32) -> Option<MacosProcessIdentity> {
125 let raw_pid = i32::try_from(pid).ok().filter(|pid| *pid > 0)?;
126 let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::zeroed();
127 let expected_size = std::mem::size_of::<libc::proc_bsdinfo>();
128 let read = unsafe {
131 libc::proc_pidinfo(
132 raw_pid,
133 libc::PROC_PIDTBSDINFO,
134 0,
135 info.as_mut_ptr().cast(),
136 i32::try_from(expected_size).ok()?,
137 )
138 };
139 if read != i32::try_from(expected_size).ok()? {
140 return None;
141 }
142 let info = unsafe { info.assume_init() };
144 Some(MacosProcessIdentity {
145 start_time: info
146 .pbi_start_tvsec
147 .saturating_mul(1_000_000)
148 .saturating_add(info.pbi_start_tvusec),
149 running: info.pbi_status != libc::SZOMB,
150 })
151}
152
153#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
161pub(crate) fn wait_for_process_stop_with_identity(
162 pid: u32,
163 expected_start_time: u64,
164 timeout: std::time::Duration,
165) -> bool {
166 let deadline = std::time::Instant::now() + timeout;
167 loop {
168 if !is_process_running_with_identity(pid, Some(expected_start_time)) {
169 let _ = try_reap_exited_child_with_identity(pid, expected_start_time);
174 return true;
175 }
176 if std::time::Instant::now() >= deadline {
177 return false;
178 }
179 std::thread::sleep(std::time::Duration::from_millis(10));
180 }
181}
182
183#[cfg(target_os = "linux")]
192pub(crate) fn wait_for_process_exit_with_identity(
193 pid: u32,
194 expected_start_time: u64,
195 timeout: std::time::Duration,
196) -> bool {
197 let deadline = std::time::Instant::now() + timeout;
198 loop {
199 if !is_process_alive_with_identity(pid, Some(expected_start_time)) {
200 return true;
201 }
202 if !is_process_running_with_identity(pid, Some(expected_start_time))
203 && try_reap_exited_child_with_identity(pid, expected_start_time)
204 {
205 return true;
206 }
207 if std::time::Instant::now() >= deadline {
208 return false;
209 }
210 std::thread::sleep(std::time::Duration::from_millis(10));
211 }
212}
213
214#[cfg(unix)]
219fn try_reap_exited_child_with_identity(pid: u32, expected_start_time: u64) -> bool {
220 if !is_process_alive_with_identity(pid, Some(expected_start_time)) {
221 return true;
222 }
223
224 let Ok(raw_pid) = i32::try_from(pid) else {
225 return false;
226 };
227 let mut status = 0;
228 let waited = unsafe { libc::waitpid(raw_pid, &mut status, libc::WNOHANG) };
229 waited == raw_pid || !is_process_alive_with_identity(pid, Some(expected_start_time))
230}
231
232#[cfg(not(unix))]
233#[allow(dead_code)]
234fn try_reap_exited_child_with_identity(pid: u32, expected_start_time: u64) -> bool {
235 !is_process_alive_with_identity(pid, Some(expected_start_time))
236}
237
238#[cfg(target_os = "linux")]
239fn linux_process_identity_from_stat(stat: &str) -> Option<(char, u64)> {
240 let fields: Vec<&str> = stat
243 .get(stat.rfind(')')? + 1..)?
244 .split_whitespace()
245 .collect();
246 let state = fields.first()?.chars().next()?;
247 let start_time = fields.get(19)?.parse().ok()?;
248 Some((state, start_time))
249}
250
251#[cfg(target_os = "linux")]
252const fn is_linux_process_state_running(state: char) -> bool {
253 !matches!(state, 'Z' | 'X' | 'x')
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn current_process_is_alive() {
262 assert!(is_process_alive(std::process::id()));
263 }
264
265 #[test]
266 fn missing_process_is_not_alive() {
267 assert!(!is_process_alive(0x7fff_fffe));
268 }
269
270 #[cfg(target_os = "macos")]
271 #[test]
272 fn macos_identity_distinguishes_a_reused_pid() {
273 let pid = std::process::id();
274 let start_time = pid_start_time(pid);
275 assert!(
276 start_time.is_some(),
277 "live process must have a start-time token"
278 );
279 assert!(is_process_alive_with_identity(pid, start_time));
280 assert!(!is_process_alive_with_identity(pid, Some(u64::MAX)));
281 assert!(is_process_running_with_identity(pid, start_time));
282 }
283
284 #[cfg(target_os = "macos")]
285 #[test]
286 #[allow(clippy::zombie_processes)] fn macos_running_identity_rejects_a_zombie() {
288 let mut child = std::process::Command::new("/bin/sleep")
289 .arg("30")
290 .spawn()
291 .unwrap();
292 let pid = child.id();
293 let start_time = pid_start_time(pid).expect("capture child identity");
294 child.kill().expect("terminate child without reaping it");
295 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
296 while is_process_running_with_identity(pid, Some(start_time))
297 && std::time::Instant::now() < deadline
298 {
299 std::thread::sleep(std::time::Duration::from_millis(5));
300 }
301
302 assert!(!is_process_running_with_identity(pid, Some(start_time)));
303 let _ = child.wait();
304 }
305
306 #[cfg(target_os = "linux")]
307 #[test]
308 fn parses_start_time_after_complex_command_name() {
309 let stat =
310 "123 (command (with) spaces) S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 4242";
311 assert_eq!(linux_process_identity_from_stat(stat), Some(('S', 4242)));
312 assert_eq!(linux_process_identity_from_stat("malformed"), None);
313 assert_eq!(linux_process_identity_from_stat("123 (short) S 1"), None);
314 }
315
316 #[cfg(target_os = "linux")]
317 #[test]
318 fn identity_rejects_a_reused_pid() {
319 let pid = std::process::id();
320 let start_time = pid_start_time(pid);
321 assert!(start_time.is_some());
322 assert!(is_process_alive_with_identity(pid, start_time));
323 assert!(!is_process_alive_with_identity(pid, Some(u64::MAX)));
324 assert!(is_process_alive_with_identity(pid, None));
325 assert!(!is_process_alive_with_identity(0x7fff_fffe, None));
326 assert!(is_process_running_with_identity(pid, start_time));
327 }
328
329 #[cfg(target_os = "linux")]
330 #[test]
331 fn classifies_zombie_and_dead_states_as_completed() {
332 for state in ['Z', 'X', 'x'] {
333 let stat = format!(
334 "123 (completed worker) {state} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 4242"
335 );
336 assert_eq!(linux_process_identity_from_stat(&stat), Some((state, 4242)));
337 assert!(!is_linux_process_state_running(state));
338 }
339 for state in ['R', 'S', 'D', 'T', 't', 'I'] {
340 assert!(is_linux_process_state_running(state));
341 }
342 }
343
344 #[cfg(target_os = "linux")]
345 #[test]
346 #[allow(clippy::zombie_processes)] fn recovered_identity_reaps_an_exited_child() {
348 let child = std::process::Command::new("true").spawn().unwrap();
349 let pid = child.id();
350 let start_time = pid_start_time(pid).unwrap();
351 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
352 while is_process_running_with_identity(pid, Some(start_time))
353 && std::time::Instant::now() < deadline
354 {
355 std::thread::sleep(std::time::Duration::from_millis(5));
356 }
357
358 assert!(is_process_alive_with_identity(pid, Some(start_time)));
359 assert!(!is_process_running_with_identity(pid, Some(start_time)));
360 assert!(wait_for_process_exit_with_identity(
361 pid,
362 start_time,
363 std::time::Duration::from_secs(1),
364 ));
365 assert!(!is_process_alive_with_identity(pid, Some(start_time)));
366 }
367
368 #[cfg(target_os = "linux")]
369 #[test]
370 #[allow(clippy::zombie_processes)] fn stop_waiter_reaps_an_exited_child() {
372 let child = std::process::Command::new("true").spawn().unwrap();
373 let pid = child.id();
374 let start_time = pid_start_time(pid).unwrap();
375 drop(child);
376
377 assert!(wait_for_process_stop_with_identity(
378 pid,
379 start_time,
380 std::time::Duration::from_secs(1),
381 ));
382 assert!(!is_process_alive_with_identity(pid, Some(start_time)));
383 }
384}