1use std::io;
4use std::process::{Child, ExitStatus};
5use std::time::Instant;
6
7use super::ProcessState;
8use crate::ProcessId;
9
10#[cfg(any(target_os = "linux", target_os = "android"))]
11mod pidfd;
12#[cfg(any(target_os = "linux", target_os = "android"))]
13use self::pidfd as sys;
14
15#[cfg(any(
16 target_vendor = "apple",
17 target_os = "freebsd",
18 target_os = "dragonfly",
19 target_os = "openbsd",
20 target_os = "netbsd"
21))]
22mod kqueue;
23#[cfg(any(
24 target_vendor = "apple",
25 target_os = "freebsd",
26 target_os = "dragonfly",
27 target_os = "openbsd",
28 target_os = "netbsd"
29))]
30use self::kqueue as sys;
31
32#[cfg(all(
33 unix,
34 not(any(
35 target_os = "linux",
36 target_os = "android",
37 target_vendor = "apple",
38 target_os = "freebsd",
39 target_os = "dragonfly",
40 target_os = "openbsd",
41 target_os = "netbsd"
42 ))
43))]
44mod unsupported;
45#[cfg(all(
46 unix,
47 not(any(
48 target_os = "linux",
49 target_os = "android",
50 target_vendor = "apple",
51 target_os = "freebsd",
52 target_os = "dragonfly",
53 target_os = "openbsd",
54 target_os = "netbsd"
55 ))
56))]
57use self::unsupported as sys;
58
59#[cfg(windows)]
60mod windows;
61#[cfg(windows)]
62use self::windows as sys;
63
64#[cfg(windows)]
65use self::windows::unknown_state as fallback_state;
66#[cfg(unix)]
67use super::unix::signal_state as fallback_state;
68
69enum Opened {
71 Watching(sys::Event),
73 Gone,
75 #[cfg(unix)]
77 Unsupported,
78}
79
80#[derive(Debug)]
81enum Source {
82 Event(sys::Event),
83 Exited,
84 #[cfg(unix)]
86 Pid,
87}
88
89#[derive(Debug)]
124pub struct ProcessHandle {
125 pid: ProcessId,
126 source: Source,
127}
128
129impl ProcessHandle {
130 pub fn open(pid: ProcessId) -> io::Result<Self> {
147 let source = match sys::open(pid.get())? {
148 Opened::Watching(event) => Source::Event(event),
149 Opened::Gone => Source::Exited,
150 #[cfg(unix)]
151 Opened::Unsupported => Source::Pid,
152 };
153 Ok(Self { pid, source })
154 }
155
156 pub fn for_child(child: &Child) -> io::Result<Self> {
164 Self::open(ProcessId::of(child))
165 }
166
167 pub fn pid(&self) -> ProcessId {
169 self.pid
170 }
171
172 pub fn is_pid_only(&self) -> bool {
175 #[cfg(unix)]
176 {
177 matches!(self.source, Source::Pid)
178 }
179 #[cfg(windows)]
180 {
181 false
182 }
183 }
184
185 pub fn has_exited(&mut self) -> io::Result<bool> {
187 self.wait_for(Some(Instant::now()))
188 }
189
190 pub fn wait(&mut self) -> io::Result<()> {
192 self.wait_for(None).map(drop)
193 }
194
195 pub fn wait_until(&mut self, deadline: Instant) -> io::Result<bool> {
197 self.wait_for(Some(deadline))
198 }
199
200 fn wait_for(&mut self, deadline: Option<Instant>) -> io::Result<bool> {
201 let exited = match &mut self.source {
202 Source::Event(event) => event.wait(deadline)?,
203 Source::Exited => true,
204 #[cfg(unix)]
205 Source::Pid => pid_wait(self.pid.get(), deadline)?,
206 };
207 if exited {
208 self.source = Source::Exited;
211 }
212 Ok(exited)
213 }
214
215 #[cfg(feature = "async")]
222 pub async fn exited(&mut self) -> io::Result<()> {
223 match &mut self.source {
224 Source::Event(event) => event.exited().await?,
225 Source::Exited => {}
226 #[cfg(unix)]
227 Source::Pid => {
228 let mut pause = crate::backoff::FIRST;
229 while !pid_exited(self.pid.get())? {
230 let nap = crate::backoff::next_pause(&mut pause, None).expect("no deadline");
231 #[expect(
232 clippy::disallowed_methods,
233 reason = "Only the PID fallback polls; supported platforms await the OS exit event."
234 )]
235 tokio::time::sleep(nap).await;
236 }
237 }
238 }
239 self.source = Source::Exited;
240 Ok(())
241 }
242
243 #[cfg(feature = "async")]
245 pub async fn exited_until(&mut self, deadline: tokio::time::Instant) -> io::Result<bool> {
246 match tokio::time::timeout_at(deadline, self.exited()).await {
247 Ok(result) => result.map(|()| true),
248 Err(_) => Ok(false),
249 }
250 }
251}
252
253pub fn process_state(pid: ProcessId) -> ProcessState {
269 match ProcessHandle::open(pid).and_then(|mut handle| handle.has_exited()) {
270 Ok(true) => ProcessState::Exited,
271 Ok(false) => ProcessState::Alive,
272 Err(_) => fallback_state(pid.get()),
273 }
274}
275
276pub fn process_has_exited(pid: ProcessId) -> bool {
278 process_state(pid) == ProcessState::Exited
279}
280
281#[cfg_attr(
285 not(any(target_os = "linux", target_os = "android", windows)),
286 allow(dead_code)
287)]
288fn millis_until(deadline: Instant, now: Instant) -> u128 {
289 deadline
290 .saturating_duration_since(now)
291 .as_nanos()
292 .div_ceil(1_000_000)
293}
294
295#[cfg(unix)]
296fn pid_exited(pid: u32) -> io::Result<bool> {
297 match fallback_state(pid) {
298 ProcessState::Exited => Ok(true),
299 ProcessState::Alive => Ok(false),
300 ProcessState::Unknown => Err(io::Error::other(
301 "the OS did not say whether the process exited",
302 )),
303 }
304}
305
306#[cfg(unix)]
308fn pid_wait(pid: u32, deadline: Option<Instant>) -> io::Result<bool> {
309 crate::backoff::poll(|| pid_exited(pid), deadline)
310}
311
312pub(crate) fn wait_child(
322 mut try_wait: impl FnMut() -> io::Result<Option<ExitStatus>>,
323 exit: Option<&mut ProcessHandle>,
324 deadline: Instant,
325) -> io::Result<bool> {
326 match exit {
327 Some(exit) if !exit.is_pid_only() => exit.wait_until(deadline),
328 _ => crate::backoff::poll(|| Ok(try_wait()?.is_some()), Some(deadline)),
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use std::io::Read;
336 use std::process::{Command, Stdio};
337 use std::time::Duration;
338
339 #[test]
341 #[ignore = "helper process for the exit-event tests"]
342 fn blocked_child_helper() {
343 if std::env::var_os("KDAEMON_BLOCKED_CHILD").is_none() {
344 return;
345 }
346 let _ = std::io::stdin().read_to_end(&mut Vec::new());
347 std::process::exit(3);
348 }
349
350 fn blocked_child() -> Child {
353 Command::new(std::env::current_exe().unwrap())
354 .args([
355 "--exact",
356 "local::process::tests::blocked_child_helper",
357 "--ignored",
358 "--nocapture",
359 "--test-threads=1",
360 ])
361 .env("KDAEMON_BLOCKED_CHILD", "1")
362 .stdin(Stdio::piped())
363 .stdout(Stdio::null())
364 .stderr(Stdio::null())
365 .spawn()
366 .unwrap()
367 }
368
369 fn release(child: &mut Child) {
370 drop(child.stdin.take());
371 }
372
373 #[test]
374 fn the_exit_event_fires_only_once_the_process_exits() {
375 let mut child = blocked_child();
376 let mut exit = ProcessHandle::for_child(&child).unwrap();
377 assert_eq!(exit.pid(), ProcessId::of(&child));
378 assert!(!exit.is_pid_only(), "no exit event on this system");
379 assert!(!exit.has_exited().unwrap());
380
381 let started = Instant::now();
384 assert!(
385 !exit
386 .wait_until(started + Duration::from_millis(100))
387 .unwrap()
388 );
389 assert!(started.elapsed() >= Duration::from_millis(100));
390 assert!(child.try_wait().unwrap().is_none());
391
392 release(&mut child);
393 assert!(
394 exit.wait_until(Instant::now() + Duration::from_secs(30))
395 .unwrap()
396 );
397 assert!(exit.has_exited().unwrap());
398 assert_eq!(child.wait().unwrap().code(), Some(3));
401 assert!(exit.has_exited().unwrap());
403 exit.wait().unwrap();
404 }
405
406 #[test]
407 fn a_blocking_wait_returns_when_the_process_exits_and_not_before() {
408 let mut child = blocked_child();
409 let mut exit = ProcessHandle::for_child(&child).unwrap();
410 let (done, waited) = std::sync::mpsc::channel();
411 let waiter = std::thread::spawn(move || {
412 let result = exit.wait();
413 let _ = done.send(());
414 result
415 });
416 assert!(
417 waited.recv_timeout(Duration::from_millis(200)).is_err(),
418 "the wait returned while the process was still running"
419 );
420 release(&mut child);
421 waiter.join().unwrap().unwrap();
422 assert_eq!(child.wait().unwrap().code(), Some(3));
423 }
424
425 #[test]
426 fn an_exited_child_not_yet_waited_for_reads_as_exited() {
427 let mut child = blocked_child();
430 let mut first = ProcessHandle::for_child(&child).unwrap();
431 release(&mut child);
432 first.wait().unwrap();
433
434 let mut late = ProcessHandle::open(ProcessId::of(&child)).unwrap();
436 assert!(late.has_exited().unwrap());
437 assert_eq!(process_state(ProcessId::of(&child)), ProcessState::Exited);
438 assert!(process_has_exited(ProcessId::of(&child)));
439 assert_eq!(child.wait().unwrap().code(), Some(3));
440 }
441
442 #[test]
443 fn a_process_that_is_gone_reads_as_exited_at_once() {
444 let mut child = blocked_child();
445 release(&mut child);
446 child.wait().unwrap();
447 let mut exit = ProcessHandle::open(ProcessId::of(&child)).unwrap();
449 assert!(exit.has_exited().unwrap());
450 exit.wait().unwrap();
451 assert_eq!(process_state(ProcessId::of(&child)), ProcessState::Exited);
452 }
453
454 #[test]
455 fn the_current_process_is_alive() {
456 let mut own = ProcessHandle::open(ProcessId::current()).unwrap();
457 assert!(!own.has_exited().unwrap());
458 assert_eq!(process_state(ProcessId::current()), ProcessState::Alive);
459 assert!(!process_has_exited(ProcessId::current()));
460 }
461
462 #[test]
463 fn deadlines_round_up_to_whole_milliseconds() {
464 let now = Instant::now();
465 assert_eq!(millis_until(now, now), 0);
466 assert_eq!(millis_until(now, now + Duration::from_secs(1)), 0);
467 assert_eq!(millis_until(now + Duration::from_nanos(1), now), 1);
468 assert_eq!(millis_until(now + Duration::from_millis(1), now), 1);
469 assert_eq!(
470 millis_until(
471 now + Duration::from_millis(1) + Duration::from_nanos(1),
472 now
473 ),
474 2
475 );
476 assert_eq!(millis_until(now + Duration::from_secs(5), now), 5000);
477 }
478
479 #[cfg(unix)]
480 fn pid_only(child: &Child) -> ProcessHandle {
481 ProcessHandle {
482 pid: ProcessId::of(child),
483 source: Source::Pid,
484 }
485 }
486
487 #[cfg(unix)]
488 #[test]
489 fn the_pid_fallback_waits_for_the_process_to_go() {
490 let mut child = blocked_child();
491 let mut exit = pid_only(&child);
492 assert!(exit.is_pid_only());
493 assert!(!exit.has_exited().unwrap());
494 let started = Instant::now();
495 assert!(
496 !exit
497 .wait_until(started + Duration::from_millis(100))
498 .unwrap()
499 );
500 assert!(started.elapsed() >= Duration::from_millis(100));
501
502 release(&mut child);
505 let reaper = std::thread::spawn(move || child.wait().unwrap());
506 assert!(
507 exit.wait_until(Instant::now() + Duration::from_secs(30))
508 .unwrap()
509 );
510 assert!(exit.has_exited().unwrap());
511 assert_eq!(reaper.join().unwrap().code(), Some(3));
512 }
513
514 #[cfg(target_os = "linux")]
516 fn listed(pid: u32) -> bool {
517 std::path::Path::new(&format!("/proc/{pid}")).exists()
518 }
519
520 #[test]
521 fn an_owned_child_is_waited_for_on_its_exit_event_and_left_unreaped() {
522 let mut child = blocked_child();
523 let mut exit = ProcessHandle::for_child(&child).unwrap();
524 let deadline = Instant::now() + Duration::from_millis(100);
525 assert!(!wait_child(|| child.try_wait(), Some(&mut exit), deadline).unwrap());
526 assert!(Instant::now() >= deadline);
527
528 release(&mut child);
529 let deadline = Instant::now() + Duration::from_secs(30);
530 assert!(wait_child(|| child.try_wait(), Some(&mut exit), deadline).unwrap());
531 #[cfg(target_os = "linux")]
533 assert!(listed(child.id()));
534 assert_eq!(child.wait().unwrap().code(), Some(3));
535 }
536
537 #[cfg(unix)]
540 #[test]
541 fn an_owned_child_whose_handle_follows_only_its_pid_is_asked_directly() {
542 let mut child = blocked_child();
543 let mut exit = pid_only(&child);
544 release(&mut child);
545 let started = Instant::now();
546 let deadline = started + Duration::from_secs(30);
547 assert!(wait_child(|| child.try_wait(), Some(&mut exit), deadline).unwrap());
548 assert!(started.elapsed() < Duration::from_secs(20));
549 #[cfg(target_os = "linux")]
551 assert!(!listed(child.id()));
552 assert_eq!(child.wait().unwrap().code(), Some(3));
553 }
554
555 #[test]
556 fn an_owned_child_without_a_handle_is_asked_directly() {
557 let mut child = blocked_child();
558 let deadline = Instant::now() + Duration::from_millis(100);
559 assert!(!wait_child(|| child.try_wait(), None, deadline).unwrap());
560 assert!(Instant::now() >= deadline);
561
562 release(&mut child);
563 let deadline = Instant::now() + Duration::from_secs(30);
564 assert!(wait_child(|| child.try_wait(), None, deadline).unwrap());
565 assert_eq!(child.wait().unwrap().code(), Some(3));
566 }
567
568 #[cfg(feature = "async")]
569 #[tokio::test]
570 async fn the_async_exit_event_fires_only_once_the_process_exits() {
571 let mut child = blocked_child();
572 let mut exit = ProcessHandle::for_child(&child).unwrap();
573 assert!(
574 tokio::time::timeout(Duration::from_millis(100), exit.exited())
575 .await
576 .is_err(),
577 "the exit future completed while the process was still running"
578 );
579 let deadline = tokio::time::Instant::now() + Duration::from_millis(50);
580 assert!(!exit.exited_until(deadline).await.unwrap());
581
582 release(&mut child);
583 let deadline = tokio::time::Instant::now() + Duration::from_secs(30);
584 assert!(exit.exited_until(deadline).await.unwrap());
585 exit.exited().await.unwrap();
586 assert!(exit.has_exited().unwrap());
587 assert_eq!(child.wait().unwrap().code(), Some(3));
588 }
589
590 #[cfg(all(unix, feature = "async"))]
591 #[tokio::test]
592 async fn the_async_pid_fallback_waits_for_the_process_to_go() {
593 let mut child = blocked_child();
594 let mut exit = pid_only(&child);
595 assert!(
596 tokio::time::timeout(Duration::from_millis(100), exit.exited())
597 .await
598 .is_err()
599 );
600 release(&mut child);
601 let reaper = std::thread::spawn(move || child.wait().unwrap());
602 let deadline = tokio::time::Instant::now() + Duration::from_secs(30);
603 assert!(exit.exited_until(deadline).await.unwrap());
604 assert_eq!(reaper.join().unwrap().code(), Some(3));
605 }
606}