1use std::sync::{
9 Arc,
10 atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
11};
12
13use time::OffsetDateTime;
14use tokio::sync::mpsc;
15
16use crate::types::RequestId;
17
18const RUN_BYTES: usize = 1024 * 1024;
21const LINE_BYTES: usize = 4096;
23const DROPPED: &str = "[more output than the Tool log keeps; the rest is not shown]";
25
26static NEXT_RUN: AtomicU64 = AtomicU64::new(1);
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub struct RunNo(u64);
32
33impl RunNo {
34 pub fn get(self) -> u64 {
35 self.0
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Stream {
41 Stdout,
42 Stderr,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum End {
48 Exited {
50 code: Option<i32>,
51 text: String,
52 },
53 Launched,
55 Done,
57 Cancelled,
58 TimedOut,
59 Failed(String),
61}
62
63impl End {
64 pub(crate) fn exited(status: &std::process::ExitStatus) -> Self {
65 Self::Exited {
66 code: match crate::platform::process_end(status) {
67 crate::platform::ProcessEnd::Exited(code) => Some(code),
68 crate::platform::ProcessEnd::Signaled(_) => None,
69 },
70 text: crate::platform::exit_description(status),
71 }
72 }
73
74 pub fn text(&self) -> String {
76 match self {
77 Self::Exited {
78 code: Some(code), ..
79 } => format!("■ exit {code}"),
80 Self::Exited { text, .. } => format!("■ {text}"),
81 Self::Launched => "■ launched".into(),
82 Self::Done => "■ done".into(),
83 Self::Cancelled => "■ cancelled".into(),
84 Self::TimedOut => "■ timed out".into(),
85 Self::Failed(error) => format!("■ error: {error}"),
86 }
87 }
88
89 pub fn error(&self) -> bool {
91 !matches!(
92 self,
93 Self::Exited { code: Some(0), .. } | Self::Launched | Self::Done
94 )
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum Kind {
100 Start {
101 at: OffsetDateTime,
102 operation: String,
103 artifact: String,
105 },
106 Line {
107 stream: Stream,
108 text: String,
109 },
110 End(End),
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct Event {
116 pub request: RequestId,
117 pub run: RunNo,
118 pub kind: Kind,
119}
120
121#[derive(Debug, Clone)]
123pub struct Sink(mpsc::UnboundedSender<Event>);
124
125pub type Events = mpsc::UnboundedReceiver<Event>;
127
128pub fn channel() -> (Sink, Events) {
129 let (sender, receiver) = mpsc::unbounded_channel();
130 (Sink(sender), receiver)
131}
132
133impl Sink {
134 pub fn start(&self, request: &RequestId, operation: &str, artifact: &str) -> Run {
136 let run = Run::new(Some((self.clone(), request.clone())));
137 run.shared.send(Kind::Start {
138 at: OffsetDateTime::now_utc(),
139 operation: operation.to_owned(),
140 artifact: artifact.to_owned(),
141 });
142 run
143 }
144}
145
146struct Shared {
147 watch: Option<(Sink, RequestId)>,
149 run: RunNo,
150 left: AtomicUsize,
152 dropped: AtomicBool,
153}
154
155impl Shared {
156 fn send(&self, kind: Kind) {
157 if let Some((sink, request)) = &self.watch {
158 let _ = sink.0.send(Event {
160 request: request.clone(),
161 run: self.run,
162 kind,
163 });
164 }
165 }
166
167 fn line(&self, stream: Stream, bytes: &[u8]) {
168 if !self.watched() {
169 return;
170 }
171 let text =
172 String::from_utf8(crate::runtime::clean_output(bytes)).expect("clean output is UTF-8");
173 let size = text.len().max(1);
174 let mut left = self.left.load(Ordering::Relaxed);
177 let kept = loop {
178 let Some(next) = left.checked_sub(size) else {
179 break false;
180 };
181 match self
182 .left
183 .compare_exchange_weak(left, next, Ordering::Relaxed, Ordering::Relaxed)
184 {
185 Ok(_) => break true,
186 Err(current) => left = current,
187 }
188 };
189 if kept {
190 self.send(Kind::Line { stream, text });
191 } else if !self.dropped.swap(true, Ordering::Relaxed) {
192 self.send(Kind::Line {
193 stream: Stream::Stderr,
194 text: DROPPED.into(),
195 });
196 }
197 }
198}
199
200pub struct Run {
203 shared: Arc<Shared>,
204 ended: bool,
205}
206
207impl std::fmt::Debug for Run {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 f.debug_struct("Run")
210 .field("run", &self.shared.run)
211 .finish()
212 }
213}
214
215impl Run {
216 fn new(watch: Option<(Sink, RequestId)>) -> Self {
217 Self {
218 shared: Arc::new(Shared {
219 watch,
220 run: RunNo(NEXT_RUN.fetch_add(1, Ordering::Relaxed)),
221 left: AtomicUsize::new(RUN_BYTES),
222 dropped: AtomicBool::new(false),
223 }),
224 ended: false,
225 }
226 }
227
228 pub fn unwatched() -> Self {
230 Self::new(None)
231 }
232
233 pub(crate) fn watched(&self) -> bool {
234 self.shared.watched()
235 }
236
237 pub(crate) fn lines(&self, stream: Stream) -> Lines {
239 Lines {
240 shared: self.shared.clone(),
241 stream,
242 pending: Vec::new(),
243 }
244 }
245
246 pub fn text(&self, stream: Stream, text: &str) {
248 let mut lines = self.lines(stream);
249 lines.push(text.as_bytes());
250 lines.finish();
251 }
252
253 pub fn end(mut self, end: End) {
254 self.ended = true;
255 self.shared.send(Kind::End(end));
256 }
257}
258
259impl Drop for Run {
260 fn drop(&mut self) {
261 if !self.ended {
262 self.shared.send(Kind::End(End::Cancelled));
263 }
264 }
265}
266
267pub(crate) struct Lines {
269 shared: Arc<Shared>,
270 stream: Stream,
271 pending: Vec<u8>,
272}
273
274impl Lines {
275 pub(crate) fn push(&mut self, bytes: &[u8]) {
276 if !self.shared.watched() {
277 return;
278 }
279 self.pending.extend_from_slice(bytes);
280 while let Some(newline) = self.pending.iter().position(|byte| *byte == b'\n') {
281 let line: Vec<u8> = self.pending.drain(..=newline).collect();
282 let line = &line[..newline];
283 self.shared
284 .line(self.stream, line.strip_suffix(b"\r").unwrap_or(line));
285 }
286 while self.pending.len() >= LINE_BYTES {
287 let end = piece(&self.pending);
288 let piece: Vec<u8> = self.pending.drain(..end).collect();
289 self.shared.line(self.stream, &piece);
290 }
291 }
292
293 pub(crate) fn finish(&mut self) {
295 if !self.pending.is_empty() {
296 let line = std::mem::take(&mut self.pending);
297 self.shared.line(self.stream, &line);
298 }
299 }
300}
301
302impl Shared {
303 fn watched(&self) -> bool {
304 self.watch.is_some()
305 }
306}
307
308fn piece(bytes: &[u8]) -> usize {
311 let end = LINE_BYTES.min(bytes.len());
312 match std::str::from_utf8(&bytes[..end]) {
313 Err(error) if error.error_len().is_none() && error.valid_up_to() > 0 => error.valid_up_to(),
314 _ => end,
315 }
316}
317
318#[derive(Debug, Clone, Default, PartialEq, Eq)]
321pub struct ToolLog {
322 entries: Vec<Entry>,
323}
324
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct Entry {
327 pub run: RunNo,
328 pub at: OffsetDateTime,
329 pub operation: String,
330 pub artifact: String,
331 pub lines: Vec<(Stream, String)>,
332 pub end: Option<End>,
333}
334
335impl Entry {
336 pub fn start_text(&self, offset: time::UtcOffset) -> String {
338 start_text(
339 self.at.to_offset(offset).time(),
340 &self.operation,
341 &self.artifact,
342 )
343 }
344}
345
346pub fn start_text(at: time::Time, operation: &str, artifact: &str) -> String {
348 format!(
349 "▶ {:02}:{:02}:{:02} · {operation} · {artifact}",
350 at.hour(),
351 at.minute(),
352 at.second()
353 )
354}
355
356impl ToolLog {
357 pub fn apply(&mut self, event: Event) {
360 match event.kind {
361 Kind::Start {
362 at,
363 operation,
364 artifact,
365 } => self.entries.push(Entry {
366 run: event.run,
367 at,
368 operation,
369 artifact,
370 lines: Vec::new(),
371 end: None,
372 }),
373 kind => {
374 let Some(entry) = self.entries.iter_mut().find(|entry| entry.run == event.run)
375 else {
376 return;
377 };
378 match kind {
379 Kind::Line { stream, text } => entry.lines.push((stream, text)),
380 Kind::End(end) => entry.end = Some(end),
381 Kind::Start { .. } => unreachable!("handled above"),
382 }
383 }
384 }
385 }
386
387 pub fn entries(&self) -> &[Entry] {
388 &self.entries
389 }
390
391 pub fn is_empty(&self) -> bool {
392 self.entries.is_empty()
393 }
394
395 pub fn clear(&mut self) {
396 self.entries.clear();
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 fn request() -> RequestId {
405 "req-1".parse().unwrap()
406 }
407
408 fn drain(events: &mut Events) -> Vec<Kind> {
409 std::iter::from_fn(|| events.try_recv().ok())
410 .map(|event| event.kind)
411 .collect()
412 }
413
414 fn line(stream: Stream, text: &str) -> Kind {
415 Kind::Line {
416 stream,
417 text: text.into(),
418 }
419 }
420
421 #[test]
422 fn output_arrives_as_lines_of_its_own_stream() {
423 let (sink, mut events) = channel();
424 let run = sink.start(&request(), "open", "notes");
425 let mut out = run.lines(Stream::Stdout);
426 let mut err = run.lines(Stream::Stderr);
427 out.push(b"one\r\ntw");
428 err.push(b"warn\n");
429 out.push(b"o\nthree");
430 out.finish();
431 err.finish();
432 run.end(End::Exited {
433 code: Some(0),
434 text: String::new(),
435 });
436 let kinds = drain(&mut events);
437 assert!(matches!(&kinds[0], Kind::Start { operation, artifact, .. }
438 if operation == "open" && artifact == "notes"));
439 assert_eq!(
440 kinds[1..],
441 [
442 line(Stream::Stdout, "one"),
443 line(Stream::Stderr, "warn"),
444 line(Stream::Stdout, "two"),
445 line(Stream::Stdout, "three"),
446 Kind::End(End::Exited {
447 code: Some(0),
448 text: String::new()
449 }),
450 ]
451 );
452 }
453
454 #[test]
455 fn a_line_without_newline_shows_in_pieces_that_keep_characters_whole() {
456 let (sink, mut events) = channel();
457 let run = sink.start(&request(), "dump", "app");
458 let mut out = run.lines(Stream::Stdout);
459 let mut text = "a".repeat(LINE_BYTES - 1);
460 text.push('é');
461 out.push(text.as_bytes());
462 let kinds = drain(&mut events);
463 assert_eq!(
464 kinds[1..],
465 [line(Stream::Stdout, &"a".repeat(LINE_BYTES - 1))]
466 );
467 out.finish();
468 assert_eq!(drain(&mut events), [line(Stream::Stdout, "é")]);
469 }
470
471 #[test]
472 fn a_run_shows_its_output_up_to_its_budget_then_says_the_rest_is_not_shown() {
473 let (sink, mut events) = channel();
474 let run = sink.start(&request(), "spew", "app");
475 let chunk = format!("{}\n", "x".repeat(LINE_BYTES - 1));
476 for _ in 0..(RUN_BYTES / (LINE_BYTES - 1) + 10) {
477 run.lines(Stream::Stdout).push(chunk.as_bytes());
478 }
479 let kinds = drain(&mut events);
480 let shown: usize = kinds
481 .iter()
482 .filter_map(|kind| match kind {
483 Kind::Line {
484 stream: Stream::Stdout,
485 text,
486 } => Some(text.len()),
487 _ => None,
488 })
489 .sum();
490 assert!(shown <= RUN_BYTES);
491 assert_eq!(kinds.last(), Some(&line(Stream::Stderr, DROPPED)));
492 assert_eq!(
493 kinds
494 .iter()
495 .filter(|kind| **kind == line(Stream::Stderr, DROPPED))
496 .count(),
497 1
498 );
499 }
500
501 #[test]
502 fn a_run_dropped_without_an_end_was_cancelled_and_a_closed_review_discards_lines() {
503 let (sink, mut events) = channel();
504 drop(sink.start(&request(), "slow", "app"));
505 assert_eq!(drain(&mut events).last(), Some(&Kind::End(End::Cancelled)));
506 let run = sink.start(&request(), "late", "app");
507 drop(events);
508 run.text(Stream::Stdout, "nobody reads this\n");
509 run.end(End::Done);
510 }
511
512 #[test]
513 fn the_log_groups_lines_under_their_run_and_ignores_runs_it_never_saw_start() {
514 let (sink, mut events) = channel();
515 let first = sink.start(&request(), "launch", "app");
516 let second = sink.start(&request(), "check", "lib");
517 second.text(Stream::Stdout, "checked\n");
518 second.end(End::Done);
519 first.text(Stream::Stderr, "app says\n");
520 let mut log = ToolLog::default();
521 for event in std::iter::from_fn(|| events.try_recv().ok()) {
522 log.apply(event);
523 }
524 let [launch, check] = log.entries() else {
525 panic!("two entries: {log:?}");
526 };
527 assert_eq!(launch.lines, [(Stream::Stderr, "app says".to_owned())]);
528 assert_eq!(launch.end, None);
529 assert_eq!(check.lines, [(Stream::Stdout, "checked".to_owned())]);
530 assert_eq!(check.end, Some(End::Done));
531 log.clear();
532 first.end(End::Done);
533 log.apply(events.try_recv().unwrap());
534 assert!(log.is_empty());
535 }
536
537 #[test]
538 fn end_lines_say_how_a_run_ended() {
539 let exit = |code| End::Exited {
540 code: Some(code),
541 text: String::new(),
542 };
543 assert_eq!(exit(0).text(), "■ exit 0");
544 assert!(!exit(0).error());
545 assert_eq!(exit(2).text(), "■ exit 2");
546 assert!(exit(2).error());
547 assert_eq!(End::Launched.text(), "■ launched");
548 assert_eq!(End::TimedOut.text(), "■ timed out");
549 assert_eq!(End::Failed("no".into()).text(), "■ error: no");
550 assert!(End::Cancelled.error());
551 assert_eq!(
552 start_text(time::Time::from_hms(9, 5, 7).unwrap(), "open", "notes"),
553 "▶ 09:05:07 · open · notes"
554 );
555 }
556}