Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_core/src/stop.rs

9.0 KiB, 1 run

created by r1870400018:21649, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Hearing the operating system ask a program to stop.
2//!
3//! A long-running program is not asked to stop in words. It is sent a signal:
4//! `SIGINT` when somebody presses Ctrl-C, `SIGTERM` when a service manager or a
5//! reboot says to go. A program that hears neither is killed where it stands,
6//! and anything holding a store open is killed in the middle of a write.
7//!
8//! [`on_stop_request`] is the whole of the module: give it a closure, and the
9//! closure is called every time this process is asked to stop.
10//!
11//! # Why this needs no `unsafe`
12//!
13//! Three of the four routes from a signal to a program need an `unsafe` block.
14//! The C library's `sigaction` and Windows's `SetConsoleCtrlHandler` are
15//! `extern "C"`; `signal_hook_registry::register` is an `unsafe fn`, for the
16//! good reason that whatever it registers runs in signal context, where almost
17//! nothing is allowed to happen.
18//!
19//! The fourth route is tokio's `signal` module, and its whole public surface is
20//! safe. The registration and the signal-context work sit inside tokio, which
21//! owns and audits them; what reaches the caller is an ordinary asynchronous
22//! stream read on an ordinary thread. So this crate keeps
23//! `#![forbid(unsafe_code)]`, and so does everything downstream of it.
24//!
25//! That matters for more than a lint. Because `on_ask` is called from a plain
26//! thread rather than from signal context, it is under none of the restrictions
27//! a signal handler is under: it may allocate, lock, log and take as long as it
28//! likes.
29//!
30//! # Not on `wasm32`
31//!
32//! There are no signals in a browser, and tokio's `signal` feature pulls in
33//! `mio`, `libc` and `signal-hook-registry`, none of which belong in a wasm
34//! build. The module is therefore compiled only for the other targets, and the
35//! dependency that carries it is declared for those targets alone.
36
37use crate::prelude::*;
38
39use std::{
40 sync::atomic::{
41 AtomicBool,
42 Ordering,
43 },
44 thread,
45};
46
47/// The name the listening thread carries, so that it can be told apart in a
48/// debugger or a stack dump.
49const THREAD_NAME: &str = "fe2o3-stop";
50
51/// Whether a listener has already been installed in this process.
52static LISTENING: AtomicBool = AtomicBool::new(false);
53
54/// Calls `on_ask` every time the operating system asks this process to stop.
55///
56/// What counts as an ask depends on the platform:
57///
58/// | Platform | Heard |
59/// |-----------|--------------------------------------------------------|
60/// | Unix | `SIGINT` and `SIGTERM` |
61/// | Windows | Ctrl-C, the console window closing, the machine going |
62/// | Other | Ctrl-C |
63///
64/// The call returns as soon as the listener is installed, and the listening is
65/// done on a thread of its own. `on_ask` is called **once per ask**, not once
66/// and then never again: a program that reads a second ask as a firmer one --
67/// the first polite, the second immediate -- gets to see both.
68///
69/// One listener to a process, because a signal arrives at a process rather than
70/// at an object. A second call is refused rather than quietly stacking a second
71/// thread behind the first; a caller with two things to do should do both in the
72/// one closure.
73///
74/// # Errors
75///
76/// Returns an error if a listener is already installed, or if the thread cannot
77/// be spawned. A failure *after* the listener is running -- a runtime that will
78/// not build, a signal that cannot be registered -- cannot be returned to
79/// anybody, and is logged at error level instead.
80///
81/// # Example
82///
83/// ```no_run
84/// use oxedyne_fe2o3_core::prelude::*;
85/// use std::sync::atomic::{AtomicUsize, Ordering};
86///
87/// static ASKS: AtomicUsize = AtomicUsize::new(0);
88///
89/// fn main() -> Outcome<()> {
90/// res!(oxedyne_fe2o3_core::stop::on_stop_request(|| {
91/// // The first ask is polite; the second means now.
92/// if ASKS.fetch_add(1, Ordering::Relaxed) > 0 {
93/// std::process::exit(130);
94/// }
95/// }));
96/// while ASKS.load(Ordering::Relaxed) == 0 {
97/// std::thread::sleep(std::time::Duration::from_millis(100));
98/// }
99/// Ok(())
100/// }
101/// ```
102pub fn on_stop_request<F>(on_ask: F) -> Outcome<()>
103where
104 F: Fn() + Send + 'static,
105{
106 if LISTENING.swap(true, Ordering::SeqCst) {
107 return Err(err!(
108 "A stop request listener is already installed in this process. A \
109 signal arrives at a process rather than at an object, so there is \
110 one listener and it should be given a closure that does everything \
111 which has to happen.";
112 Conflict, Exists));
113 }
114 let _listener = res!(thread::Builder::new()
115 .name(THREAD_NAME.to_string())
116 .spawn(move || match listen(&on_ask) {
117 Ok(()) => (),
118 // In statement position on purpose: `error!` expands to a call and
119 // a semicolon, which a future compiler will refuse to read as an
120 // expression.
121 Err(e) => {
122 error!(e,
123 "The stop request listener could not start, so a Ctrl-C or \
124 a service manager's stop will kill this process where it \
125 stands rather than ask it to come home.");
126 },
127 }), Thread, Init);
128 Ok(())
129}
130
131/// The listening thread's whole life: a runtime of its own, and then waiting.
132///
133/// A current-thread runtime, because this thread has exactly one thing to wait
134/// for and a worker pool for it would be an absurdity. It is built here rather
135/// than asked of the caller so that a program with no asynchronous code
136/// anywhere, which is most of them, can still be asked to stop.
137fn listen<F>(on_ask: &F) -> Outcome<()>
138where
139 F: Fn(),
140{
141 let rt = res!(tokio::runtime::Builder::new_current_thread()
142 .enable_all()
143 .build(), Init, System);
144 rt.block_on(wait(on_ask))
145}
146
147/// Waits on `SIGINT` and `SIGTERM`, answering each until the process ends.
148///
149/// `SIGINT` is Ctrl-C at a terminal and `SIGTERM` is what a service manager, and
150/// every reboot, sends first. Both are asks rather than orders: the kill that
151/// cannot be caught is `SIGKILL`, and by then it is too late to do anything at
152/// all.
153#[cfg(unix)]
154async fn wait<F>(on_ask: &F) -> Outcome<()>
155where
156 F: Fn(),
157{
158 use tokio::signal::unix::{
159 signal,
160 SignalKind,
161 };
162
163 let mut int = res!(signal(SignalKind::interrupt()), Init, System);
164 let mut term = res!(signal(SignalKind::terminate()), Init, System);
165 loop {
166 // Both arms are cancel-safe, which is what makes this loop legitimate:
167 // the arm not taken is dropped part way through its wait and loses
168 // nothing by it.
169 let heard = tokio::select! {
170 got = int.recv() => got,
171 got = term.recv() => got,
172 };
173 match heard {
174 Some(()) => on_ask(),
175 // Neither stream ends while the process lives. If one somehow did,
176 // there would be nothing left to hear and going round again would
177 // only spin.
178 None => return Ok(()),
179 }
180 }
181}
182
183/// Waits on the three console events Windows sends, answering each.
184///
185/// Ctrl-C, the console window being closed, and the machine shutting down.
186/// The last two are on a clock: Windows gives the process a few seconds after
187/// them and then ends it regardless, so whatever `on_ask` sets in motion should
188/// be brief.
189#[cfg(windows)]
190async fn wait<F>(on_ask: &F) -> Outcome<()>
191where
192 F: Fn(),
193{
194 use tokio::signal::windows::{
195 ctrl_c,
196 ctrl_close,
197 ctrl_shutdown,
198 };
199
200 let mut int = res!(ctrl_c(), Init, System);
201 let mut closed = res!(ctrl_close(), Init, System);
202 let mut down = res!(ctrl_shutdown(), Init, System);
203 loop {
204 let heard = tokio::select! {
205 got = int.recv() => got,
206 got = closed.recv() => got,
207 got = down.recv() => got,
208 };
209 match heard {
210 Some(()) => on_ask(),
211 None => return Ok(()),
212 }
213 }
214}
215
216/// Waits on Ctrl-C, which is all any other platform promises.
217#[cfg(not(any(unix, windows)))]
218async fn wait<F>(on_ask: &F) -> Outcome<()>
219where
220 F: Fn(),
221{
222 loop {
223 res!(tokio::signal::ctrl_c().await, IO, System);
224 on_ask();
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 /// A listener installs, and a second one is refused rather than stacked.
233 ///
234 /// What this can check in-process, and no more. A test cannot send itself a
235 /// signal and go on being a test: the closure would run in whichever test
236 /// binary happened to be sharing the process. The claim that a real signal
237 /// reaches a real program is a process-level test, and belongs to whoever
238 /// has a program to stop.
239 #[test]
240 fn test_a_listener_installs_once_00() -> Outcome<()> {
241 res!(on_stop_request(|| {}));
242 let again = on_stop_request(|| {});
243 req!(again.is_err(), true,
244 "A second listener was installed. Two threads would then answer \
245 the same signal.");
246 Ok(())
247 }
248}