Skip to main content

rightkit_daemon/
stop.rs

1//! Stop fan-in for a daemon host (issue #375): one [`StopSource`] that several inputs may
2//! trigger (an OS stop signal, the parent closing this process's stdin, or the program itself),
3//! and [`StopToken`]s that the host's threads wait on. The first cause wins and later triggers
4//! are ignored.
5//!
6//! Signals: [`stop_on_signals`] installs handlers for SIGINT and SIGTERM on Unix and the console
7//! control handler on Windows (Ctrl+C, Ctrl+Break, close). The handler only records the event in a
8//! static atomic (the one thing a signal handler may do here); a watcher thread turns that into a
9//! trigger, polling every 50 ms, and exits once the source is stopped. Other platforms return
10//! `Unsupported`.
11//!
12//! Stdin: [`stop_on_stdin_close`] triggers the source when stdin reaches end of file (see
13//! [`crate::private::watch_stdin_eof`]).
14
15use std::io;
16use std::sync::{Arc, Condvar, Mutex};
17use std::thread::JoinHandle;
18use std::time::{Duration, Instant};
19
20/// Why the stop was triggered. The first trigger's cause is the one reported.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum StopCause {
23    /// SIGINT, Ctrl+C or Ctrl+Break.
24    Interrupt,
25    /// SIGTERM or the console close event.
26    Terminate,
27    /// This process's stdin reached end of file: the parent went away.
28    StdinClosed,
29    /// The program asked for the stop.
30    Requested,
31}
32
33struct Inner {
34    state: Mutex<Option<StopCause>>,
35    changed: Condvar,
36}
37
38/// The trigger side. Clone it to give inputs their own handle; every clone triggers the same stop.
39#[derive(Clone)]
40pub struct StopSource {
41    inner: Arc<Inner>,
42}
43
44/// The waiting side. Cheap to clone; it sees the stop that its source triggers.
45#[derive(Clone)]
46pub struct StopToken {
47    inner: Arc<Inner>,
48}
49
50impl Default for StopSource {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl StopSource {
57    pub fn new() -> Self {
58        Self {
59            inner: Arc::new(Inner {
60                state: Mutex::new(None),
61                changed: Condvar::new(),
62            }),
63        }
64    }
65
66    /// A token that observes this source.
67    pub fn token(&self) -> StopToken {
68        StopToken {
69            inner: self.inner.clone(),
70        }
71    }
72
73    /// Trigger the stop. Returns `true` for the call that set it; later calls change nothing.
74    pub fn trigger(&self, cause: StopCause) -> bool {
75        let mut state = self.inner.state.lock().unwrap_or_else(|e| e.into_inner());
76        if state.is_some() {
77            return false;
78        }
79        *state = Some(cause);
80        self.inner.changed.notify_all();
81        true
82    }
83}
84
85impl StopToken {
86    /// The cause, once the stop has been triggered.
87    pub fn cause(&self) -> Option<StopCause> {
88        *self.inner.state.lock().unwrap_or_else(|e| e.into_inner())
89    }
90
91    pub fn is_stopped(&self) -> bool {
92        self.cause().is_some()
93    }
94
95    /// Wait up to `timeout` for the stop. Returns its cause as soon as it is triggered, or `None`
96    /// when the timeout passes first. Returns at once when the stop is already set.
97    pub fn wait(&self, timeout: Duration) -> Option<StopCause> {
98        let deadline = Instant::now() + timeout;
99        let mut state = self.inner.state.lock().unwrap_or_else(|e| e.into_inner());
100        loop {
101            if let Some(cause) = *state {
102                return Some(cause);
103            }
104            let now = Instant::now();
105            if now >= deadline {
106                return None;
107            }
108            let (next, _) = self
109                .inner
110                .changed
111                .wait_timeout(state, deadline - now)
112                .unwrap_or_else(|e| e.into_inner());
113            state = next;
114        }
115    }
116}
117
118/// Static slot the signal handler writes; 0 means none pending.
119#[cfg(unix)]
120static PENDING_SIGNAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
121
122#[cfg(unix)]
123const SIGINT: i32 = 2;
124#[cfg(unix)]
125const SIGTERM: i32 = 15;
126
127#[cfg(unix)]
128extern "C" fn on_unix_signal(signal: i32) {
129    // Only an atomic store here: the one operation a signal handler may safely do.
130    PENDING_SIGNAL.store(signal, std::sync::atomic::Ordering::SeqCst);
131}
132
133#[cfg(unix)]
134extern "C" {
135    fn signal(signum: i32, handler: usize) -> usize;
136}
137
138#[cfg(windows)]
139static PENDING_CONSOLE_EVENT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
140
141#[cfg(windows)]
142unsafe extern "system" fn on_console_event(event: u32) -> i32 {
143    PENDING_CONSOLE_EVENT.store(event + 1, std::sync::atomic::Ordering::SeqCst);
144    1 // handled: the default handler (which ends the process) is not run
145}
146
147#[cfg(windows)]
148extern "system" {
149    fn SetConsoleCtrlHandler(
150        handler: Option<unsafe extern "system" fn(u32) -> i32>,
151        add: i32,
152    ) -> i32;
153}
154
155/// Install OS stop-signal handlers that trigger `source` (see the module docs). The watcher thread
156/// ends when the source is stopped by any input.
157pub fn stop_on_signals(source: &StopSource) -> io::Result<JoinHandle<()>> {
158    install_os_handlers()?;
159    let source = source.clone();
160    std::thread::Builder::new()
161        .name("rk-stop-signals".into())
162        .spawn(move || loop {
163            if source.token().is_stopped() {
164                break;
165            }
166            if let Some(cause) = take_pending_signal() {
167                source.trigger(cause);
168                break;
169            }
170            std::thread::sleep(Duration::from_millis(50));
171        })
172}
173
174/// Trigger `source` when this process's stdin reaches end of file.
175pub fn stop_on_stdin_close(source: &StopSource) -> JoinHandle<()> {
176    let source = source.clone();
177    crate::private::watch_stdin_eof(move || {
178        source.trigger(StopCause::StdinClosed);
179    })
180}
181
182#[cfg(unix)]
183fn install_os_handlers() -> io::Result<()> {
184    for sig in [SIGINT, SIGTERM] {
185        let handler = on_unix_signal as extern "C" fn(i32) as usize;
186        // SAFETY: `signal` is the C library function; the handler is an `extern "C"` function
187        // that only stores to an atomic. SIG_ERR is `usize::MAX` on both Linux and macOS.
188        let previous = unsafe { signal(sig, handler) };
189        if previous == usize::MAX {
190            return Err(io::Error::last_os_error());
191        }
192    }
193    Ok(())
194}
195
196#[cfg(unix)]
197fn take_pending_signal() -> Option<StopCause> {
198    match PENDING_SIGNAL.swap(0, std::sync::atomic::Ordering::SeqCst) {
199        0 => None,
200        SIGINT => Some(StopCause::Interrupt),
201        _ => Some(StopCause::Terminate),
202    }
203}
204
205#[cfg(windows)]
206fn install_os_handlers() -> io::Result<()> {
207    // SAFETY: the handler is an `unsafe extern "system"` function that only stores to an atomic.
208    let ok = unsafe { SetConsoleCtrlHandler(Some(on_console_event), 1) };
209    if ok == 0 {
210        return Err(io::Error::last_os_error());
211    }
212    Ok(())
213}
214
215#[cfg(windows)]
216fn take_pending_signal() -> Option<StopCause> {
217    // Console events: 0 Ctrl+C and 1 Ctrl+Break are interrupts; 2 (close) and the logoff and
218    // shutdown events are terminations. The stored value is event + 1, so 0 is "none".
219    match PENDING_CONSOLE_EVENT.swap(0, std::sync::atomic::Ordering::SeqCst) {
220        0 => None,
221        1 | 2 => Some(StopCause::Interrupt),
222        _ => Some(StopCause::Terminate),
223    }
224}
225
226#[cfg(not(any(unix, windows)))]
227fn install_os_handlers() -> io::Result<()> {
228    Err(io::Error::new(
229        io::ErrorKind::Unsupported,
230        "stop signals are not implemented on this platform",
231    ))
232}
233
234#[cfg(not(any(unix, windows)))]
235fn take_pending_signal() -> Option<StopCause> {
236    None
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn the_first_trigger_wins() {
245        let source = StopSource::new();
246        assert!(source.trigger(StopCause::Terminate));
247        assert!(!source.trigger(StopCause::StdinClosed));
248        assert_eq!(source.token().cause(), Some(StopCause::Terminate));
249    }
250
251    #[test]
252    fn a_wait_times_out_until_triggered() {
253        let source = StopSource::new();
254        let token = source.token();
255        assert_eq!(token.wait(Duration::from_millis(5)), None);
256        assert!(!token.is_stopped());
257        let clone = source.clone();
258        let t = std::thread::spawn(move || {
259            std::thread::sleep(Duration::from_millis(10));
260            clone.trigger(StopCause::Requested);
261        });
262        assert_eq!(
263            token.wait(Duration::from_secs(5)),
264            Some(StopCause::Requested)
265        );
266        t.join().unwrap();
267    }
268}