use std::io::Write;
use std::sync::Arc;
use crate::cli::{STYLE_ERROR, paint};
use crate::progress_ticker::Line;
pub(crate) const INTERRUPTED: &str = "fdu: interrupted";
pub(crate) fn on_interrupt(line: &Line, color: bool, terminate: impl FnOnce()) {
{
let mut state = line.lock();
let was_drawing = !state.stopped;
state.stop();
if was_drawing {
let _ = writeln!(state.out, "{}", paint(INTERRUPTED, STYLE_ERROR, color))
.and_then(|()| state.out.flush());
}
}
terminate();
}
#[cfg(unix)]
pub(crate) fn install(line: Arc<Line>, color: bool) {
use signal_hook::consts::SIGINT;
use signal_hook::iterator::Signals;
use signal_hook::low_level::emulate_default_handler;
let Ok(mut signals) = Signals::new([SIGINT]) else {
return;
};
let _ = std::thread::Builder::new().name("fdu-interrupt".to_string()).spawn(move || {
if signals.forever().next().is_some() {
on_interrupt(&line, color, || {
let _ = emulate_default_handler(SIGINT);
});
}
});
}
#[cfg(windows)]
pub(crate) fn install(line: Arc<Line>, color: bool) {
const STATUS_CONTROL_C_EXIT: i32 = i32::from_ne_bytes(0xC000_013A_u32.to_ne_bytes());
let _ = ctrlc::set_handler(move || {
on_interrupt(&line, color, || std::process::exit(STATUS_CONTROL_C_EXIT));
});
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use crate::progress_ticker::{ERASE_LINE, SharedBuffer};
#[test]
fn an_interrupt_stops_erases_says_so_and_only_then_terminates() {
let out = SharedBuffer::default();
let line = Line::new(Box::new(out.clone()));
line.lock().draw("⠋ ~ Scanning");
assert!(line.lock().frame_on_screen);
let at_terminate = Mutex::new(None);
on_interrupt(&line, false, || {
let state = line.lock();
*at_terminate.lock().expect("no poison") =
Some((state.stopped, state.frame_on_screen, out.text()));
});
let (stopped, frame_on_screen, text) =
at_terminate.into_inner().expect("no poison").expect("terminate was called");
assert!(stopped, "the line is stopped before the process ends");
assert!(!frame_on_screen);
assert_eq!(text, format!("{ERASE_LINE}⠋ ~ Scanning{ERASE_LINE}{INTERRUPTED}\n"));
assert_eq!(out.text(), text, "nothing is written after terminate");
}
#[test]
fn the_message_takes_the_error_style_under_the_color_rule() {
let out = SharedBuffer::default();
let line = Line::new(Box::new(out.clone()));
on_interrupt(&line, true, || {});
assert_eq!(out.text(), "\u{1b}[1m\u{1b}[31mfdu: interrupted\u{1b}[0m\n");
assert!(!out.text().contains(ERASE_LINE), "no frame was showing, so nothing to erase");
}
#[test]
fn after_the_ticker_has_stopped_an_interrupt_is_silent_but_still_terminates() {
let out = SharedBuffer::default();
let line = Line::new(Box::new(out.clone()));
line.lock().draw("⠋ ~ Scanning");
line.lock().stop();
let before = out.text();
assert!(before.ends_with(ERASE_LINE));
let mut terminated = false;
on_interrupt(&line, false, || terminated = true);
assert!(terminated);
assert_eq!(out.text(), before, "no message once the report is being written");
on_interrupt(&line, false, || {});
assert_eq!(out.text(), before);
}
#[test]
fn no_frame_is_drawn_after_an_interrupt_stops_the_line() {
use crate::progress_line::ProgressPlan;
use crate::progress_ticker::{ProgressIo, Ticker, Timing, scanned};
let root = tempfile::tempdir().expect("tempdir");
let out = SharedBuffer::default();
let io = ProgressIo {
out: Box::new(out.clone()),
timing: Timing { delay: Duration::ZERO, tick: Duration::from_millis(1) },
width: || 100,
interrupt: |_, _| {},
};
let plan = ProgressPlan {
draw: true,
root: "~".to_string(),
color: false,
size: fdu_core::query::SizeMetric::Allocated,
};
let mut ticker = Ticker::start(plan, scanned(root.path()), Instant::now(), io);
let deadline = Instant::now() + Duration::from_secs(10);
while out.contents().is_empty() {
assert!(Instant::now() < deadline, "the ticker never drew");
std::thread::sleep(Duration::from_millis(1));
}
on_interrupt(ticker.line(), false, || {});
let after_interrupt = out.text();
assert!(after_interrupt.ends_with(&format!("{ERASE_LINE}{INTERRUPTED}\n")));
std::thread::sleep(Duration::from_millis(30));
assert_eq!(out.text(), after_interrupt, "a frame was drawn after the interrupt");
ticker.stop();
assert_eq!(out.text(), after_interrupt);
}
}