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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! Lines kache shows a person while it runs as a compiler wrapper: opt-in
//! progress, explain-miss diffs, heartbeats and the stuck-compiler warning.
//!
//! Cargo keeps a unit's compiler stderr and replays it whenever the unit is
//! fresh, so a line kache writes there reappears on later builds where kache
//! never ran for that unit (kunobi-ninja/kache#1007). These lines go to the
//! controlling terminal instead, which Cargo never captures. Without one (CI,
//! an IDE, a detached build) a line the person asked for with
//! `KACHE_PROGRESS` still goes to stderr, where they expect it, and anything
//! else goes to the log.
//!
//! Only Unix has a terminal here. Windows' `CONOUT$` opens whenever the
//! process has a console, including the hidden one a CI runner attaches, so
//! it cannot tell a person watching from nobody; Windows keeps the fallback.
use std::io::Write;
/// Where a line ended up.
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Shown {
Terminal,
Stderr,
Log,
}
/// Show a line nobody asked for: to the terminal, else to the log.
pub(crate) fn show(line: &str) {
deliver(terminal(), None::<std::io::Stderr>, line);
}
/// Show a line the person asked for: to the terminal, else to stderr.
pub(crate) fn show_requested(line: &str) {
deliver(terminal(), Some(std::io::stderr()), line);
}
/// Write `line` to `terminal` when there is one and it takes the write,
/// else to `stderr` when given, else to the log.
fn deliver(terminal: Option<impl Write>, stderr: Option<impl Write>, line: &str) -> Shown {
if let Some(mut terminal) = terminal
&& writeln!(terminal, "{line}").is_ok()
{
return Shown::Terminal;
}
if let Some(mut stderr) = stderr
&& writeln!(stderr, "{line}").is_ok()
{
return Shown::Stderr;
}
tracing::info!(target: "kache::notice", "{line}");
Shown::Log
}
/// The controlling terminal, if this process has one.
#[cfg(unix)]
fn terminal() -> Option<std::fs::File> {
std::fs::OpenOptions::new()
.write(true)
.open("/dev/tty")
.ok()
}
#[cfg(not(unix))]
fn terminal() -> Option<std::fs::File> {
None
}
#[cfg(test)]
mod tests {
use super::*;
/// A writer that refuses every write, like a closed terminal.
struct Closed;
impl Write for Closed {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::ErrorKind::BrokenPipe.into())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn a_line_goes_to_the_terminal_when_there_is_one() {
let mut screen = Vec::new();
let mut stderr = Vec::new();
assert_eq!(
deliver(Some(&mut screen), Some(&mut stderr), "[kache] foo: hit"),
Shown::Terminal
);
assert_eq!(screen, b"[kache] foo: hit\n");
assert!(
stderr.is_empty(),
"Cargo must not see a line the terminal took"
);
}
#[test]
fn without_a_terminal_a_requested_line_goes_to_stderr() {
let mut stderr = Vec::new();
assert_eq!(
deliver(None::<Vec<u8>>, Some(&mut stderr), "[kache] foo: hit"),
Shown::Stderr
);
assert_eq!(stderr, b"[kache] foo: hit\n");
}
#[test]
fn without_a_terminal_any_other_line_is_logged() {
assert_eq!(
deliver(None::<Vec<u8>>, None::<Vec<u8>>, "[kache] foo: hit"),
Shown::Log
);
}
#[test]
fn a_terminal_that_refuses_the_write_falls_back() {
let mut stderr = Vec::new();
assert_eq!(
deliver(Some(Closed), Some(&mut stderr), "[kache] foo: hit"),
Shown::Stderr
);
assert_eq!(
deliver(Some(Closed), None::<Vec<u8>>, "[kache] foo: hit"),
Shown::Log
);
assert_eq!(
deliver(None::<Vec<u8>>, Some(Closed), "[kache] foo: hit"),
Shown::Log
);
}
}