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