Skip to main content

nova_runtime/
signal.rs

1//! The process-wide shutdown flag behind `std/http`'s graceful
2//! `Server::listen`.
3//!
4//! One `AtomicBool`, set by the first termination request the process
5//! receives: SIGTERM or SIGINT on Unix, CTRL_BREAK or CTRL_C on Windows. A
6//! request that arrives once the flag is already set takes the signal's
7//! default action instead, so a stalled shutdown can always be ended.
8//!
9//! **Opt-in, and permanent once in.** Nothing is installed until the first
10//! call to [`nova_rt_shutdown_requested`], which only `std/http`'s `Server`
11//! makes: the builtin `shutdown_requested` is `STD_ONLY`. A program that never
12//! calls it keeps every signal's default action. Once installed, the handler
13//! stays for the life of the process, and the flag is never cleared.
14//!
15//! **The handler touches nothing but [`SHUTDOWN`].** On Unix it runs in signal
16//! context, where only async-signal-safe calls are allowed. On Windows the OS
17//! runs it on a thread of its own, where this crate's `thread_local!` state
18//! (the executor, the socket table, the heap) is a different, empty copy. So
19//! it never wakes the executor: `poll.rs` retries an interrupted wait, and Nova
20//! code sees the flag at its next deadline. `Server::listen` provides one
21//! every 100 ms. `docs/adr/0022-process-shutdown-signals.md` records the
22//! decision and the alternatives it rejected.
23//!
24//! **An inherited ignore stays ignored.** A process started with SIGINT
25//! ignored (a job backgrounded by a non-interactive shell) keeps ignoring it.
26//! Windows' inherited Ctrl-C ignore flag keeps CTRL_C from reaching any
27//! handler at all. SIGTERM and CTRL_BREAK are not affected.
28//!
29//! `pub`, unlike [`crate::time`], so the integration tests under
30//! `crates/nova-runtime/tests/` can drive it. Each process-global scenario
31//! there is its own test binary, because neither the install nor the flag can
32//! be undone within one process.
33
34use std::sync::atomic::{AtomicBool, Ordering};
35use std::sync::Once;
36
37/// Whether the process has been asked to stop.
38static SHUTDOWN: AtomicBool = AtomicBool::new(false);
39
40/// What a handler does with one termination request.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42enum Action {
43    /// The first request: set [`SHUTDOWN`] and carry on.
44    SetFlag,
45    /// A later one: take the signal's default action.
46    Default,
47}
48
49/// The first request sets the flag; any later one takes the default action.
50fn action(already_set: bool) -> Action {
51    if already_set {
52        Action::Default
53    } else {
54        Action::SetFlag
55    }
56}
57
58/// Windows console control events, numbered as `windows-sys` numbers them
59/// (checked against it at compile time below). Plain constants, so the
60/// decision logic's unit tests run on every operating system.
61#[cfg(any(windows, test))]
62const CTRL_C: u32 = 0;
63#[cfg(any(windows, test))]
64const CTRL_BREAK: u32 = 1;
65
66#[cfg(windows)]
67const _: () = {
68    use windows_sys::Win32::System::Console::{CTRL_BREAK_EVENT, CTRL_C_EVENT};
69    assert!(CTRL_C == CTRL_C_EVENT && CTRL_BREAK == CTRL_BREAK_EVENT);
70};
71
72/// Whether a console control event asks for a graceful shutdown. Close,
73/// logoff and shutdown events keep the default action.
74#[cfg(any(windows, test))]
75fn is_termination_event(event: u32) -> bool {
76    event == CTRL_C || event == CTRL_BREAK
77}
78
79/// `shutdown_requested() -> Bool`: installs the handler on the first call,
80/// then reports whether the flag is set, as `1` or `0`.
81#[no_mangle]
82pub extern "C" fn nova_rt_shutdown_requested() -> i8 {
83    install();
84    i8::from(SHUTDOWN.load(Ordering::SeqCst))
85}
86
87/// Installs the handler, once per process.
88fn install() {
89    static INSTALL: Once = Once::new();
90    INSTALL.call_once(install_handler);
91}
92
93#[cfg(unix)]
94fn install_handler() {
95    // SAFETY: an all-zero `sigaction` is a valid value (no flags, empty mask,
96    // `SIG_DFL`). Every field set below is a plain integer, or the address of
97    // `unix_handler`, whose signature is the one `sa_sigaction` expects when
98    // `SA_SIGINFO` is not set. `sigaction` reads `sa` and writes `old` only
99    // for the duration of each call.
100    unsafe {
101        let mut sa: libc::sigaction = std::mem::zeroed();
102        sa.sa_sigaction = unix_handler as extern "C" fn(libc::c_int) as libc::sighandler_t;
103        sa.sa_flags = libc::SA_RESTART;
104        libc::sigemptyset(&mut sa.sa_mask);
105        libc::sigaction(libc::SIGTERM, &sa, std::ptr::null_mut());
106        let mut old: libc::sigaction = std::mem::zeroed();
107        libc::sigaction(libc::SIGINT, std::ptr::null(), &mut old);
108        if old.sa_sigaction != libc::SIG_IGN {
109            libc::sigaction(libc::SIGINT, &sa, std::ptr::null_mut());
110        }
111    }
112}
113
114/// The SIGTERM and SIGINT handler. Only async-signal-safe work: an atomic
115/// swap and, on a second signal, `signal` and `raise`, both on POSIX's list.
116#[cfg(unix)]
117extern "C" fn unix_handler(sig: libc::c_int) {
118    if action(SHUTDOWN.swap(true, Ordering::SeqCst)) == Action::Default {
119        // SAFETY: restores `sig`'s default disposition and raises it again.
120        // `sig` is blocked while this handler runs, so the raised signal is
121        // delivered as the handler returns, with the default action.
122        unsafe {
123            libc::signal(sig, libc::SIG_DFL);
124            libc::raise(sig);
125        }
126    }
127}
128
129#[cfg(windows)]
130fn install_handler() {
131    use windows_sys::Win32::System::Console::SetConsoleCtrlHandler;
132    // SAFETY: `console_handler` has the `PHANDLER_ROUTINE` signature, and as a
133    // plain function it outlives the registration.
134    let _ = unsafe {
135        SetConsoleCtrlHandler(
136            Some(console_handler as unsafe extern "system" fn(u32) -> windows_sys::core::BOOL),
137            1,
138        )
139    };
140}
141
142/// The console control routine [`install`] registers on Windows. It returns
143/// TRUE (handled) for the first CTRL_C or CTRL_BREAK and FALSE for anything
144/// else, so the next routine runs instead: the default one, which ends the
145/// process with `0xC000013A`.
146///
147/// The OS calls it on a thread of its own. It touches only [`SHUTDOWN`], an
148/// atomic, so it is safe on any thread. `pub` so `tests/signal_console.rs` can
149/// call it directly.
150#[cfg(windows)]
151pub extern "system" fn console_handler(event: u32) -> windows_sys::core::BOOL {
152    if !is_termination_event(event) {
153        return 0;
154    }
155    i32::from(action(SHUTDOWN.swap(true, Ordering::SeqCst)) == Action::SetFlag)
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn the_first_request_sets_the_flag_and_a_later_one_takes_the_default_action() {
164        assert_eq!(action(false), Action::SetFlag);
165        assert_eq!(action(true), Action::Default);
166    }
167
168    #[test]
169    fn only_ctrl_c_and_ctrl_break_ask_for_a_graceful_shutdown() {
170        // CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT and CTRL_SHUTDOWN_EVENT are 2, 5
171        // and 6 in `windows-sys`.
172        for (event, expected) in [
173            (CTRL_C, true),
174            (CTRL_BREAK, true),
175            (2, false),
176            (5, false),
177            (6, false),
178        ] {
179            assert_eq!(is_termination_event(event), expected, "event {event}");
180        }
181    }
182}