Skip to main content

kunobi_daemon/
candidate.rs

1use std::{
2    io,
3    ops::{Deref, DerefMut},
4    process::{Child, ExitStatus},
5};
6
7#[cfg(feature = "local")]
8use crate::local::ProcessHandle;
9
10/// A newly spawned candidate is disposable until its selection commits.
11/// Dropping a selected candidate only reaps it; it never terminates the daemon.
12pub struct Candidate {
13    process: Process,
14    selected: bool,
15}
16enum Process {
17    Child {
18        child: Reaped,
19        #[cfg(feature = "local")]
20        exit: Option<ProcessHandle>,
21    },
22    #[cfg(feature = "launch")]
23    Daemon(crate::launch::DaemonChild),
24}
25impl Candidate {
26    /// Take ownership immediately after spawning a staged candidate.
27    ///
28    /// With `local` this also opens the candidate's exit handle, while
29    /// nothing has waited for the child, so the handle follows this process
30    /// even if its PID is reused after a later reap.
31    pub fn new(process: Child) -> Self {
32        Self {
33            process: Process::Child {
34                #[cfg(feature = "local")]
35                exit: ProcessHandle::for_child(&process).ok(),
36                child: Reaped::new(process),
37            },
38            selected: false,
39        }
40    }
41    /// OS process identity used by the live verifier.
42    pub fn id(&self) -> crate::ProcessId {
43        match &self.process {
44            Process::Child { child, .. } => crate::ProcessId::of(child),
45            #[cfg(feature = "launch")]
46            Process::Daemon(daemon) => daemon.id(),
47        }
48    }
49    /// Observe whether the candidate exited before readiness.
50    pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
51        match &mut self.process {
52            Process::Child { child, .. } => child.try_wait(),
53            #[cfg(feature = "launch")]
54            Process::Daemon(daemon) => daemon.try_wait(),
55        }
56    }
57    /// The candidate's exit as an event, for a verifier that waits on it
58    /// between probes instead of sleeping. The handle is opened by
59    /// [`Self::new`]; this fails if that could not be done, and
60    /// [`Self::wait_until`] then asks the child directly.
61    #[cfg(feature = "local")]
62    pub fn exit_handle(&mut self) -> io::Result<&mut ProcessHandle> {
63        match &mut self.process {
64            Process::Child { exit, .. } => exit
65                .as_mut()
66                .ok_or_else(|| io::Error::other("the candidate's exit handle could not be opened")),
67            #[cfg(feature = "launch")]
68            Process::Daemon(daemon) => Ok(daemon.exit_handle()),
69        }
70    }
71    /// Block until the candidate exits or `deadline` passes: its exit status,
72    /// or `None` while it still runs.
73    #[cfg(feature = "local")]
74    pub fn wait_until(&mut self, deadline: std::time::Instant) -> io::Result<Option<ExitStatus>> {
75        match &mut self.process {
76            Process::Child { child, exit } => {
77                if !crate::local::wait_child(|| child.try_wait(), exit.as_mut(), deadline)? {
78                    return Ok(None);
79                }
80                child.wait().map(Some)
81            }
82            #[cfg(feature = "launch")]
83            Process::Daemon(daemon) => daemon.wait_until(deadline),
84        }
85    }
86    /// Mark the selection boundary before doing repairable follow-up work.
87    pub fn selected(&mut self) {
88        self.selected = true;
89    }
90    /// Kill the candidate and wait for it to exit.
91    fn stop(&mut self) -> io::Result<ExitStatus> {
92        match &mut self.process {
93            Process::Child { child, .. } => {
94                child.kill()?;
95                child.wait()
96            }
97            #[cfg(feature = "launch")]
98            Process::Daemon(daemon) => {
99                daemon.kill()?;
100                daemon.wait()
101            }
102        }
103    }
104}
105/// A daemon started by [`DaemonCommand`](crate::launch::DaemonCommand) as a
106/// candidate: dropped before [`Candidate::selected`], it is stopped. Take its
107/// readiness channel first; the candidate keeps it but does not give it out.
108#[cfg(feature = "launch")]
109impl From<crate::launch::DaemonChild> for Candidate {
110    fn from(daemon: crate::launch::DaemonChild) -> Self {
111        Self {
112            process: Process::Daemon(daemon),
113            selected: false,
114        }
115    }
116}
117impl Drop for Candidate {
118    /// Stop an unselected candidate. Dropping its process then reaps it.
119    fn drop(&mut self) {
120        if !self.selected {
121            let _ = self.stop();
122        }
123    }
124}
125
126/// A child that is reaped, never stopped, when dropped: at once if it has
127/// exited, otherwise by a thread that waits for it. On Unix a child nothing
128/// waits for stays a zombie after it exits. Reaping a running child is best
129/// effort: if no thread can be created, nothing waits for it.
130#[derive(Debug)]
131pub(crate) struct Reaped(Option<Child>);
132impl Reaped {
133    pub(crate) fn new(child: Child) -> Self {
134        Self(Some(child))
135    }
136}
137impl Deref for Reaped {
138    type Target = Child;
139    fn deref(&self) -> &Child {
140        self.0
141            .as_ref()
142            .expect("the child is taken only when dropped")
143    }
144}
145impl DerefMut for Reaped {
146    fn deref_mut(&mut self) -> &mut Child {
147        self.0
148            .as_mut()
149            .expect("the child is taken only when dropped")
150    }
151}
152impl Drop for Reaped {
153    fn drop(&mut self) {
154        let Some(mut child) = self.0.take() else {
155            return;
156        };
157        if child.try_wait().is_ok_and(|status| status.is_some()) {
158            return;
159        }
160        // A failed spawn drops the closure and the child with it; the
161        // `try_wait` above was the last chance to reap it.
162        let _ = std::thread::Builder::new()
163            .name("daemon-reaper".into())
164            .spawn(move || {
165                let _ = child.wait();
166            });
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    #[cfg(feature = "local")]
173    #[test]
174    fn a_candidate_watches_its_process_from_the_moment_it_is_taken() {
175        // Opened later, after a `try_wait` had reaped the child, the handle
176        // could follow whatever process reused the PID.
177        let child = std::process::Command::new(std::env::current_exe().unwrap())
178            .arg("--list")
179            .stdout(std::process::Stdio::null())
180            .spawn()
181            .unwrap();
182        let pid = crate::ProcessId::of(&child);
183        let mut candidate = super::Candidate::new(child);
184        assert_eq!(candidate.exit_handle().unwrap().pid(), pid);
185        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
186        assert!(candidate.wait_until(deadline).unwrap().unwrap().success());
187    }
188}