breakmancer/sigint.rs
1//! Supports managed suppression of interrupt signals.
2//!
3//! When the user presses ctrl-c when interacting with a CLI, the
4//! application receives an interrupt signal (SIGINT). Applications by
5//! default do not handle this signal, and therefore are terminated
6//! instead.
7//!
8//! If you want to explicitly handle this signal, you can register a
9//! handler, but then ctrl-c will not terminate your application
10//! unless your handler explicitly invokes `exit`. This is then the
11//! opposite problem of the original one; rather than not being able
12//! to handle ctrl-c, you have to always handle it. Failing to do so
13//! means the user will not be able to terminate your CLI app without
14//! using annoying and non-obvious workarounds such as backgrounding
15//! or finding the process ID.
16//!
17//! The `Manager` struct takes the approach of registering a handler
18//! that will `exit(0)` whenever a SIGINT is received unless one or
19//! more suppressors are alive. Suppressors can be created and dropped
20//! at any time, and multiple may be alive concurrently.
21//!
22//! # Examples
23//!
24//! ```
25//! use breakmancer::sigint::Manager;
26//!
27//! let mut sigint_manager = Manager::setup(); // one-time setup
28//!
29//! let suppress = sigint_manager.suppress();
30//! // ...more code here...
31//! let interrupt_fired = suppress.release();
32//! ```
33//!
34//! The code that runs while suppression is in effect should be
35//! written to avoid any chance of deadlock or indefinite blocking, as
36//! that will leave the user with no easy way to terminate a hung
37//! application.
38//!
39//! [`SuppressorHandle::was_signaled`] can be queried at any time to
40//! determine whether a SIGINT has occurred since the start of this
41//! suppression (or the last call of the function). Additionally, the
42//! return value of [`SuppressorHandle::release`] gives a final
43//! opportunity to learn whether another SIGINT has occurred.
44
45use std::sync::atomic::{AtomicBool, Ordering::Relaxed};
46use std::sync::{Arc, Mutex, Weak};
47use std::thread;
48
49use signal_hook::consts::SIGINT;
50use signal_hook::iterator::Signals;
51
52/// The core of a [`SuppressorHandle`]. Dropping this releases the
53/// suppression.
54struct Suppressor {
55 /// True when a signal has arrived since the last check.
56 new_signal: AtomicBool,
57}
58
59impl Suppressor {
60 /// Record that a SIGINT has been received.
61 fn signal_received(&self) {
62 self.new_signal.store(true, Relaxed);
63 }
64}
65
66/// Keeps ^C from exiting the application.
67///
68/// Drop or call [`SuppressorHandle::release`] to remove this suppression.
69pub struct SuppressorHandle {
70 /// We use Arc here to share this with the manager (which only
71 /// keeps a Weak Arc.)
72 state: Arc<Suppressor>,
73}
74
75impl SuppressorHandle {
76 /// Create suppression handle. This doesn't do anything until the
77 /// manager registers it.
78 fn new() -> SuppressorHandle {
79 SuppressorHandle {
80 state: Arc::new(Suppressor {
81 new_signal: AtomicBool::new(false),
82 }),
83 }
84 }
85
86 /// Return whether signal has arrived since previous call.
87 pub fn was_signaled(&self) -> bool {
88 self.state.new_signal.swap(false, Relaxed)
89 }
90
91 /// Releases the suppression (via drop) and returns final signal state.
92 ///
93 /// Approximately equivalent to calling [`SuppressorHandle::was_signaled`] and then
94 /// dropping the handle.
95 pub fn release(self) -> bool {
96 self.was_signaled()
97 }
98}
99
100type Suppressors = Arc<Mutex<Vec<Weak<Suppressor>>>>;
101
102/// SIGINT manager.
103///
104/// Setting up the manager ensures that the interrupt signal will be
105/// converted to an application exit, but also that there is a way to
106/// suppress this behavior (via [`Manager::suppress`]).
107pub struct Manager {
108 suppressors: Suppressors,
109}
110
111/// Core of [`Manager::setup`].
112///
113/// Spawns a thread that will monitor for SIGINT and exit if there are
114/// no extant suppressors.
115fn enable_suppression(suppressors: Suppressors) {
116 thread::spawn(move || {
117 let mut signals = Signals::new([SIGINT])
118 .map_err(|err| format!("Couldn't register interrupt handler: {err}"))
119 .unwrap();
120
121 for _signal in signals.forever() {
122 let mut suppressors = suppressors.lock().unwrap();
123 if suppressors.is_empty() {
124 std::process::exit(0)
125 } else {
126 let mut remaining: Vec<Weak<Suppressor>> = vec![];
127 for weak in suppressors.iter() {
128 if let Some(s) = weak.upgrade() {
129 s.signal_received();
130 // Only keep the weak refs that are still valid
131 remaining.push(weak.clone());
132 }
133 }
134 *suppressors = remaining;
135 }
136 }
137 });
138}
139
140impl Manager {
141 /// Start handling SIGINT suppression and provide default (exit)
142 /// response when no suppressors exist.
143 pub fn setup() -> Manager {
144 let suppressors = Arc::new(Mutex::new(Vec::<Weak<Suppressor>>::new()));
145 enable_suppression(suppressors.clone());
146 Manager { suppressors }
147 }
148
149 /// Suppress the interrupt handler while the returned handle is alive.
150 pub fn suppress(&mut self) -> SuppressorHandle {
151 let handle = SuppressorHandle::new();
152 let suppressor = handle.state.clone();
153 self.suppressors
154 .lock()
155 .unwrap()
156 .push(Arc::downgrade(&suppressor));
157 handle
158 }
159}