mj_controller/
termination.rs1use std::sync::Arc;
9#[cfg(unix)]
10use std::sync::atomic::AtomicBool;
11use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
12
13use tokio_util::sync::CancellationToken;
14
15#[cfg(windows)]
16use tokio::signal::windows::{CtrlBreak, CtrlC};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19enum SignalAction {
20 Graceful,
21 Force,
22}
23
24static SUPPRESSED_INTERRUPTS: AtomicUsize = AtomicUsize::new(0);
25
26pub struct SuppressInterruptGuard;
32
33pub fn suppress_interrupts() -> SuppressInterruptGuard {
34 SUPPRESSED_INTERRUPTS.fetch_add(1, Ordering::AcqRel);
35 SuppressInterruptGuard
36}
37
38impl Drop for SuppressInterruptGuard {
39 fn drop(&mut self) {
40 SUPPRESSED_INTERRUPTS.fetch_sub(1, Ordering::AcqRel);
41 }
42}
43
44fn next_signal_action(signals_seen: &AtomicU8) -> SignalAction {
47 match signals_seen.fetch_add(1, Ordering::AcqRel) {
48 0 => SignalAction::Graceful,
49 _ => SignalAction::Force,
50 }
51}
52
53#[derive(Clone, Debug)]
54pub struct Coordinator {
55 token: CancellationToken,
56 signals_seen: Arc<AtomicU8>,
57}
58
59impl Coordinator {
60 pub fn install() -> Self {
61 let coordinator = Self {
62 token: CancellationToken::new(),
63 signals_seen: Arc::new(AtomicU8::new(0)),
64 };
65 #[cfg(unix)]
66 install_unix_signals(&coordinator);
67 #[cfg(windows)]
68 {
69 let (ctrl_c, ctrl_break) = install_windows_signals();
73 let listener = coordinator.clone();
74 tokio::spawn(async move { listener.listen(ctrl_c, ctrl_break).await });
75 }
76 coordinator
77 }
78
79 pub fn token(&self) -> CancellationToken {
80 self.token.clone()
81 }
82
83 fn received_signal(&self, signal: i32) {
84 #[cfg(unix)]
85 if signal == libc::SIGINT && SUPPRESSED_INTERRUPTS.load(Ordering::Acquire) > 0 {
86 return;
87 }
88 #[cfg(windows)]
89 if signal == 0 && SUPPRESSED_INTERRUPTS.load(Ordering::Acquire) > 0 {
90 return;
91 }
92 #[cfg(unix)]
98 if signal == libc::SIGHUP {
99 std::process::exit(exit_code(signal));
100 }
101 match next_signal_action(&self.signals_seen) {
102 SignalAction::Graceful => self.token.cancel(),
103 SignalAction::Force => std::process::exit(exit_code(signal)),
104 }
105 }
106
107 #[cfg(windows)]
108 async fn listen(self, mut ctrl_c: CtrlC, mut ctrl_break: CtrlBreak) {
109 loop {
110 tokio::select! {
111 _ = ctrl_c.recv() => self.received_signal(0),
112 _ = ctrl_break.recv() => self.received_signal(1),
113 }
114 }
115 }
116}
117
118#[cfg(unix)]
119fn install_unix_signals(coordinator: &Coordinator) {
120 use std::io::Read;
121 use std::os::fd::AsRawFd;
122
123 let (mut wake_reader, wake_writer) =
128 std::io::pipe().expect("create termination signal wake pipe");
129 let nonblocking = unsafe {
131 let fd = wake_writer.as_raw_fd();
132 let flags = libc::fcntl(fd, libc::F_GETFL);
133 flags != -1 && libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) != -1
134 };
135 assert!(
136 nonblocking,
137 "make the termination signal wake pipe non-blocking: {}",
138 std::io::Error::last_os_error()
139 );
140 let wake_writer = Arc::new(wake_writer);
143
144 let requested = Arc::new(AtomicBool::new(false));
145 for signal in [libc::SIGINT, libc::SIGTERM] {
146 signal_hook::flag::register_conditional_shutdown(
150 signal,
151 exit_code(signal),
152 requested.clone(),
153 )
154 .expect("install forced termination signal handler");
155 let requested = requested.clone();
156 let wake_writer = wake_writer.clone();
157 unsafe {
164 signal_hook::low_level::register(signal, move || {
165 if signal != libc::SIGINT || SUPPRESSED_INTERRUPTS.load(Ordering::Acquire) == 0 {
166 requested.store(true, Ordering::SeqCst);
167 libc::write(wake_writer.as_raw_fd(), b"x".as_ptr().cast(), 1);
168 }
169 })
170 }
171 .expect("install graceful termination signal handler");
172 }
173
174 signal_hook::flag::register_conditional_shutdown(
175 libc::SIGHUP,
176 exit_code(libc::SIGHUP),
177 Arc::new(AtomicBool::new(true)),
178 )
179 .expect("install hangup signal handler");
180
181 let listener = coordinator.clone();
182 std::thread::Builder::new()
183 .name("hel-termination".to_string())
184 .spawn(move || {
185 let mut wake = [0_u8; 64];
191 while !requested.load(Ordering::Acquire) {
192 match wake_reader.read(&mut wake) {
193 Ok(0) => {
194 tracing::error!(
195 "termination wake pipe closed; a signal can no longer start graceful shutdown"
196 );
197 return;
198 }
199 Ok(_) => {}
200 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
201 Err(error) => {
202 tracing::error!(
203 %error,
204 "termination wake pipe failed; a signal can no longer start graceful shutdown"
205 );
206 return;
207 }
208 }
209 }
210 listener.received_signal(0);
211 })
212 .expect("spawn termination signal listener");
213}
214
215#[cfg(windows)]
216fn install_windows_signals() -> (CtrlC, CtrlBreak) {
217 use tokio::signal::windows::{ctrl_break, ctrl_c};
218
219 (
220 ctrl_c().expect("install Ctrl-C listener"),
221 ctrl_break().expect("install Ctrl-Break listener"),
222 )
223}
224
225#[cfg(unix)]
226const fn exit_code(signal: i32) -> i32 {
227 128 + signal
228}
229
230#[cfg(not(unix))]
231const fn exit_code(_signal: i32) -> i32 {
232 1
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 static INTERRUPT_SUPPRESSION_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
240
241 #[test]
242 fn first_then_repeated_signal_transitions_to_force() {
243 let signals_seen = AtomicU8::new(0);
244 assert_eq!(next_signal_action(&signals_seen), SignalAction::Graceful);
245 assert_eq!(next_signal_action(&signals_seen), SignalAction::Force);
246 }
247
248 #[cfg(unix)]
249 #[test]
250 fn suppressed_interrupt_does_not_advance_shutdown() {
251 let _lock = INTERRUPT_SUPPRESSION_TEST_LOCK.lock().unwrap();
252 let coordinator = Coordinator {
253 token: CancellationToken::new(),
254 signals_seen: Arc::new(AtomicU8::new(0)),
255 };
256
257 let guard = suppress_interrupts();
258 coordinator.received_signal(libc::SIGINT);
259 assert!(!coordinator.token().is_cancelled());
260 assert_eq!(coordinator.signals_seen.load(Ordering::Acquire), 0);
261
262 drop(guard);
263 coordinator.received_signal(libc::SIGINT);
264 assert!(coordinator.token().is_cancelled());
265 assert_eq!(coordinator.signals_seen.load(Ordering::Acquire), 1);
266 }
267
268 #[tokio::test]
269 async fn coordinator_cancellation_fans_out_to_late_subscribers() {
270 let lock = INTERRUPT_SUPPRESSION_TEST_LOCK.lock().unwrap();
271 let coordinator = Coordinator {
272 token: CancellationToken::new(),
273 signals_seen: Arc::new(AtomicU8::new(0)),
274 };
275 let early = coordinator.token().child_token();
276 coordinator.received_signal(0);
277 drop(lock);
278 let late = coordinator.token().child_token();
279 early.cancelled().await;
280 late.cancelled().await;
281 }
282}