1use std::io::{self, Write};
6use std::sync::{Mutex, OnceLock};
7
8use crate::Outcome;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Cached {
13 Hit,
14 RemoteHit,
15 Miss,
16}
17
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub enum Event {
20 Planned {
22 tasks: usize,
23 },
24 Started {
26 id: String,
27 checking: bool,
28 },
29 StartedContinuous {
31 id: String,
32 },
33 Cache {
34 id: String,
35 cached: Cached,
36 },
37 Finished {
38 id: String,
39 outcome: Outcome,
40 },
41 Skipped {
43 id: String,
44 },
45 Stopping {
46 id: String,
47 },
48 Stopped {
49 id: String,
50 },
51}
52
53impl std::fmt::Display for Event {
54 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
56 match self {
57 Self::Planned { .. } => Ok(()),
58 Self::Started { id, checking: true } => write!(f, "qk: checking {id}"),
59 Self::Started {
60 id,
61 checking: false,
62 } => write!(f, "qk: running {id}"),
63 Self::StartedContinuous { id } => write!(f, "qk: started {id} (continuous)"),
64 Self::Cache {
65 id,
66 cached: Cached::Hit,
67 } => write!(f, "qk: cache hit {id}"),
68 Self::Cache {
69 id,
70 cached: Cached::RemoteHit,
71 } => write!(f, "qk: remote cache hit {id}"),
72 Self::Cache {
73 id,
74 cached: Cached::Miss,
75 } => write!(f, "qk: cache miss {id}"),
76 Self::Finished {
77 id,
78 outcome: Outcome::Success,
79 } => write!(f, "qk: finished {id}"),
80 Self::Finished {
81 id,
82 outcome: Outcome::Failed(code),
83 } => {
84 write!(f, "qk: failed {id} (exit {code})")
85 }
86 Self::Finished {
87 id,
88 outcome: Outcome::Cancelled,
89 } => write!(f, "qk: cancelled {id}"),
90 Self::Skipped { id } => write!(f, "qk: skipped {id} (dependency failed)"),
91 Self::Stopping { id } => write!(f, "qk: stopping {id} (no longer needed)"),
92 Self::Stopped { id } => write!(f, "qk: stopped {id}"),
93 }
94 }
95}
96
97pub trait Sink: Send + Sync {
98 fn event(&self, event: &Event);
99 fn warning(&self, line: &str);
101 fn output(&self, stderr: bool, bytes: &[u8]);
103 fn output_finished(&self) {}
105}
106
107impl<T: Sink + ?Sized> Sink for std::sync::Arc<T> {
108 fn event(&self, event: &Event) {
109 (**self).event(event);
110 }
111 fn warning(&self, line: &str) {
112 (**self).warning(line);
113 }
114 fn output(&self, stderr: bool, bytes: &[u8]) {
115 (**self).output(stderr, bytes);
116 }
117 fn output_finished(&self) {
118 (**self).output_finished();
119 }
120}
121
122pub struct Lines;
125
126impl Sink for Lines {
127 fn event(&self, event: &Event) {
128 let line = event.to_string();
129 if !line.is_empty() {
130 write_all(true, format!("{line}\n").as_bytes());
131 }
132 }
133
134 fn warning(&self, line: &str) {
135 write_all(true, format!("{line}\n").as_bytes());
136 }
137
138 fn output(&self, stderr: bool, bytes: &[u8]) {
139 write_all(stderr, bytes);
140 }
141}
142
143pub fn write_all(stderr: bool, bytes: &[u8]) {
144 let _ = if stderr {
145 io::stderr().lock().write_all(bytes)
146 } else {
147 io::stdout().lock().write_all(bytes)
148 };
149}
150
151static SINK: OnceLock<Box<dyn Sink>> = OnceLock::new();
152
153pub fn install(sink: Box<dyn Sink>) {
155 let _ = SINK.set(sink);
156}
157
158fn sink() -> &'static dyn Sink {
159 SINK.get().map_or(&Lines, |sink| sink.as_ref())
160}
161
162pub fn event(event: Event) {
163 let _guard = OUTPUT.lock().unwrap();
164 sink().event(&event);
165}
166
167pub fn warning(line: &str) {
168 let _guard = OUTPUT.lock().unwrap();
169 sink().warning(line);
170}
171
172static OUTPUT: Mutex<()> = Mutex::new(());
175
176pub(crate) fn output_batch<T>(consume: impl FnOnce(&mut dyn FnMut(bool, &[u8])) -> T) -> T {
177 let _guard = OUTPUT.lock().unwrap();
178 let result = consume(&mut |stderr, bytes| {
179 if !bytes.is_empty() {
180 sink().output(stderr, bytes);
181 }
182 });
183 sink().output_finished();
184 result
185}
186
187pub fn output(stderr: bool, bytes: &[u8]) {
188 output_batch(|output| output(stderr, bytes));
189}