Skip to main content

kunobi_daemon/local/
process.rs

1//! Process exit as an event: [`ProcessHandle`], and the PID checks built on it.
2
3use 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
69/// What opening a process found.
70enum Opened {
71    /// The OS will report the process's exit through this event.
72    Watching(sys::Event),
73    /// No process has this PID: it exited, or never existed.
74    Gone,
75    /// This system cannot report the exit as an event.
76    #[cfg(unix)]
77    Unsupported,
78}
79
80#[derive(Debug)]
81enum Source {
82    Event(sys::Event),
83    Exited,
84    /// No exit event: check whether the PID still names a running process.
85    #[cfg(unix)]
86    Pid,
87}
88
89/// One process whose exit this process can wait for.
90///
91/// The OS tells the handle when the process exits, so nothing checks a PID on
92/// a timer, and a PID the OS later gives to another process does not change
93/// the answer:
94///
95/// | Platform | Exit event |
96/// | --- | --- |
97/// | Linux, Android | a pidfd (`pidfd_open`, Linux 5.3 and later), readable once the process exits |
98/// | macOS, FreeBSD, NetBSD, OpenBSD, DragonFly | a kqueue watching `EVFILT_PROC` for `NOTE_EXIT` |
99/// | Windows | a process handle, signaled once the process exits |
100///
101/// Open it with [`ProcessHandle::open`] while the PID is known to name the
102/// process you mean, for example from the peer credentials of a connection
103/// that is still open, or with [`ProcessHandle::for_child`] for a child that
104/// has not been waited for. From then on the handle follows that process, not
105/// its PID.
106///
107/// Waiting reports only that the process exited. Its exit status belongs to
108/// its parent: for a child, call `wait` or `try_wait` on the child once the
109/// handle reports the exit. An error means this process could not find out,
110/// never that the watched process failed.
111///
112/// # Fallback without an exit event
113///
114/// On Linux before 5.3, where `pidfd_open` is missing or a seccomp policy
115/// refuses it, and on other Unix systems, the handle checks the PID instead, with a
116/// pause that grows to 50 ms between checks. That fallback has the PID's two
117/// weaknesses: a PID reused by another process reads as still running, and,
118/// except on Linux, a child that exited but has not been waited for reads as
119/// running too. [`ProcessHandle::is_pid_only`] reports it. The crate's waits
120/// for its own children (`launch::DaemonChild`, `Candidate` and the warmup
121/// reaper) then ask the child through `try_wait` instead, and do the same
122/// when the handle could not be opened at all.
123#[derive(Debug)]
124pub struct ProcessHandle {
125    pid: ProcessId,
126    source: Source,
127}
128
129impl ProcessHandle {
130    /// Watch the process that has `pid` now.
131    ///
132    /// A process that already exited, or a PID no process has, gives a
133    /// handle that reports the exit at once.
134    ///
135    /// The process need not be a child. Linux takes the PID in this
136    /// process's PID namespace, and macOS lets any process be watched.
137    /// FreeBSD watches only a process this user may debug and falls back to
138    /// the PID for others. Windows needs the right to wait on the process,
139    /// which an elevated or protected process may refuse with
140    /// `PermissionDenied`.
141    ///
142    /// A PID read from a discovery record may already name another process.
143    /// Take it from the peer credentials of a live connection instead
144    /// ([`super::peer::PeerCredentials`]), and open the handle while that
145    /// connection is still open.
146    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    /// Watch a child of this process.
157    ///
158    /// On Windows the child's own handle keeps its PID from being reused. On
159    /// Unix the PID stays the child's until it is waited for, so open the
160    /// handle before anything waits for it. A process that ignores SIGCHLD has
161    /// its children reaped by the kernel as they exit, and there a child can
162    /// be gone, and its PID reused, before this runs.
163    pub fn for_child(child: &Child) -> io::Result<Self> {
164        Self::open(ProcessId::of(child))
165    }
166
167    /// The PID this handle was opened with.
168    pub fn pid(&self) -> ProcessId {
169        self.pid
170    }
171
172    /// True when this handle checks the PID because no exit event was
173    /// available. See the fallback section above.
174    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    /// Whether the process has exited, without blocking.
186    pub fn has_exited(&mut self) -> io::Result<bool> {
187        self.wait_for(Some(Instant::now()))
188    }
189
190    /// Block until the process exits.
191    pub fn wait(&mut self) -> io::Result<()> {
192        self.wait_for(None).map(drop)
193    }
194
195    /// Block until the process exits or `deadline` passes. True if it exited.
196    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            // A kqueue reports the exit once; remember it, and release the
209            // descriptor or handle now that it has nothing more to say.
210            self.source = Source::Exited;
211        }
212        Ok(exited)
213    }
214
215    /// Complete when the process exits.
216    ///
217    /// On Unix this registers the descriptor with the Tokio reactor, so it
218    /// needs a runtime with I/O enabled (`enable_io` or `enable_all`). On
219    /// Windows a thread-pool wait wakes it. Cancelling the future stops only
220    /// this wait.
221    #[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    /// As [`Self::exited`], giving up at `deadline`. True if it exited.
244    #[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
253/// What the OS establishes about `pid`: see [`ProcessState`].
254///
255/// This opens a [`ProcessHandle`] and checks it once. With only a PID the
256/// answer is about whichever process holds that PID now; to follow one
257/// process, keep a handle opened while the PID was known to be it. Where the
258/// handle cannot be opened, for example because access was denied, Unix
259/// falls back to `kill(pid, 0)`, which reports a process this user may not
260/// signal as alive, and Windows reports `Unknown`.
261///
262/// PID 1 is answered like any other: on Unix it is init or launchd, or a
263/// container's entrypoint, which may be the daemon itself, so it reads as
264/// alive. Only the caller knows whether its daemon can run as an entrypoint;
265/// one that cannot should treat a record naming PID 1 as corrupt rather than
266/// as a live owner. PID 0 never reaches this function: [`ProcessId`] excludes
267/// it.
268pub 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
276/// True only when the OS establishes that a previously verified PID exited.
277pub fn process_has_exited(pid: ProcessId) -> bool {
278    process_state(pid) == ProcessState::Exited
279}
280
281/// Whole milliseconds from `now` to `deadline`, rounded up so that a wait for
282/// that long never ends before the deadline. Zero once it has passed.
283// A kqueue takes its timeout in nanoseconds.
284#[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/// The PID fallback's blocking wait.
307#[cfg(unix)]
308fn pid_wait(pid: u32, deadline: Option<Instant>) -> io::Result<bool> {
309    crate::backoff::poll(|| pid_exited(pid), deadline)
310}
311
312/// Wait until a child this process owns exits or `deadline` passes. True if
313/// it exited.
314///
315/// `exit` is the child's handle, or `None` if it could not be opened, and
316/// `try_wait` asks the child itself. Without an exit event, whether for want
317/// of a handle or because the handle only follows the PID, this polls
318/// `try_wait`: a PID check outside Linux reads an exited child that has not
319/// been waited for as running, but the child's parent can tell. That path
320/// reaps the child, and its status stays with it.
321pub(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    /// Run by [`blocked_child`]: block until stdin closes, then exit 3.
340    #[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    /// A child that runs until its stdin is closed, then exits with code 3.
351    /// Dropping its `stdin` releases it; nothing else does.
352    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        // The child cannot exit before it is released, so this must wait the
382        // whole time and then report no exit.
383        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        // The event fired before the status was collected, and the status
399        // is still the child's to report.
400        assert_eq!(child.wait().unwrap().code(), Some(3));
401        // Reaping does not make the handle forget.
402        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        // Before the exit event, a zombie answered kill(pid, 0) and read as
428        // alive everywhere except Linux.
429        let mut child = blocked_child();
430        let mut first = ProcessHandle::for_child(&child).unwrap();
431        release(&mut child);
432        first.wait().unwrap();
433
434        // Exited, not yet waited for: its PID still names it.
435        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        // Reaped, so no process has the PID (barring reuse within this test).
448        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        // Without /proc the fallback cannot tell a zombie from a running
503        // process, so reap the child where the test does not wait.
504        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    /// Whether the kernel still lists `pid`, as a process or a zombie.
515    #[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        // The event leaves the child for its owner to wait for.
532        #[cfg(target_os = "linux")]
533        assert!(listed(child.id()));
534        assert_eq!(child.wait().unwrap().code(), Some(3));
535    }
536
537    /// Outside Linux a PID check reads an exited, unreaped child as running,
538    /// so the wait asks the child and must end promptly everywhere.
539    #[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        // Asked through try_wait, the child is reaped and keeps its status.
550        #[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}