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}