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