Skip to main content

proc_cli/ui/
interrupt.rs

1//! Putting the terminal back when a signal ends proc.
2//!
3//! `watch` turns the terminal's raw mode on, switches to the alternate screen
4//! and hides the cursor; a confirmation prompt hides the cursor and turns off
5//! echo and line editing while it waits for a key. A normal end puts all of it
6//! back. A signal does not run any of that: the process just dies, and the
7//! shell is left typing nothing back and showing no cursor until `stty sane`.
8//!
9//! While a [`Guard`] is alive, SIGINT, SIGTERM, SIGHUP and SIGQUIT (on Windows
10//! Ctrl-C, Ctrl-Break and a closing console) are caught, the terminal is put
11//! back, and proc ends by that same signal, so a shell reports 128 plus the
12//! signal number (130 for Ctrl-C, 143 for SIGTERM, 129 for SIGHUP, 131 for
13//! SIGQUIT) and knows a signal ended it: Ctrl-C stops a script's loop. Windows
14//! has no signals, so there proc exits with 130. A signal is how a person ends
15//! a program, so it is not one of proc's own exit codes, and nothing is printed.
16//!
17//! The signal is not handled in the signal handler, which may not take a lock
18//! or allocate: `signal-hook` only records it, and a thread of ours that waits
19//! for it does the work. The thread is started by the first guard and then
20//! stays, so a signal after a guard is gone also ends proc this way (nothing
21//! is left to put back by then). While a guard is alive the calling thread
22//! blocks these signals, so they are always taken by that thread and never
23//! interrupt the one that is reading a key.
24//!
25//! Ctrl+C typed into a prompt is not a signal (the terminal is in raw mode, so
26//! the key arrives as a character and the prompt reports it as an interrupted
27//! read): [`ctrl_c`] ends proc the same way.
28
29use crate::ui::out;
30use crossterm::{cursor, execute, terminal};
31use std::io::Write;
32use std::sync::{Mutex, MutexGuard, Once};
33
34/// What the terminal state to put back is
35enum Restore {
36    /// `watch`: raw mode, the alternate screen and the hidden cursor
37    Screen,
38    /// A prompt: the cursor, and the line mode that was set before it asked
39    Prompt(Saved),
40}
41
42/// The terminal's line mode (echo, line editing) before a prompt changed it
43#[cfg(unix)]
44type Saved = Option<libc::termios>;
45#[cfg(not(unix))]
46struct Saved;
47
48/// What to put back if a signal comes now; none while nothing is changed
49static NEEDS: Mutex<Option<Restore>> = Mutex::new(None);
50
51fn needs() -> MutexGuard<'static, Option<Restore>> {
52    // The state is plain data; a panic while holding the lock leaves it right
53    NEEDS
54        .lock()
55        .unwrap_or_else(std::sync::PoisonError::into_inner)
56}
57
58/// Keeps the terminal state in `NEEDS` for as long as it is alive
59pub struct Guard(());
60
61impl Guard {
62    fn arm(restore: Restore) -> Self {
63        static LISTENING: Once = Once::new();
64        // The thread starts before this one blocks the signals, so it does not
65        // inherit the block
66        LISTENING.call_once(listen);
67        *needs() = Some(restore);
68        block_signals(true);
69        Guard(())
70    }
71}
72
73impl Drop for Guard {
74    fn drop(&mut self) {
75        block_signals(false);
76        *needs() = None;
77    }
78}
79
80/// From here until the guard is dropped, a signal puts the screen back: raw
81/// mode off, cursor shown, normal screen
82pub fn protect_screen() -> Guard {
83    Guard::arm(Restore::Screen)
84}
85
86/// From here until the guard is dropped, a signal shows the cursor and puts
87/// the line mode back to what it is now
88pub fn protect_prompt() -> Guard {
89    Guard::arm(Restore::Prompt(save_mode()))
90}
91
92/// Put the terminal back the way it was before `watch`: raw mode off, cursor
93/// shown, normal screen. Every step is tried, whatever an earlier one did.
94pub fn restore_screen(out: &mut impl Write) {
95    let _ = terminal::disable_raw_mode();
96    let _ = execute!(out, cursor::Show, terminal::LeaveAlternateScreen);
97}
98
99/// Show the cursor a prompt hid and put the line mode back
100fn restore_prompt(out: &mut impl Write, saved: &Saved) {
101    let _ = execute!(out, cursor::Show);
102    put_back_mode(saved);
103}
104
105#[cfg(unix)]
106fn save_mode() -> Saved {
107    let mut mode = std::mem::MaybeUninit::<libc::termios>::uninit();
108    // SAFETY: tcgetattr fills the struct when it returns 0
109    let read = unsafe { libc::tcgetattr(libc::STDIN_FILENO, mode.as_mut_ptr()) } == 0;
110    // SAFETY: initialised, because tcgetattr returned 0
111    read.then(|| unsafe { mode.assume_init() })
112}
113
114#[cfg(unix)]
115fn put_back_mode(saved: &Saved) {
116    if let Some(mode) = saved {
117        // SAFETY: a valid termios, read from this same descriptor earlier
118        unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, mode) };
119    }
120}
121
122#[cfg(not(unix))]
123fn save_mode() -> Saved {
124    Saved
125}
126
127#[cfg(not(unix))]
128fn put_back_mode(_: &Saved) {}
129
130/// Put back whatever the armed guard says is changed
131fn put_right() {
132    match needs().as_ref() {
133        Some(Restore::Screen) => restore_screen(&mut out::stdout_unrecorded()),
134        Some(Restore::Prompt(saved)) => restore_prompt(&mut out::stderr_unrecorded(), saved),
135        None => {}
136    }
137}
138
139/// Block (or unblock) the signals we listen for on the calling thread, so that
140/// another thread takes them: this one may be sitting in a read that a signal
141/// would cut short, and then fail and end proc before the signal's own thread
142/// had ended it with the right status.
143#[cfg(unix)]
144fn block_signals(block: bool) {
145    // SAFETY: an empty set filled with four valid signal numbers, then applied
146    // to this thread only
147    unsafe {
148        let mut set = std::mem::MaybeUninit::<libc::sigset_t>::uninit();
149        libc::sigemptyset(set.as_mut_ptr());
150        for signal in LISTENED {
151            libc::sigaddset(set.as_mut_ptr(), signal);
152        }
153        let how = if block {
154            libc::SIG_BLOCK
155        } else {
156            libc::SIG_UNBLOCK
157        };
158        libc::pthread_sigmask(how, set.as_ptr(), std::ptr::null_mut());
159    }
160}
161
162#[cfg(not(unix))]
163fn block_signals(_: bool) {}
164
165/// Start `command`'s child with no signal blocked. While a guard is alive this
166/// thread blocks the signals that end proc, and a child inherits the mask (the
167/// standard library keeps it), so an `lsof` that `watch` runs on a refresh
168/// could not be stopped by Ctrl-C, SIGTERM, SIGHUP or SIGQUIT.
169#[cfg(unix)]
170pub fn unblocked_in_child(command: &mut std::process::Command) -> &mut std::process::Command {
171    use std::os::unix::process::CommandExt;
172    // SAFETY: the hook runs in the child between fork and exec, and calls only
173    // sigemptyset and pthread_sigmask, which are async-signal-safe
174    unsafe { command.pre_exec(unblock_all) }
175}
176
177/// Block no signal on the calling thread
178#[cfg(unix)]
179fn unblock_all() -> std::io::Result<()> {
180    // SAFETY: an empty set, applied to this thread only
181    unsafe {
182        let mut set = std::mem::MaybeUninit::<libc::sigset_t>::uninit();
183        libc::sigemptyset(set.as_mut_ptr());
184        libc::pthread_sigmask(libc::SIG_SETMASK, set.as_ptr(), std::ptr::null_mut());
185    }
186    Ok(())
187}
188
189/// The signals that end proc, here and in `listen`
190#[cfg(unix)]
191const LISTENED: [i32; 4] = [
192    signal_hook::consts::SIGINT,
193    signal_hook::consts::SIGTERM,
194    signal_hook::consts::SIGHUP,
195    signal_hook::consts::SIGQUIT,
196];
197
198/// End proc as Ctrl+C does, status 130, after putting the terminal right. For a
199/// prompt that reports Ctrl+C as a key instead of the signal.
200pub fn ctrl_c() {
201    serve(std::iter::once(2), exit_with);
202}
203
204fn exit_with(code: i32) {
205    // End by the signal itself, as a program without this handler would. A
206    // shell then knows the signal ended proc: Ctrl-C stops a script's loop,
207    // which an exit status of 130 does not, though it reads the same. `exit`
208    // is for Windows, which has no signals.
209    #[cfg(unix)]
210    {
211        write_coverage();
212        let _ = signal_hook::low_level::emulate_default_handler(code - 128);
213    }
214    std::process::exit(code)
215}
216
217/// A process that a signal ends writes no coverage profile, so a coverage
218/// build writes it first: the tests that end proc by a signal are the only
219/// ones that run this part of it
220#[cfg(all(unix, coverage))]
221fn write_coverage() {
222    extern "C" {
223        fn __llvm_profile_write_file() -> libc::c_int;
224    }
225    // SAFETY: the profiler runtime that every coverage build links defines it
226    unsafe { __llvm_profile_write_file() };
227}
228
229#[cfg(all(unix, not(coverage)))]
230fn write_coverage() {}
231
232/// For each signal that arrives: put the terminal right, then end proc by it.
233/// `end` gets 128 plus the signal's number and is [`exit_with`]; a test passes
234/// something that does not exit.
235fn serve(signals: impl Iterator<Item = i32>, end: impl Fn(i32)) {
236    for signal in signals {
237        put_right();
238        end(128 + signal);
239    }
240}
241
242#[cfg(unix)]
243fn listen() {
244    // If the signals cannot be watched there is nothing to be done about it:
245    // proc goes on as it did before this existed
246    let _ = signal_hook::iterator::Signals::new(LISTENED)
247        .map(|mut signals| std::thread::spawn(move || serve(signals.forever(), exit_with)));
248}
249
250#[cfg(windows)]
251fn listen() {
252    let _ = ctrlc::set_handler(ctrl_c);
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use std::cell::RefCell;
259
260    #[test]
261    fn restoring_the_screen_shows_the_cursor_and_leaves_the_alternate_screen() {
262        let mut out = Vec::new();
263        restore_screen(&mut out);
264        assert_eq!(String::from_utf8(out).unwrap(), "\x1b[?25h\x1b[?1049l");
265    }
266
267    #[test]
268    fn restoring_a_prompt_shows_the_cursor() {
269        let mut out = Vec::new();
270        restore_prompt(&mut out, &save_mode());
271        assert_eq!(String::from_utf8(out).unwrap(), "\x1b[?25h");
272    }
273
274    // One test for both, because both use the one static
275    #[test]
276    fn a_signal_ends_proc_with_128_plus_its_number_and_a_guard_clears_the_state() {
277        let ended = RefCell::new(Vec::new());
278        // SIGHUP, SIGINT, SIGQUIT, SIGTERM
279        serve([1, 2, 3, 15].into_iter(), |code| {
280            ended.borrow_mut().push(code)
281        });
282        assert_eq!(*ended.borrow(), [129, 130, 131, 143]);
283
284        // Nothing is armed through `Guard::arm` here, which would start
285        // catching signals in the test run
286        *needs() = Some(Restore::Prompt(save_mode()));
287        assert!(needs().is_some());
288        drop(Guard(()));
289        assert!(needs().is_none());
290    }
291
292    // A child started while a guard blocks the signals can still be ended by
293    // them. `sleep` keeps the default action, so a blocked SIGTERM would leave
294    // it running for the whole minute.
295    #[cfg(unix)]
296    #[test]
297    fn a_child_started_while_the_signals_are_blocked_can_still_be_ended() {
298        use std::os::unix::process::ExitStatusExt;
299        block_signals(true);
300        let child = unblocked_in_child(&mut std::process::Command::new("sleep"))
301            .arg("60")
302            .spawn();
303        // The parent half of the test leaves this thread as it found it
304        let _ = unblock_all();
305        let mut child = child.unwrap();
306        unsafe { libc::kill(child.id() as i32, libc::SIGTERM) };
307        let start = std::time::Instant::now();
308        let status = child.wait().unwrap();
309        assert_eq!(status.signal(), Some(libc::SIGTERM));
310        assert!(start.elapsed() < std::time::Duration::from_secs(10));
311    }
312}