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}