Skip to main content

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}