Skip to main content

rightkit_daemon/
supervisor.rs

1//! A supervisor loop for a daemon-pipe child (issue #295).
2//!
3//! A [`PrivateChild`] is an engine the app started itself and talks to over the daemon pipe. The
4//! `rightkit-job-queue` engine supervisor needs a sidecar stdio `hello` frame, which a daemon
5//! service does not speak; this one watches such a child with the two `rightkit-child-process`
6//! trackers instead:
7//!
8//! * **poll**: every [`SupervisorConfig::poll_interval`] the loop asks whether the child has
9//!   exited (`try_wait`, no reaping surprises: the supervisor owns the child);
10//! * **probe**: a caller-supplied closure says whether the child answers (the usual one is
11//!   [`ping_probe`]: connect with the child's own secret, verify the host, call `ping`). Each
12//!   result is fed to a [`HealthTracker`];
13//! * **relaunch**: an exit, an unhealthy child or a startup that never became healthy is stopped
14//!   (stdin closed first, then the tree is killed after [`SupervisorConfig::stop_grace`]) and the
15//!   [`RestartTracker`] decides: relaunch after its backoff, wait for demand, or give up
16//!   ([`SupervisorEvent::Exhausted`]).
17//!
18//! Decisions that are easy to get wrong, and what was chosen:
19//!
20//! * While a generation has never answered a probe, failures are expected (it is starting), so
21//!   only [`HealthOutcome::StartupTimedOut`] acts; `Unhealthy` acts only after the first success.
22//! * The first successful probe calls [`RestartTracker::ready`], so a child that comes up and later
23//!   crashes starts again from the base backoff (per the restart policy's own rules).
24//! * A failed launch (the closure returned an error) is charged to the restart budget like a
25//!   crash, so a binary that cannot start is not retried at poll speed forever.
26//! * [`SupervisorHandle::restart_now`] on a running child counts as an exit of that generation.
27//!   After the policy gave up ([`SupervisorState::Exhausted`]) it forgets the budget and starts
28//!   again: an explicit user start.
29//! * The probe runs on the supervisor thread and may block as long as its own deadline; give it a
30//!   short one ([`ping_probe`] takes the bound as an argument).
31//! * The child is created, probed and stopped on the supervisor thread only; the launch closure is
32//!   the one thing that crosses to it.
33
34use crate::client::{Client, ClientConfig};
35use crate::host_identity::HostExpectation;
36use crate::private::PrivateChild;
37use rightkit_child_process::{
38    AdoptedIdentity, Generation, HealthOutcome, HealthPolicy, HealthProbe, HealthTracker,
39    RestartOutcome, RestartPermit, RestartPolicy, RestartTracker,
40};
41use serde_json::json;
42use std::io;
43use std::sync::mpsc::{self, RecvTimeoutError};
44use std::sync::{Arc, Mutex};
45use std::time::{Duration, Instant};
46
47/// Default [`SupervisorConfig::poll_interval`].
48pub const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(1);
49/// Default [`SupervisorConfig::stop_grace`].
50pub const DEFAULT_STOP_GRACE: Duration = Duration::from_secs(5);
51
52const MIN_POLL: Duration = Duration::from_millis(10);
53
54/// How a child is watched and restarted.
55#[derive(Debug, Clone, Copy)]
56pub struct SupervisorConfig {
57    restart: RestartPolicy,
58    health: HealthPolicy,
59    poll_interval: Duration,
60    stop_grace: Duration,
61}
62
63impl SupervisorConfig {
64    /// `restart` decides when and whether a stopped child starts again; `health` decides when
65    /// probe results make a child unhealthy or a start a failure.
66    pub fn new(restart: RestartPolicy, health: HealthPolicy) -> Self {
67        Self {
68            restart,
69            health,
70            poll_interval: DEFAULT_POLL_INTERVAL,
71            stop_grace: DEFAULT_STOP_GRACE,
72        }
73    }
74
75    /// Time between checks (exit poll plus one probe). Default [`DEFAULT_POLL_INTERVAL`]; raised
76    /// to at least 10 ms. A restart's backoff is honoured to its own precision, not this one's.
77    #[must_use]
78    pub fn poll_interval(mut self, interval: Duration) -> Self {
79        self.poll_interval = interval.max(MIN_POLL);
80        self
81    }
82
83    /// How long a child gets to exit by itself after its stdin was closed before its process tree
84    /// is killed. Default [`DEFAULT_STOP_GRACE`].
85    #[must_use]
86    pub fn stop_grace(mut self, grace: Duration) -> Self {
87        self.stop_grace = grace;
88        self
89    }
90}
91
92/// What the supervisor is doing now.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum SupervisorState {
95    /// A child is running and has not yet answered a probe.
96    Starting,
97    /// A child is running and has answered a probe.
98    Ready,
99    /// The child is gone and the next start is waiting out its backoff.
100    WaitingRestart,
101    /// The child is gone and the policy waits for [`SupervisorHandle::restart_now`].
102    AwaitingDemand,
103    /// The restart budget is spent; nothing runs until [`SupervisorHandle::restart_now`].
104    Exhausted,
105    /// The policy never restarts and the child is gone, or the supervisor has stopped.
106    Stopped,
107}
108
109/// A snapshot of the supervisor, from [`SupervisorHandle::status`].
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub struct SupervisorStatus {
112    pub state: SupervisorState,
113    /// Children started so far (the running one included).
114    pub launches: u64,
115    /// The running child's process id, when there is one.
116    pub pid: Option<u32>,
117}
118
119/// What happened, content-free. `launch` is the 1-based count of children started.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub enum SupervisorEvent {
122    Started {
123        launch: u64,
124        pid: u32,
125    },
126    /// The child answered its first probe.
127    Ready {
128        launch: u64,
129    },
130    /// The child exited by itself.
131    Exited {
132        launch: u64,
133        code: Option<i32>,
134        success: bool,
135    },
136    /// A child that had been healthy failed `consecutive_failures` probes in a row; it is stopped.
137    Unhealthy {
138        launch: u64,
139        consecutive_failures: u32,
140    },
141    /// The child never answered a probe within the policy's startup timeout; it is stopped.
142    StartupTimedOut {
143        launch: u64,
144    },
145    /// The launch closure failed (never started a child).
146    LaunchFailed {
147        error: String,
148    },
149    RestartScheduled {
150        delay: Duration,
151    },
152    AwaitingDemand,
153    /// The restart budget is spent.
154    Exhausted,
155    /// The supervisor will not start another child (a policy that never restarts, or `stop`).
156    Stopped,
157    /// A stop was not confirmed (the child is still there after its grace and the kill): no
158    /// replacement starts until it is gone (rightkit#936 b2).
159    StopUnconfirmed {
160        launch: u64,
161        pid: u32,
162    },
163    /// The child exited on purpose after forwarding; the instance with `pid` is supervised by
164    /// identity now ([`ExitAction::AdoptForeign`]).
165    Adopted {
166        launch: u64,
167        pid: u32,
168    },
169    /// The adopted foreign instance is gone.
170    ForeignExited {
171        launch: u64,
172        pid: u32,
173    },
174}
175
176/// Callback for [`SupervisorEvent`]s. Runs on the supervisor thread; keep it short.
177pub type SupervisorEventFn = Arc<dyn Fn(&SupervisorEvent) + Send + Sync>;
178
179/// The app's lock state, as input to a restart (the lock mirror itself stays the app's: a
180/// replacement core starts locked exactly when the app says the lock blocked its data).
181/// [`Unknown`](Self::Unknown) is the state of an unlock (or lock) request that got no answer
182/// because the child died first: it starts the replacement locked, so an unanswered unlock
183/// can never restart a child unlocked.
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum LockState {
186    Locked,
187    Unlocked,
188    Unknown,
189}
190
191impl LockState {
192    /// The `locked` start flag a launch gets for this state. Only a positively answered
193    /// [`Unlocked`](Self::Unlocked) starts a replacement unlocked.
194    pub fn starts_locked(self) -> bool {
195        !matches!(self, LockState::Unlocked)
196    }
197}
198
199/// Supplies the current [`LockState`] at each launch. Runs on the supervisor thread.
200pub type LockStateFn = Arc<dyn Fn() -> LockState + Send + Sync>;
201
202/// How a child that exited by itself ended (what [`SupervisorHooks::exit_action`] is asked about
203/// and what [`SupervisorHandle::last_exit`] reports).
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub struct ExitInfo {
206    /// The 1-based launch count of the child that exited.
207    pub launch: u64,
208    /// Its exit code, when it had one (a signal gives none on Unix).
209    pub code: Option<i32>,
210    pub success: bool,
211}
212
213/// What the supervisor does after a child exited by itself ([`SupervisorHooks::exit_action`]).
214#[derive(Debug)]
215#[non_exhaustive]
216pub enum ExitAction {
217    /// The restart policy decides (the default).
218    Default,
219    /// This exit is final: nothing restarts and the supervisor ends in
220    /// [`SupervisorState::Stopped`]. The supervising process reads the code from
221    /// [`SupervisorHandle::final_exit`] and exits with it, so launchd and the notches see the
222    /// child's own disposition (0, 70, 75, ...) (rightkit#936 b20).
223    Final,
224    /// The child exited on purpose after forwarding to an instance that already runs (a second
225    /// launch of a single-instance app). Supervise that instance by identity instead: liveness
226    /// only (no probe, it is not this supervisor's child), never signalled. When it is gone the
227    /// restart policy decides (rightkit#936 b2).
228    AdoptForeign(AdoptedIdentity),
229}
230
231/// Asked before each launch ([`SupervisorHooks::start_gate`]).
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub struct StartGateInfo {
234    /// The 1-based launch count the next child would have.
235    pub next_launch: u64,
236}
237
238/// Whether the next launch may happen now.
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum StartGate {
241    /// Launch now.
242    Proceed,
243    /// Not yet (another supervisor's child is still finishing a handover): ask again after this
244    /// long. No restart budget is charged for waiting.
245    Wait(Duration),
246    /// Never launch: the supervisor ends in [`SupervisorState::Stopped`].
247    Stop,
248}
249
250type ExitActionHook = Box<dyn FnMut(&ExitInfo) -> ExitAction + Send>;
251type StartGateHook = Box<dyn FnMut(&StartGateInfo) -> StartGate + Send>;
252
253/// Optional decisions the supervisor asks its host (rightkit#936). Every hook runs on the
254/// supervisor thread; keep each short.
255#[derive(Default)]
256pub struct SupervisorHooks {
257    /// Called when a child exits by itself.
258    pub exit_action: Option<ExitActionHook>,
259    /// Called before every launch, the first included (a start precondition: wait for a handover).
260    pub start_gate: Option<StartGateHook>,
261}
262
263enum Cmd {
264    Stop,
265    RestartNow(Option<LockState>),
266    ReturnChild(mpsc::Sender<Option<PrivateChild>>),
267}
268
269/// Control of a running supervisor. Dropping it stops the supervisor and its child.
270pub struct SupervisorHandle {
271    tx: mpsc::Sender<Cmd>,
272    status: Arc<Mutex<SupervisorStatus>>,
273    exits: Arc<Mutex<ExitRecord>>,
274    thread: Option<std::thread::JoinHandle<()>>,
275}
276
277#[derive(Default)]
278struct ExitRecord {
279    last: Option<ExitInfo>,
280    final_exit: Option<ExitInfo>,
281}
282
283impl SupervisorHandle {
284    /// The most recent self-exit of a child, if any child has exited by itself.
285    pub fn last_exit(&self) -> Option<ExitInfo> {
286        self.exits.lock().unwrap_or_else(|e| e.into_inner()).last
287    }
288
289    /// The exit a [`SupervisorHooks::exit_action`] declared [`ExitAction::Final`], if one did. The
290    /// supervising process exits with its `code`.
291    pub fn final_exit(&self) -> Option<ExitInfo> {
292        self.exits
293            .lock()
294            .unwrap_or_else(|e| e.into_inner())
295            .final_exit
296    }
297
298    /// The state now.
299    pub fn status(&self) -> SupervisorStatus {
300        *self.status.lock().unwrap_or_else(|e| e.into_inner())
301    }
302
303    /// Ask for a start: a running child is stopped and counted as an exit (then the policy
304    /// decides, so a policy that waits for demand starts at once); a child waiting for demand
305    /// starts; an exhausted or stopped policy forgets its budget and starts. A pending backoff is
306    /// left to run. Returns before the work is done.
307    pub fn restart_now(&self) {
308        let _ = self.tx.send(Cmd::RestartNow(None));
309    }
310
311    /// [`restart_now`](Self::restart_now) with the lock state the next launch starts from,
312    /// overriding the [`LockStateFn`] for that one launch. [`LockState::Unknown`] (an unlock
313    /// that was never answered) starts the replacement locked.
314    pub fn restart_now_with_lock(&self, state: LockState) {
315        let _ = self.tx.send(Cmd::RestartNow(Some(state)));
316    }
317
318    /// Stop the child (stdin closed, then killed after the grace) and end the supervisor. Waits
319    /// for the thread to finish.
320    pub fn stop(mut self) {
321        self.stop_inner();
322    }
323
324    /// End the supervisor without stopping the child, handing the running child back once
325    /// (`None` when no child is running) so the caller can shut it down in its own order.
326    /// No restart follows: the supervisor thread has ended when this returns. Consumes the
327    /// handle, so the child can be taken only once.
328    pub fn stop_returning_child(mut self) -> Option<PrivateChild> {
329        let (tx, rx) = mpsc::channel();
330        if self.tx.send(Cmd::ReturnChild(tx)).is_err() {
331            return None;
332        }
333        if let Some(thread) = self.thread.take() {
334            let _ = thread.join();
335        }
336        rx.recv().ok().flatten()
337    }
338
339    fn stop_inner(&mut self) {
340        let _ = self.tx.send(Cmd::Stop);
341        if let Some(thread) = self.thread.take() {
342            let _ = thread.join();
343        }
344    }
345}
346
347impl Drop for SupervisorHandle {
348    fn drop(&mut self) {
349        self.stop_inner();
350    }
351}
352
353/// Start supervising. `launch` makes a child (normally [`PrivateChild::spawn`] of an
354/// `OwnedCommand`, with the per-run suite made fresh each time if the child needs one); `probe`
355/// says whether the running child answers (see [`ping_probe`]); `on_event` hears what happens.
356/// The first child is started at once, on the supervisor thread.
357pub fn supervise<L, P>(
358    config: SupervisorConfig,
359    mut launch: L,
360    probe: P,
361    on_event: Option<SupervisorEventFn>,
362) -> io::Result<SupervisorHandle>
363where
364    L: FnMut() -> io::Result<PrivateChild> + Send + 'static,
365    P: FnMut(&PrivateChild) -> bool + Send + 'static,
366{
367    spawn_core(
368        config,
369        move |_locked| launch(),
370        probe,
371        None,
372        None,
373        SupervisorHooks::default(),
374        on_event,
375    )
376}
377
378/// [`supervise_with_lock_state`] with [`SupervisorHooks`] (rightkit#936): a start gate before each
379/// launch, an exit disposition, a hold-off while a stop is unconfirmed (always on) and supervision
380/// of a foreign instance by identity.
381pub fn supervise_with_hooks<L, P>(
382    config: SupervisorConfig,
383    launch: L,
384    probe: P,
385    lock_state: Option<LockStateFn>,
386    hooks: SupervisorHooks,
387    on_event: Option<SupervisorEventFn>,
388) -> io::Result<SupervisorHandle>
389where
390    L: FnMut(bool) -> io::Result<PrivateChild> + Send + 'static,
391    P: FnMut(&PrivateChild) -> bool + Send + 'static,
392{
393    spawn_core(config, launch, probe, None, lock_state, hooks, on_event)
394}
395
396/// [`supervise`] for a child whose replacement must start from the app's lock state: every
397/// launch receives `locked`, resolved at launch time from a
398/// [`restart_now_with_lock`](SupervisorHandle::restart_now_with_lock) override first, then
399/// `lock_state`, then `false`. [`LockState::Unknown`] resolves to locked, so an unlock the
400/// dying child never answered cannot restart it unlocked. The lock mirror that produces
401/// the state stays the app's input.
402pub fn supervise_with_lock_state<L, P>(
403    config: SupervisorConfig,
404    launch: L,
405    probe: P,
406    lock_state: Option<LockStateFn>,
407    on_event: Option<SupervisorEventFn>,
408) -> io::Result<SupervisorHandle>
409where
410    L: FnMut(bool) -> io::Result<PrivateChild> + Send + 'static,
411    P: FnMut(&PrivateChild) -> bool + Send + 'static,
412{
413    spawn_core(
414        config,
415        launch,
416        probe,
417        None,
418        lock_state,
419        SupervisorHooks::default(),
420        on_event,
421    )
422}
423
424/// Supervise a child the caller already spawned (and usually already verified: it counts
425/// as launch 1, already ready, so its first crash restarts from the base backoff). The
426/// child is transferred untouched — its stdin stays open and it is not probed before the
427/// first poll. Replacements come from `launch`, with the lock flag resolved as in
428/// [`supervise_with_lock_state`].
429pub fn supervise_adopted<L, P>(
430    config: SupervisorConfig,
431    child: PrivateChild,
432    launch: L,
433    probe: P,
434    lock_state: Option<LockStateFn>,
435    on_event: Option<SupervisorEventFn>,
436) -> io::Result<SupervisorHandle>
437where
438    L: FnMut(bool) -> io::Result<PrivateChild> + Send + 'static,
439    P: FnMut(&PrivateChild) -> bool + Send + 'static,
440{
441    spawn_core(
442        config,
443        launch,
444        probe,
445        Some(child),
446        lock_state,
447        SupervisorHooks::default(),
448        on_event,
449    )
450}
451
452fn spawn_core<L, P>(
453    config: SupervisorConfig,
454    launch: L,
455    probe: P,
456    adopted: Option<PrivateChild>,
457    lock_state: Option<LockStateFn>,
458    hooks: SupervisorHooks,
459    on_event: Option<SupervisorEventFn>,
460) -> io::Result<SupervisorHandle>
461where
462    L: FnMut(bool) -> io::Result<PrivateChild> + Send + 'static,
463    P: FnMut(&PrivateChild) -> bool + Send + 'static,
464{
465    let (tx, rx) = mpsc::channel();
466    let exits = Arc::new(Mutex::new(ExitRecord::default()));
467    let status = Arc::new(Mutex::new(SupervisorStatus {
468        state: SupervisorState::Starting,
469        launches: 0,
470        pid: None,
471    }));
472    let core = Core {
473        cfg: config,
474        launch,
475        probe,
476        tracker: RestartTracker::new(config.restart),
477        origin: Instant::now(),
478        launches: 0,
479        status: status.clone(),
480        on_event,
481        phase: Phase::Idle(SupervisorState::Stopped),
482        adopted,
483        lock_state,
484        pending_locked: None,
485        hooks,
486        exits: exits.clone(),
487    };
488    let thread = std::thread::Builder::new()
489        .name("rk-supervisor".into())
490        .spawn(move || core.run(rx))?;
491    Ok(SupervisorHandle {
492        tx,
493        status,
494        exits,
495        thread: Some(thread),
496    })
497}
498
499/// A probe for [`supervise`]: connect to the child's service with the child's own secret, verify
500/// the host when `expect` is given, and call `ping`. True only for a `{"pong": true}` answer.
501/// `timeout` bounds one probe (connect, hello and the call). Use [`HostExpectation`] whenever the
502/// secret is sent: a secret goes to whatever answered at the endpoint.
503pub fn ping_probe(
504    config: ClientConfig,
505    expect: Option<HostExpectation>,
506    timeout: Duration,
507) -> impl FnMut(&PrivateChild) -> bool + Send + 'static {
508    move |child| {
509        let config = config
510            .clone()
511            .secret(child.secret().clone())
512            .deadline(Some(timeout));
513        let client = match &expect {
514            Some(expect) => Client::connect_expecting(&config, expect).ok(),
515            None => Client::connect_config(&config).ok(),
516        };
517        match client {
518            Some(client) => matches!(
519                client.call("ping", json!({})),
520                Ok(value) if value["pong"] == true
521            ),
522            None => false,
523        }
524    }
525}
526
527struct Running {
528    child: PrivateChild,
529    generation: Generation,
530    health: HealthTracker,
531    ready: bool,
532    launch: u64,
533}
534
535enum Phase {
536    Running(Box<Running>),
537    /// The next start is waiting for `due`.
538    Waiting(RestartPermit, Instant),
539    /// Nothing runs: `AwaitingDemand`, `Exhausted` or `Stopped`.
540    Idle(SupervisorState),
541    /// The start gate said wait; ask again at the instant.
542    Gated(Instant),
543    /// A stop was not confirmed: the child is held (and stopped again each tick) and nothing
544    /// replaces it until it is gone. The flag is an explicit restart demand to honour then.
545    Unconfirmed(Box<Running>, bool),
546    /// An instance this supervisor did not spawn, watched by identity.
547    Foreign(Box<ForeignRun>),
548}
549
550struct ForeignRun {
551    identity: AdoptedIdentity,
552    generation: Generation,
553    launch: u64,
554}
555
556struct Core<L, P> {
557    cfg: SupervisorConfig,
558    launch: L,
559    probe: P,
560    tracker: RestartTracker,
561    origin: Instant,
562    launches: u64,
563    status: Arc<Mutex<SupervisorStatus>>,
564    on_event: Option<SupervisorEventFn>,
565    phase: Phase,
566    adopted: Option<PrivateChild>,
567    lock_state: Option<LockStateFn>,
568    /// One-launch lock override from `restart_now_with_lock`.
569    pending_locked: Option<bool>,
570    hooks: SupervisorHooks,
571    exits: Arc<Mutex<ExitRecord>>,
572}
573
574impl<L, P> Core<L, P>
575where
576    L: FnMut(bool) -> io::Result<PrivateChild>,
577    P: FnMut(&PrivateChild) -> bool,
578{
579    fn at(&self) -> Duration {
580        self.origin.elapsed()
581    }
582
583    fn emit(&self, event: SupervisorEvent) {
584        if let Some(f) = &self.on_event {
585            f(&event);
586        }
587    }
588
589    fn set_status(&self, state: SupervisorState, pid: Option<u32>) {
590        let mut s = self.status.lock().unwrap_or_else(|e| e.into_inner());
591        s.state = state;
592        s.launches = self.launches;
593        s.pid = pid;
594    }
595
596    fn run(mut self, rx: mpsc::Receiver<Cmd>) {
597        match self.adopted.take() {
598            Some(child) => self.adopt(child),
599            None => self.start(),
600        }
601        loop {
602            let wait = match &self.phase {
603                Phase::Waiting(_, due) | Phase::Gated(due) => due
604                    .saturating_duration_since(Instant::now())
605                    .min(self.cfg.poll_interval),
606                _ => self.cfg.poll_interval,
607            };
608            match rx.recv_timeout(wait) {
609                Ok(Cmd::RestartNow(state)) => self.restart_now(state),
610                Ok(Cmd::ReturnChild(reply)) => {
611                    self.return_child(reply);
612                    return;
613                }
614                Ok(Cmd::Stop) | Err(RecvTimeoutError::Disconnected) => {
615                    self.shutdown();
616                    return;
617                }
618                Err(RecvTimeoutError::Timeout) => self.tick(),
619            }
620        }
621    }
622
623    /// Take over an already-spawned child as launch 1, already ready (the caller spawned
624    /// and verified it). The child itself is not touched: no stdin close, no probe yet.
625    fn adopt(&mut self, child: PrivateChild) {
626        let at = self.at();
627        let generation = self.tracker.started(at);
628        self.tracker.ready(generation);
629        self.launches += 1;
630        let pid = child.child().id();
631        let mut health = HealthTracker::new(self.cfg.health, at);
632        let _ = health.observe(at, HealthProbe::Success);
633        self.set_status(SupervisorState::Ready, Some(pid));
634        self.emit(SupervisorEvent::Started {
635            launch: self.launches,
636            pid,
637        });
638        self.emit(SupervisorEvent::Ready {
639            launch: self.launches,
640        });
641        self.phase = Phase::Running(Box::new(Running {
642            child,
643            generation,
644            health,
645            ready: true,
646            launch: self.launches,
647        }));
648    }
649
650    /// The `locked` flag for the next launch: explicit restart override, then the app's
651    /// lock-state input, then unlocked. `Unknown` resolves locked (see [`LockState`]).
652    fn next_locked(&mut self) -> bool {
653        if let Some(locked) = self.pending_locked.take() {
654            return locked;
655        }
656        match &self.lock_state {
657            Some(f) => f().starts_locked(),
658            None => false,
659        }
660    }
661
662    /// Launch a child now. A launch failure is charged to the budget like a crash.
663    fn start(&mut self) {
664        if let Some(gate) = self.hooks.start_gate.as_mut() {
665            match gate(&StartGateInfo {
666                next_launch: self.launches + 1,
667            }) {
668                StartGate::Proceed => {}
669                StartGate::Wait(wait) => {
670                    self.set_status(SupervisorState::WaitingRestart, None);
671                    self.phase = Phase::Gated(Instant::now() + wait.max(MIN_POLL));
672                    return;
673                }
674                StartGate::Stop => {
675                    self.tracker.shutdown();
676                    self.emit(SupervisorEvent::Stopped);
677                    self.set_status(SupervisorState::Stopped, None);
678                    self.phase = Phase::Idle(SupervisorState::Stopped);
679                    return;
680                }
681            }
682        }
683        let locked = self.next_locked();
684        match (self.launch)(locked) {
685            Ok(child) => {
686                let at = self.at();
687                let generation = self.tracker.started(at);
688                self.launches += 1;
689                let pid = child.child().id();
690                self.set_status(SupervisorState::Starting, Some(pid));
691                self.emit(SupervisorEvent::Started {
692                    launch: self.launches,
693                    pid,
694                });
695                self.phase = Phase::Running(Box::new(Running {
696                    child,
697                    generation,
698                    health: HealthTracker::new(self.cfg.health, at),
699                    ready: false,
700                    launch: self.launches,
701                }));
702            }
703            Err(error) => {
704                self.emit(SupervisorEvent::LaunchFailed {
705                    error: error.to_string(),
706                });
707                let outcome = self.tracker.request_restart(self.at());
708                self.apply(outcome);
709            }
710        }
711    }
712
713    fn apply(&mut self, outcome: RestartOutcome) {
714        match outcome {
715            RestartOutcome::Restart(permit) => {
716                let delay = permit.delay();
717                self.emit(SupervisorEvent::RestartScheduled { delay });
718                self.set_status(SupervisorState::WaitingRestart, None);
719                self.phase = Phase::Waiting(permit, Instant::now() + delay);
720            }
721            RestartOutcome::AwaitingDemand => {
722                self.emit(SupervisorEvent::AwaitingDemand);
723                self.set_status(SupervisorState::AwaitingDemand, None);
724                self.phase = Phase::Idle(SupervisorState::AwaitingDemand);
725            }
726            RestartOutcome::Exhausted => {
727                self.emit(SupervisorEvent::Exhausted);
728                self.set_status(SupervisorState::Exhausted, None);
729                self.phase = Phase::Idle(SupervisorState::Exhausted);
730            }
731            // `IgnoredStale` cannot happen for a generation this loop owns; if it ever did,
732            // stopping loudly beats a silent loop.
733            RestartOutcome::Stopped | RestartOutcome::IgnoredStale => {
734                self.emit(SupervisorEvent::Stopped);
735                self.set_status(SupervisorState::Stopped, None);
736                self.phase = Phase::Idle(SupervisorState::Stopped);
737            }
738        }
739    }
740
741    fn tick(&mut self) {
742        let placeholder = Phase::Idle(SupervisorState::Stopped);
743        match std::mem::replace(&mut self.phase, placeholder) {
744            Phase::Running(running) => self.tick_running(*running),
745            Phase::Waiting(permit, due) => {
746                if Instant::now() >= due && self.tracker.permit_valid(&permit, self.at()) {
747                    self.start();
748                } else {
749                    self.phase = Phase::Waiting(permit, due);
750                }
751            }
752            Phase::Gated(due) => {
753                if Instant::now() >= due {
754                    self.start();
755                } else {
756                    self.phase = Phase::Gated(due);
757                }
758            }
759            Phase::Unconfirmed(running, demand) => self.tick_unconfirmed(*running, demand),
760            Phase::Foreign(foreign) => self.tick_foreign(*foreign),
761            idle @ Phase::Idle(_) => self.phase = idle,
762        }
763    }
764
765    /// The held child again: stop it once more; once it is gone the replacement may follow.
766    fn tick_unconfirmed(&mut self, mut running: Running, demand: bool) {
767        let gone = matches!(running.child.child_mut().try_wait(), Ok(Some(_)))
768            || self.stop_child(&mut running.child);
769        if gone {
770            self.settle(running, demand);
771        } else {
772            self.phase = Phase::Unconfirmed(Box::new(running), demand);
773        }
774    }
775
776    /// An adopted foreign instance: liveness by identity only. A read that fails is not taken
777    /// for an exit.
778    fn tick_foreign(&mut self, foreign: ForeignRun) {
779        if matches!(foreign.identity.is_running(), Ok(true) | Err(_)) {
780            self.phase = Phase::Foreign(Box::new(foreign));
781            return;
782        }
783        self.emit(SupervisorEvent::ForeignExited {
784            launch: foreign.launch,
785            pid: foreign.identity.pid(),
786        });
787        let outcome = self.tracker.exited(foreign.generation, self.at());
788        self.apply(outcome);
789    }
790
791    /// The child is confirmed gone: tell the restart tracker and act on its answer. `demand`
792    /// turns a wait-for-demand answer into a start (an explicit restart).
793    fn settle(&mut self, running: Running, demand: bool) {
794        drop(running.child);
795        let at = self.at();
796        let mut outcome = self.tracker.exited(running.generation, at);
797        if demand && outcome == RestartOutcome::AwaitingDemand {
798            outcome = self.tracker.request_restart(at);
799        }
800        self.apply(outcome);
801    }
802
803    /// Stop the child and settle, or hold it when the stop is unconfirmed.
804    fn stop_and_settle(&mut self, mut running: Running, demand: bool) {
805        if self.stop_child(&mut running.child) {
806            self.settle(running, demand);
807        } else {
808            let pid = running.child.child().id();
809            self.emit(SupervisorEvent::StopUnconfirmed {
810                launch: running.launch,
811                pid,
812            });
813            self.set_status(SupervisorState::WaitingRestart, Some(pid));
814            self.phase = Phase::Unconfirmed(Box::new(running), demand);
815        }
816    }
817
818    fn tick_running(&mut self, mut running: Running) {
819        match running.child.child_mut().try_wait() {
820            Ok(Some(status)) => {
821                self.emit(SupervisorEvent::Exited {
822                    launch: running.launch,
823                    code: status.code(),
824                    success: status.success(),
825                });
826                let info = ExitInfo {
827                    launch: running.launch,
828                    code: status.code(),
829                    success: status.success(),
830                };
831                self.exits.lock().unwrap_or_else(|e| e.into_inner()).last = Some(info);
832                let action = match self.hooks.exit_action.as_mut() {
833                    Some(decide) => decide(&info),
834                    None => ExitAction::Default,
835                };
836                // The child has been reaped by `try_wait`; dropping the wrapper closes its stdin.
837                drop(running.child);
838                match action {
839                    ExitAction::Final => {
840                        self.exits
841                            .lock()
842                            .unwrap_or_else(|e| e.into_inner())
843                            .final_exit = Some(info);
844                        self.tracker.shutdown();
845                        self.emit(SupervisorEvent::Stopped);
846                        self.set_status(SupervisorState::Stopped, None);
847                        self.phase = Phase::Idle(SupervisorState::Stopped);
848                    }
849                    ExitAction::AdoptForeign(identity) => {
850                        let pid = identity.pid();
851                        self.emit(SupervisorEvent::Adopted {
852                            launch: running.launch,
853                            pid,
854                        });
855                        self.set_status(SupervisorState::Ready, Some(pid));
856                        self.phase = Phase::Foreign(Box::new(ForeignRun {
857                            identity,
858                            generation: running.generation,
859                            launch: running.launch,
860                        }));
861                    }
862                    _ => {
863                        let outcome = self.tracker.exited(running.generation, self.at());
864                        self.apply(outcome);
865                    }
866                }
867                return;
868            }
869            Ok(None) => {}
870            Err(_) => {
871                // The state of the child cannot be read: do not guess it is fine.
872                self.stop_and_settle(running, false);
873                return;
874            }
875        }
876        let answered = (self.probe)(&running.child);
877        let at = self.at();
878        let outcome = running.health.observe(
879            at,
880            if answered {
881                HealthProbe::Success
882            } else {
883                HealthProbe::Failure
884            },
885        );
886        if answered && !running.ready {
887            running.ready = true;
888            self.tracker.ready(running.generation);
889            self.set_status(SupervisorState::Ready, Some(running.child.child().id()));
890            self.emit(SupervisorEvent::Ready {
891                launch: running.launch,
892            });
893        }
894        let failure = match outcome {
895            HealthOutcome::StartupTimedOut => Some(SupervisorEvent::StartupTimedOut {
896                launch: running.launch,
897            }),
898            // Failures before the first success are a child still starting, not an unhealthy one.
899            HealthOutcome::Unhealthy {
900                consecutive_failures,
901            } if running.ready => Some(SupervisorEvent::Unhealthy {
902                launch: running.launch,
903                consecutive_failures,
904            }),
905            _ => None,
906        };
907        match failure {
908            None => self.phase = Phase::Running(Box::new(running)),
909            Some(event) => {
910                self.emit(event);
911                self.stop_and_settle(running, false);
912            }
913        }
914    }
915
916    /// Stdin closed (the polite cue), then the whole tree is killed after the grace. True when the
917    /// child is confirmed gone; false when the stop could not be confirmed.
918    fn stop_child(&self, child: &mut PrivateChild) -> bool {
919        child.close_stdin();
920        match child.child_mut().wait_then_kill(self.cfg.stop_grace) {
921            Ok(_) => true,
922            Err(_) => matches!(child.child_mut().try_wait(), Ok(Some(_))),
923        }
924    }
925
926    fn restart_now(&mut self, state: Option<LockState>) {
927        if let Some(state) = state {
928            self.pending_locked = Some(state.starts_locked());
929        }
930        let placeholder = Phase::Idle(SupervisorState::Stopped);
931        match std::mem::replace(&mut self.phase, placeholder) {
932            // This is the demand: a wait-for-demand answer becomes a start.
933            Phase::Running(running) => self.stop_and_settle(*running, true),
934            Phase::Unconfirmed(running, _) => self.phase = Phase::Unconfirmed(running, true),
935            // A start is already on its way, or an instance this supervisor never signals runs.
936            held @ (Phase::Waiting(..) | Phase::Gated(_) | Phase::Foreign(_)) => self.phase = held,
937            Phase::Idle(SupervisorState::AwaitingDemand) => {
938                match self.tracker.request_restart(self.at()) {
939                    // Nothing ever ran, so the tracker was not waiting: start directly.
940                    RestartOutcome::AwaitingDemand => self.start(),
941                    other => self.apply(other),
942                }
943            }
944            Phase::Idle(_) => {
945                self.tracker.reset_budget();
946                self.start();
947            }
948        }
949    }
950
951    /// Hand the running child back untouched and end supervision: no stdin close, no
952    /// kill, no restart. The caller owns the child's shutdown from here.
953    fn return_child(&mut self, reply: mpsc::Sender<Option<PrivateChild>>) {
954        self.tracker.shutdown();
955        let child = match std::mem::replace(&mut self.phase, Phase::Idle(SupervisorState::Stopped))
956        {
957            Phase::Running(running) => Some(running.child),
958            _ => None,
959        };
960        self.set_status(SupervisorState::Stopped, None);
961        self.emit(SupervisorEvent::Stopped);
962        let _ = reply.send(child);
963    }
964
965    fn shutdown(&mut self) {
966        self.tracker.shutdown();
967        match std::mem::replace(&mut self.phase, Phase::Idle(SupervisorState::Stopped)) {
968            Phase::Running(mut running) | Phase::Unconfirmed(mut running, _) => {
969                self.stop_child(&mut running.child);
970            }
971            _ => {}
972        }
973        self.set_status(SupervisorState::Stopped, None);
974        self.emit(SupervisorEvent::Stopped);
975    }
976}