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