mj_controller/
termination.rs1use std::sync::Arc;
9#[cfg(unix)]
10use std::sync::atomic::AtomicBool;
11use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
12#[cfg(unix)]
13use std::time::Duration;
14
15use tokio_util::sync::CancellationToken;
16
17#[cfg(windows)]
18use tokio::signal::windows::{CtrlBreak, CtrlC};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21enum SignalAction {
22 Graceful,
23 Force,
24}
25
26static SUPPRESSED_INTERRUPTS: AtomicUsize = AtomicUsize::new(0);
27
28#[cfg(unix)]
29const SIGNAL_POLL_INTERVAL: Duration = Duration::from_millis(10);
30
31pub struct SuppressInterruptGuard;
37
38pub fn suppress_interrupts() -> SuppressInterruptGuard {
39 SUPPRESSED_INTERRUPTS.fetch_add(1, Ordering::AcqRel);
40 SuppressInterruptGuard
41}
42
43impl Drop for SuppressInterruptGuard {
44 fn drop(&mut self) {
45 SUPPRESSED_INTERRUPTS.fetch_sub(1, Ordering::AcqRel);
46 }
47}
48
49fn next_signal_action(signals_seen: &AtomicU8) -> SignalAction {
52 match signals_seen.fetch_add(1, Ordering::AcqRel) {
53 0 => SignalAction::Graceful,
54 _ => SignalAction::Force,
55 }
56}
57
58#[derive(Clone, Debug)]
59pub struct Coordinator {
60 token: CancellationToken,
61 signals_seen: Arc<AtomicU8>,
62}
63
64impl Coordinator {
65 pub fn install() -> Self {
66 let coordinator = Self {
67 token: CancellationToken::new(),
68 signals_seen: Arc::new(AtomicU8::new(0)),
69 };
70 #[cfg(unix)]
71 install_unix_signals(&coordinator);
72 #[cfg(windows)]
73 {
74 let (ctrl_c, ctrl_break) = install_windows_signals();
78 let listener = coordinator.clone();
79 tokio::spawn(async move { listener.listen(ctrl_c, ctrl_break).await });
80 }
81 coordinator
82 }
83
84 pub fn token(&self) -> CancellationToken {
85 self.token.clone()
86 }
87
88 fn received_signal(&self, signal: i32) {
89 #[cfg(unix)]
90 if signal == libc::SIGINT && SUPPRESSED_INTERRUPTS.load(Ordering::Acquire) > 0 {
91 return;
92 }
93 #[cfg(windows)]
94 if signal == 0 && SUPPRESSED_INTERRUPTS.load(Ordering::Acquire) > 0 {
95 return;
96 }
97 #[cfg(unix)]
103 if signal == libc::SIGHUP {
104 std::process::exit(exit_code(signal));
105 }
106 match next_signal_action(&self.signals_seen) {
107 SignalAction::Graceful => self.token.cancel(),
108 SignalAction::Force => std::process::exit(exit_code(signal)),
109 }
110 }
111
112 #[cfg(windows)]
113 async fn listen(self, mut ctrl_c: CtrlC, mut ctrl_break: CtrlBreak) {
114 loop {
115 tokio::select! {
116 _ = ctrl_c.recv() => self.received_signal(0),
117 _ = ctrl_break.recv() => self.received_signal(1),
118 }
119 }
120 }
121}
122
123#[cfg(unix)]
124fn install_unix_signals(coordinator: &Coordinator) {
125 let requested = Arc::new(AtomicBool::new(false));
126 for signal in [libc::SIGINT, libc::SIGTERM] {
127 signal_hook::flag::register_conditional_shutdown(
130 signal,
131 exit_code(signal),
132 requested.clone(),
133 )
134 .expect("install forced termination signal handler");
135 let requested = requested.clone();
136 unsafe {
140 signal_hook::low_level::register(signal, move || {
141 if signal != libc::SIGINT || SUPPRESSED_INTERRUPTS.load(Ordering::Acquire) == 0 {
142 requested.store(true, Ordering::SeqCst);
143 }
144 })
145 }
146 .expect("install graceful termination signal handler");
147 }
148
149 signal_hook::flag::register_conditional_shutdown(
150 libc::SIGHUP,
151 exit_code(libc::SIGHUP),
152 Arc::new(AtomicBool::new(true)),
153 )
154 .expect("install hangup signal handler");
155
156 let listener = coordinator.clone();
157 std::thread::Builder::new()
158 .name("hel-termination".to_string())
159 .spawn(move || {
160 while !requested.load(Ordering::Acquire) {
161 std::thread::sleep(SIGNAL_POLL_INTERVAL);
162 }
163 listener.received_signal(0);
164 })
165 .expect("spawn termination signal listener");
166}
167
168#[cfg(windows)]
169fn install_windows_signals() -> (CtrlC, CtrlBreak) {
170 use tokio::signal::windows::{ctrl_break, ctrl_c};
171
172 (
173 ctrl_c().expect("install Ctrl-C listener"),
174 ctrl_break().expect("install Ctrl-Break listener"),
175 )
176}
177
178#[cfg(unix)]
179const fn exit_code(signal: i32) -> i32 {
180 128 + signal
181}
182
183#[cfg(not(unix))]
184const fn exit_code(_signal: i32) -> i32 {
185 1
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 static INTERRUPT_SUPPRESSION_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
193
194 #[test]
195 fn first_then_repeated_signal_transitions_to_force() {
196 let signals_seen = AtomicU8::new(0);
197 assert_eq!(next_signal_action(&signals_seen), SignalAction::Graceful);
198 assert_eq!(next_signal_action(&signals_seen), SignalAction::Force);
199 }
200
201 #[test]
202 fn interrupt_suppression_is_scoped() {
203 let _lock = INTERRUPT_SUPPRESSION_TEST_LOCK.lock().unwrap();
204 assert_eq!(SUPPRESSED_INTERRUPTS.load(Ordering::Acquire), 0);
205 {
206 let _guard = suppress_interrupts();
207 assert_eq!(SUPPRESSED_INTERRUPTS.load(Ordering::Acquire), 1);
208 }
209 assert_eq!(SUPPRESSED_INTERRUPTS.load(Ordering::Acquire), 0);
210 }
211
212 #[cfg(unix)]
213 #[test]
214 fn suppressed_interrupt_does_not_advance_shutdown() {
215 let _lock = INTERRUPT_SUPPRESSION_TEST_LOCK.lock().unwrap();
216 let coordinator = Coordinator {
217 token: CancellationToken::new(),
218 signals_seen: Arc::new(AtomicU8::new(0)),
219 };
220
221 let guard = suppress_interrupts();
222 coordinator.received_signal(libc::SIGINT);
223 assert!(!coordinator.token().is_cancelled());
224 assert_eq!(coordinator.signals_seen.load(Ordering::Acquire), 0);
225
226 drop(guard);
227 coordinator.received_signal(libc::SIGINT);
228 assert!(coordinator.token().is_cancelled());
229 assert_eq!(coordinator.signals_seen.load(Ordering::Acquire), 1);
230 }
231
232 #[tokio::test]
233 async fn coordinator_cancellation_fans_out_to_late_subscribers() {
234 let lock = INTERRUPT_SUPPRESSION_TEST_LOCK.lock().unwrap();
235 let coordinator = Coordinator {
236 token: CancellationToken::new(),
237 signals_seen: Arc::new(AtomicU8::new(0)),
238 };
239 let early = coordinator.token().child_token();
240 coordinator.received_signal(0);
241 drop(lock);
242 let late = coordinator.token().child_token();
243 early.cancelled().await;
244 late.cancelled().await;
245 }
246
247 #[cfg(windows)]
248 #[tokio::test]
249 async fn windows_signal_streams_register_synchronously() {
250 let (_ctrl_c, _ctrl_break) = install_windows_signals();
251 }
252}