Skip to main content

supercov_engine/
progress.rs

1//! A single branded status line on stderr while a long, otherwise-silent
2//! step runs.
3//!
4//! After one short delay the line `❋ message…` appears once — no animation,
5//! no redrawing — so agents, CI logs, and quick commands never see churn,
6//! and fast steps see nothing at all. It only appears when stderr is an
7//! interactive terminal. The owner must drop it before writing other output.
8
9// The status line is written only on Unix; the Windows arm stays silent, so
10// the trait is unused there and the first Windows build said so.
11use std::{
12    io::IsTerminal,
13    sync::{
14        Arc,
15        atomic::{AtomicBool, Ordering},
16    },
17    thread::JoinHandle,
18};
19#[cfg(unix)]
20use std::{io::Write, time::Duration};
21
22/// How long a step must run before the status line appears.
23// Read only by the Unix progress path and its test.
24#[cfg(unix)]
25const QUIET_PERIOD: Duration = Duration::from_millis(120);
26
27/// The run holds std's locked stderr as its diagnostics writer, so this
28/// thread must never take that lock: an `eprintln!` here deadlocks against
29/// it, and joining the thread then hangs the run — the field case was a real
30/// project's first slow workspace phase on a TTY. The line goes straight to
31/// a duplicate of the descriptor instead.
32#[cfg(unix)]
33fn status_output() -> Option<std::fs::File> {
34    use std::os::fd::FromRawFd;
35    let descriptor = unsafe { libc::dup(2) };
36    (descriptor >= 0).then(|| unsafe { std::fs::File::from_raw_fd(descriptor) })
37}
38
39pub struct ProgressLine {
40    stop: Arc<AtomicBool>,
41    handle: Option<JoinHandle<()>>,
42}
43
44impl ProgressLine {
45    pub fn start(message: &'static str) -> Option<Self> {
46        if cfg!(not(unix)) || !std::io::stderr().is_terminal() {
47            return None;
48        }
49        Self::start_on_terminal(message)
50    }
51
52    #[cfg(unix)]
53    fn start_on_terminal(message: &'static str) -> Option<Self> {
54        let mut output = status_output()?;
55        let stop = Arc::new(AtomicBool::new(false));
56        let flag = Arc::clone(&stop);
57        let handle = std::thread::spawn(move || {
58            std::thread::sleep(QUIET_PERIOD);
59            if flag.load(Ordering::Relaxed) {
60                return;
61            }
62            let _ = writeln!(output, "❋ {message}…");
63        });
64        Some(Self {
65            stop,
66            handle: Some(handle),
67        })
68    }
69
70    #[cfg(not(unix))]
71    fn start_on_terminal(_message: &'static str) -> Option<Self> {
72        // Only unix has the lock-free descriptor path; other platforms stay
73        // silent rather than risk the diagnostics writer's stderr lock.
74        None
75    }
76}
77
78impl Drop for ProgressLine {
79    fn drop(&mut self) {
80        self.stop.store(true, Ordering::Relaxed);
81        if let Some(handle) = self.handle.take() {
82            let _ = handle.join();
83        }
84    }
85}
86
87#[cfg(all(test, unix))]
88mod tests {
89    use super::*;
90
91    /// The regression that shipped in 0.0.22: the run holds std's stderr
92    /// lock as its diagnostics writer for the whole run, the status thread
93    /// blocked on that lock via `eprint!`, and `Drop`'s join then hung the
94    /// process. The status line must start, write, and drop to completion
95    /// while the calling thread holds std's stderr lock.
96    #[test]
97    fn status_line_never_needs_stds_stderr_lock() {
98        let diagnostics = std::io::stderr().lock();
99        let line = ProgressLine::start_on_terminal("proving the status line stays lock-free");
100        std::thread::sleep(QUIET_PERIOD * 2);
101        let (sender, receiver) = std::sync::mpsc::channel();
102        std::thread::spawn(move || {
103            drop(line);
104            let _ = sender.send(());
105        });
106        let dropped = receiver.recv_timeout(Duration::from_secs(10));
107        drop(diagnostics);
108        assert!(
109            dropped.is_ok(),
110            "dropping the status line deadlocked against std's stderr lock"
111        );
112    }
113}