1use std::io;
16use std::sync::{Arc, Condvar, Mutex};
17use std::thread::JoinHandle;
18use std::time::{Duration, Instant};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum StopCause {
23 Interrupt,
25 Terminate,
27 StdinClosed,
29 Requested,
31}
32
33struct Inner {
34 state: Mutex<Option<StopCause>>,
35 changed: Condvar,
36}
37
38#[derive(Clone)]
40pub struct StopSource {
41 inner: Arc<Inner>,
42}
43
44#[derive(Clone)]
46pub struct StopToken {
47 inner: Arc<Inner>,
48}
49
50impl Default for StopSource {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56impl StopSource {
57 pub fn new() -> Self {
58 Self {
59 inner: Arc::new(Inner {
60 state: Mutex::new(None),
61 changed: Condvar::new(),
62 }),
63 }
64 }
65
66 pub fn token(&self) -> StopToken {
68 StopToken {
69 inner: self.inner.clone(),
70 }
71 }
72
73 pub fn trigger(&self, cause: StopCause) -> bool {
75 let mut state = self.inner.state.lock().unwrap_or_else(|e| e.into_inner());
76 if state.is_some() {
77 return false;
78 }
79 *state = Some(cause);
80 self.inner.changed.notify_all();
81 true
82 }
83}
84
85impl StopToken {
86 pub fn cause(&self) -> Option<StopCause> {
88 *self.inner.state.lock().unwrap_or_else(|e| e.into_inner())
89 }
90
91 pub fn is_stopped(&self) -> bool {
92 self.cause().is_some()
93 }
94
95 pub fn wait(&self, timeout: Duration) -> Option<StopCause> {
98 let deadline = Instant::now() + timeout;
99 let mut state = self.inner.state.lock().unwrap_or_else(|e| e.into_inner());
100 loop {
101 if let Some(cause) = *state {
102 return Some(cause);
103 }
104 let now = Instant::now();
105 if now >= deadline {
106 return None;
107 }
108 let (next, _) = self
109 .inner
110 .changed
111 .wait_timeout(state, deadline - now)
112 .unwrap_or_else(|e| e.into_inner());
113 state = next;
114 }
115 }
116}
117
118#[cfg(unix)]
120static PENDING_SIGNAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
121
122#[cfg(unix)]
123const SIGINT: i32 = 2;
124#[cfg(unix)]
125const SIGTERM: i32 = 15;
126
127#[cfg(unix)]
128extern "C" fn on_unix_signal(signal: i32) {
129 PENDING_SIGNAL.store(signal, std::sync::atomic::Ordering::SeqCst);
131}
132
133#[cfg(unix)]
134extern "C" {
135 fn signal(signum: i32, handler: usize) -> usize;
136}
137
138#[cfg(windows)]
139static PENDING_CONSOLE_EVENT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
140
141#[cfg(windows)]
142unsafe extern "system" fn on_console_event(event: u32) -> i32 {
143 PENDING_CONSOLE_EVENT.store(event + 1, std::sync::atomic::Ordering::SeqCst);
144 1 }
146
147#[cfg(windows)]
148extern "system" {
149 fn SetConsoleCtrlHandler(
150 handler: Option<unsafe extern "system" fn(u32) -> i32>,
151 add: i32,
152 ) -> i32;
153}
154
155pub fn stop_on_signals(source: &StopSource) -> io::Result<JoinHandle<()>> {
158 install_os_handlers()?;
159 let source = source.clone();
160 std::thread::Builder::new()
161 .name("rk-stop-signals".into())
162 .spawn(move || loop {
163 if source.token().is_stopped() {
164 break;
165 }
166 if let Some(cause) = take_pending_signal() {
167 source.trigger(cause);
168 break;
169 }
170 std::thread::sleep(Duration::from_millis(50));
171 })
172}
173
174pub fn stop_on_stdin_close(source: &StopSource) -> JoinHandle<()> {
176 let source = source.clone();
177 crate::private::watch_stdin_eof(move || {
178 source.trigger(StopCause::StdinClosed);
179 })
180}
181
182#[cfg(unix)]
183fn install_os_handlers() -> io::Result<()> {
184 for sig in [SIGINT, SIGTERM] {
185 let handler = on_unix_signal as extern "C" fn(i32) as usize;
186 let previous = unsafe { signal(sig, handler) };
189 if previous == usize::MAX {
190 return Err(io::Error::last_os_error());
191 }
192 }
193 Ok(())
194}
195
196#[cfg(unix)]
197fn take_pending_signal() -> Option<StopCause> {
198 match PENDING_SIGNAL.swap(0, std::sync::atomic::Ordering::SeqCst) {
199 0 => None,
200 SIGINT => Some(StopCause::Interrupt),
201 _ => Some(StopCause::Terminate),
202 }
203}
204
205#[cfg(windows)]
206fn install_os_handlers() -> io::Result<()> {
207 let ok = unsafe { SetConsoleCtrlHandler(Some(on_console_event), 1) };
209 if ok == 0 {
210 return Err(io::Error::last_os_error());
211 }
212 Ok(())
213}
214
215#[cfg(windows)]
216fn take_pending_signal() -> Option<StopCause> {
217 match PENDING_CONSOLE_EVENT.swap(0, std::sync::atomic::Ordering::SeqCst) {
220 0 => None,
221 1 | 2 => Some(StopCause::Interrupt),
222 _ => Some(StopCause::Terminate),
223 }
224}
225
226#[cfg(not(any(unix, windows)))]
227fn install_os_handlers() -> io::Result<()> {
228 Err(io::Error::new(
229 io::ErrorKind::Unsupported,
230 "stop signals are not implemented on this platform",
231 ))
232}
233
234#[cfg(not(any(unix, windows)))]
235fn take_pending_signal() -> Option<StopCause> {
236 None
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn the_first_trigger_wins() {
245 let source = StopSource::new();
246 assert!(source.trigger(StopCause::Terminate));
247 assert!(!source.trigger(StopCause::StdinClosed));
248 assert_eq!(source.token().cause(), Some(StopCause::Terminate));
249 }
250
251 #[test]
252 fn a_wait_times_out_until_triggered() {
253 let source = StopSource::new();
254 let token = source.token();
255 assert_eq!(token.wait(Duration::from_millis(5)), None);
256 assert!(!token.is_stopped());
257 let clone = source.clone();
258 let t = std::thread::spawn(move || {
259 std::thread::sleep(Duration::from_millis(10));
260 clone.trigger(StopCause::Requested);
261 });
262 assert_eq!(
263 token.wait(Duration::from_secs(5)),
264 Some(StopCause::Requested)
265 );
266 t.join().unwrap();
267 }
268}