Skip to main content

ambient_ci/
runlog.rs

1//! Run logs for Ambient.
2
3#![allow(dead_code)]
4#![allow(missing_docs)]
5
6use std::{
7    ffi::{OsStr, OsString},
8    fs::OpenOptions,
9    path::{Path, PathBuf},
10    process::{Command, Output},
11    time::{Duration, SystemTime},
12};
13
14use clingwrap::runner::CommandError;
15use html_page::{Element, HtmlPage, Tag};
16use serde::{Deserialize, Serialize};
17
18use crate::{
19    action::RunnableAction,
20    action_impl::{debian::repo::Needed, Custom, NpmError},
21    config::Config,
22    plan::RunnablePlan,
23    util::format_timestamp,
24};
25
26const CSS: &str = include_str!("style.css");
27
28// A run log message without metadata.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(tag = "type", rename_all = "snake_case")]
31enum RunLogMessageDetail {
32    AmbientStarts {
33        name: String,
34        version: String,
35    },
36    AmbientEndsSuccssfully,
37    AmbientEndsInFailure,
38    AmbientRuntimeConfig(Config),
39    ExecutorStarts {
40        name: String,
41        version: String,
42    },
43    ExecutorEndsSuccessfully,
44    ExecutorEndsInFailure {
45        exit_code: i32,
46    },
47    RunnablePlan(RunnablePlan),
48    RunCi {
49        project_name: String,
50    },
51    SkipCi {
52        project_name: String,
53    },
54    Debug {
55        debug: String,
56    },
57    ExecuteAction(RunnableAction),
58    ActionSucceeded(RunnableAction),
59    ActionFailed(RunnableAction),
60    DebGet {
61        packages: Vec<Needed>,
62    },
63    NpmGetSucceded,
64    NpmGetFailed1 {
65        error: String,
66    },
67    CustomActionStarts {
68        source: PathBuf,
69        custom: Custom,
70        exe: PathBuf,
71        exe_exists: bool,
72    },
73    CustomActionOutput {
74        stdout: Vec<u8>,
75        stderr: Vec<u8>,
76    },
77    PlanSucceeded,
78    StartProgram {
79        argv: Vec<OsString>,
80    },
81    ProgramSucceeded {
82        exit_code: i32,
83        stdout: String,
84        stderr: String,
85    },
86    ProgramFailed {
87        exit_code: Option<i32>,
88        stdout: String,
89        stderr: String,
90    },
91    StartQemu {
92        argv: Vec<OsString>,
93    },
94    QemuSucceeded {
95        exit_code: i32,
96        stdout: String,
97        stderr: String,
98    },
99    QemuFailed {
100        exit_code: Option<i32>,
101        stdout: String,
102        stderr: String,
103    },
104}
105
106impl RunLogMessageDetail {
107    fn was_successful(&self) -> bool {
108        !matches!(
109            self,
110            Self::ProgramFailed { .. }
111                | Self::ActionFailed(_)
112                | Self::NpmGetFailed1 { .. }
113                | Self::AmbientEndsInFailure
114                | Self::QemuFailed { .. }
115        )
116    }
117}
118
119/// A run log message with metadata.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct RunLogMessage {
122    #[serde(flatten)]
123    detail: RunLogMessageDetail,
124    timestamp: SystemTime,
125    log_source: RunLogSource,
126}
127
128#[allow(missing_docs)]
129impl RunLogMessage {
130    fn new(source: RunLogSource, detail: RunLogMessageDetail) -> Self {
131        Self {
132            detail,
133            timestamp: SystemTime::now(),
134            log_source: source,
135        }
136    }
137
138    fn format_timestamp(&self) -> String {
139        format_timestamp(self.timestamp).unwrap_or("time stamp error".into())
140    }
141
142    pub fn to_json(&self) -> String {
143        match serde_json::to_string(self) {
144            Ok(json) => json,
145            Err(err) => {
146                format!(r#"{{"error":"failed to convert RunLogMessage to JSON: {err}""#)
147            }
148        }
149    }
150}
151
152/// All the log messages for a CI run.
153///
154/// The default version collects messages, but doesn't write them. If the output
155/// is set later, the collected messages get written there.
156#[derive(Default)]
157pub struct RunLog {
158    // We collect messages here via the `push` and `write` methods.
159    msgs: Vec<RunLogMessage>,
160
161    output: Option<Box<dyn std::io::Write>>,
162}
163
164impl std::fmt::Debug for RunLog {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        write!(f, "RunLog.msgs={:?}", self.msgs)
167    }
168}
169
170impl RunLog {
171    fn flush(&mut self) {
172        if !self.msgs.is_empty() {
173            while !self.msgs.is_empty() {
174                let msg = self.msgs.remove(0);
175                self.write_to_output(&msg);
176            }
177        }
178    }
179
180    fn write_to_output(&mut self, msg: &RunLogMessage) {
181        if let Some(output) = &mut self.output {
182            let buf = format!("{}\n", msg.to_json());
183            output.write_all(buf.as_bytes()).ok();
184        }
185    }
186
187    /// Write messages to stdout. Flush any collected messages.
188    pub fn stdout(&mut self) {
189        self.output = Some(Box::new(std::io::stdout()));
190        self.flush();
191    }
192
193    /// Change `RunLog` so that further log messages are written to a
194    /// named file.
195    pub fn to_named_file(&mut self, filename: impl AsRef<Path>) -> Result<(), RunLogError> {
196        let filename = filename.as_ref();
197        let file = OpenOptions::new()
198            .append(true)
199            .open(filename)
200            .map_err(|err| RunLogError::Create(filename.to_path_buf(), err))?;
201        self.output = Some(Box::new(file));
202        self.flush();
203        Ok(())
204    }
205
206    /// All messages.
207    pub fn msgs(&self) -> &[RunLogMessage] {
208        &self.msgs
209    }
210
211    /// Append a message to the run log.
212    pub fn push(&mut self, msg: RunLogMessage) {
213        assert!(self.output.is_none());
214        self.msgs.push(msg);
215    }
216
217    /// Output a message to stderr.
218    #[allow(clippy::unwrap_used)]
219    pub fn write(&mut self, msg: &RunLogMessage) {
220        if self.output.is_none() {
221            self.push(msg.clone());
222        } else {
223            self.write_to_output(msg);
224        }
225    }
226
227    /// Load run log file a reader. This reads the output from the "run log"
228    /// serial port of QEMU and extracts the JSON lines from that for parsing.
229    pub fn read_raw(mut reader: impl std::io::Read) -> Result<Self, RunLogError> {
230        let mut buf = vec![];
231        reader.read_to_end(&mut buf).map_err(RunLogError::ReadLog)?;
232        Self::from_raw(buf)
233    }
234
235    /// Load run log from in-memory raw data. This parses the output to extract
236    /// JSON Lihes.
237    pub fn from_raw(buf: Vec<u8>) -> Result<Self, RunLogError> {
238        let buf = String::from_utf8(buf).map_err(RunLogError::Utf8)?;
239
240        const BEGIN: &str = "====================== BEGIN ======================";
241        if let Some((_, suffix)) = buf.split_once(BEGIN) {
242            Self::from_lines(suffix.lines().filter(|line| line.starts_with("{")))
243        } else {
244            Err(RunLogError::NoBegin)
245        }
246    }
247
248    /// Load run log file a reader. This reads pure JSON Lines run log.
249    pub fn read_jsonl(mut reader: impl std::io::Read) -> Result<Self, RunLogError> {
250        let mut buf = vec![];
251        reader.read_to_end(&mut buf).map_err(RunLogError::ReadLog)?;
252        Self::parse_jsonl(buf)
253    }
254
255    /// Parse JSON run log from memory.
256    pub fn parse_jsonl(data: Vec<u8>) -> Result<Self, RunLogError> {
257        let buf = String::from_utf8(data).map_err(RunLogError::Utf8)?;
258        Self::from_lines(buf.lines())
259    }
260
261    fn from_lines<'a>(lines: impl Iterator<Item = &'a str>) -> Result<Self, RunLogError> {
262        let mut runlog = Self::default();
263        for line in lines {
264            let msg: RunLogMessage = serde_json::from_str(line).map_err(|err| {
265                let prefix = line.split_at_checked(40).map(|(a, _)| a).unwrap_or("");
266                RunLogError::Json(prefix.to_string(), err)
267            })?;
268            runlog.push(msg);
269        }
270
271        Ok(runlog)
272    }
273}
274
275// Methods to create and write run log messages.
276impl RunLog {
277    pub fn debug(&mut self, source: RunLogSource, msg: impl Into<String>) {
278        self.write(&RunLogMessage::new(
279            source,
280            RunLogMessageDetail::Debug { debug: msg.into() },
281        ));
282    }
283
284    pub fn ambient_starts<N: Into<String>, V: Into<String>>(
285        &mut self,
286        source: RunLogSource,
287        name: N,
288        version: V,
289    ) {
290        self.write(&RunLogMessage::new(
291            source,
292            RunLogMessageDetail::AmbientStarts {
293                name: name.into(),
294                version: version.into(),
295            },
296        ))
297    }
298
299    pub fn ambient_ends_successfully(&mut self, source: RunLogSource) {
300        self.write(&RunLogMessage::new(
301            source,
302            RunLogMessageDetail::AmbientEndsSuccssfully,
303        ))
304    }
305
306    pub fn ambient_ends_in_failure(&mut self, source: RunLogSource) {
307        self.write(&RunLogMessage::new(
308            source,
309            RunLogMessageDetail::AmbientEndsInFailure,
310        ))
311    }
312
313    pub fn ambient_runtime_config(&mut self, source: RunLogSource, config: &Config) {
314        self.write(&RunLogMessage::new(
315            source,
316            RunLogMessageDetail::AmbientRuntimeConfig(config.clone()),
317        ))
318    }
319
320    pub fn run_ci(&mut self, source: RunLogSource, project_name: impl Into<String>) {
321        let project_name = project_name.into();
322
323        // We output this to stderr for the test suite.
324        eprintln!("run CI for {project_name}");
325
326        self.write(&RunLogMessage::new(
327            source,
328            RunLogMessageDetail::RunCi { project_name },
329        ))
330    }
331
332    pub fn skip_ci(&mut self, source: RunLogSource, project_name: impl Into<String>) {
333        let project_name = project_name.into();
334
335        // We output this to stderr for the test suite.
336        eprintln!("skip CI for {project_name}");
337
338        self.write(&RunLogMessage::new(
339            source,
340            RunLogMessageDetail::SkipCi { project_name },
341        ))
342    }
343
344    pub fn executor_starts(
345        &mut self,
346        source: RunLogSource,
347        name: impl Into<String>,
348        version: impl Into<String>,
349    ) {
350        self.write(&RunLogMessage::new(
351            source,
352            RunLogMessageDetail::ExecutorStarts {
353                name: name.into(),
354                version: version.into(),
355            },
356        ))
357    }
358
359    pub fn executor_ends_successfully(&mut self, source: RunLogSource) {
360        self.write(&RunLogMessage::new(
361            source,
362            RunLogMessageDetail::ExecutorEndsSuccessfully,
363        ))
364    }
365
366    pub fn executor_ends_in_failure(&mut self, source: RunLogSource, exit_code: i32) {
367        self.write(&RunLogMessage::new(
368            source,
369            RunLogMessageDetail::ExecutorEndsInFailure { exit_code },
370        ))
371    }
372
373    /// Log runnable plan at start of execution.
374    pub fn runnable_plan(&mut self, source: RunLogSource, plan: &RunnablePlan) {
375        self.write(&RunLogMessage::new(
376            source,
377            RunLogMessageDetail::RunnablePlan(plan.clone()),
378        ))
379    }
380
381    /// Execute an action.
382    pub fn execute_action(&mut self, source: RunLogSource, action: &RunnableAction) {
383        self.write(&RunLogMessage::new(
384            source,
385            RunLogMessageDetail::ExecuteAction(action.clone()),
386        ));
387    }
388
389    /// Action succeded.
390    pub fn action_succeeded(&mut self, source: RunLogSource, action: &RunnableAction) {
391        self.write(&RunLogMessage::new(
392            source,
393            RunLogMessageDetail::ActionSucceeded(action.clone()),
394        ));
395    }
396
397    /// Action failed.
398    pub fn action_failed(&mut self, source: RunLogSource, action: &RunnableAction) {
399        self.write(&RunLogMessage::new(
400            source,
401            RunLogMessageDetail::ActionFailed(action.clone()),
402        ));
403    }
404
405    // `deb_get` action succeeded.
406    pub fn deb_get(&mut self, source: RunLogSource, packages: &[Needed]) {
407        self.write(&RunLogMessage::new(
408            source,
409            RunLogMessageDetail::DebGet {
410                packages: packages.to_vec(),
411            },
412        ));
413    }
414
415    /// `npm_get` action succeeded.
416    pub fn npm_get_succeeded(&mut self, source: RunLogSource) {
417        self.write(&RunLogMessage::new(
418            source,
419            RunLogMessageDetail::NpmGetSucceded,
420        ));
421    }
422
423    /// `npm_get` action failed.
424    pub fn npm_get_failed(&mut self, log_source: RunLogSource, err: &NpmError) {
425        self.write(&RunLogMessage::new(
426            log_source,
427            RunLogMessageDetail::NpmGetFailed1 {
428                error: err.to_string(),
429            },
430        ));
431    }
432
433    /// Custom action starts.
434    pub fn custom_action_starts(
435        &mut self,
436        log_source: RunLogSource,
437        source: PathBuf,
438        custom: Custom,
439        exe: PathBuf,
440        exe_exists: bool,
441    ) {
442        self.write(&RunLogMessage::new(
443            log_source,
444            RunLogMessageDetail::CustomActionStarts {
445                source,
446                custom,
447                exe,
448                exe_exists,
449            },
450        ));
451    }
452
453    /// CUstom action succeeded.
454    pub fn custom_action_output(&mut self, source: RunLogSource, stdout: Vec<u8>, stderr: Vec<u8>) {
455        self.write(&RunLogMessage::new(
456            source,
457            RunLogMessageDetail::CustomActionOutput { stdout, stderr },
458        ));
459    }
460
461    /// All actions in plan succeded.
462    pub fn plan_succeeded(&mut self, source: RunLogSource) {
463        self.write(&RunLogMessage::new(
464            source,
465            RunLogMessageDetail::PlanSucceeded,
466        ));
467    }
468
469    /// Start a program.
470    pub fn start_program(&mut self, source: RunLogSource, cmd: &Command) {
471        fn oss(os: &OsStr) -> OsString {
472            os.to_os_string()
473        }
474
475        let mut argv = vec![oss(cmd.get_program())];
476        for arg in cmd.get_args() {
477            argv.push(oss(arg));
478        }
479
480        self.write(&RunLogMessage::new(
481            source,
482            RunLogMessageDetail::StartProgram { argv },
483        ));
484    }
485
486    /// Program succeeded.
487    pub fn program_succeeded(&mut self, source: RunLogSource, output: &Output) {
488        self.write(&RunLogMessage::new(
489            source,
490            RunLogMessageDetail::ProgramSucceeded {
491                #[allow(clippy::unwrap_used)]
492                exit_code: output.status.code().unwrap(),
493                stdout: String::from_utf8_lossy(&output.stdout).to_string(),
494                stderr: String::from_utf8_lossy(&output.stderr).to_string(),
495            },
496        ));
497    }
498
499    /// Program failed.
500    pub fn program_failed(&mut self, source: RunLogSource, err: &CommandError) {
501        let (exit_code, stdout, stderr) = match err {
502            CommandError::CommandFailed {
503                exit_code, output, ..
504            } => {
505                let stdout = String::from_utf8_lossy(&output.stdout).to_string();
506                let stderr = String::from_utf8_lossy(&output.stderr).to_string();
507                (Some(exit_code), stdout, stderr)
508            }
509            CommandError::KilledBySignal { .. }
510            | CommandError::NoSuchCommand(_)
511            | CommandError::NoPermission(_)
512            | CommandError::Other { .. }
513            | CommandError::Stdin(_)
514            | CommandError::PipeCapture(_)
515            | CommandError::PipeClone(_)
516            | CommandError::ReadCombined(_) => {
517                let stdout = String::new();
518                let stderr = err.to_string();
519                (None, stdout, stderr)
520            }
521        };
522        self.write(&RunLogMessage::new(
523            source,
524            RunLogMessageDetail::ProgramFailed {
525                exit_code: exit_code.copied(),
526                stdout,
527                stderr,
528            },
529        ));
530    }
531
532    /// Start a program.
533    pub fn start_qemu(&mut self, source: RunLogSource, cmd: &Command) {
534        fn oss(os: &OsStr) -> OsString {
535            os.to_os_string()
536        }
537
538        let mut argv = vec![oss(cmd.get_program())];
539        for arg in cmd.get_args() {
540            argv.push(oss(arg));
541        }
542
543        self.write(&RunLogMessage::new(
544            source,
545            RunLogMessageDetail::StartQemu { argv },
546        ));
547    }
548
549    /// Program succeeded.
550    pub fn qemu_succeeded(&mut self, source: RunLogSource, output: &Output) {
551        self.write(&RunLogMessage::new(
552            source,
553            RunLogMessageDetail::QemuSucceeded {
554                #[allow(clippy::unwrap_used)]
555                exit_code: output.status.code().unwrap(),
556                stdout: String::from_utf8_lossy(&output.stdout).to_string(),
557                stderr: String::from_utf8_lossy(&output.stderr).to_string(),
558            },
559        ));
560    }
561
562    /// Program failed.
563    pub fn qemu_failed(&mut self, source: RunLogSource, err: &CommandError) {
564        let (exit_code, stdout, stderr) = match err {
565            CommandError::CommandFailed {
566                exit_code, output, ..
567            } => {
568                let stdout = String::from_utf8_lossy(&output.stdout).to_string();
569                let stderr = String::from_utf8_lossy(&output.stderr).to_string();
570                (Some(exit_code), stdout, stderr)
571            }
572            CommandError::KilledBySignal { .. }
573            | CommandError::NoSuchCommand(_)
574            | CommandError::NoPermission(_)
575            | CommandError::Other { .. }
576            | CommandError::Stdin(_)
577            | CommandError::PipeCapture(_)
578            | CommandError::PipeClone(_)
579            | CommandError::ReadCombined(_) => {
580                let stdout = String::new();
581                let stderr = err.to_string();
582                (None, stdout, stderr)
583            }
584        };
585        self.write(&RunLogMessage::new(
586            source,
587            RunLogMessageDetail::QemuFailed {
588                exit_code: exit_code.copied(),
589                stdout,
590                stderr,
591            },
592        ));
593    }
594}
595
596/// Where does run log message come from?
597#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
598pub enum RunLogSource {
599    Prelude,
600    PrePlan,
601    Plan,
602    PostPlan,
603    Epilog,
604}
605
606impl RunLogSource {
607    fn class(&self) -> &'static str {
608        match self {
609            Self::Prelude => "prelude",
610            Self::PrePlan => "pre-plan",
611            Self::Plan => "plan",
612            Self::PostPlan => "post-plan",
613            Self::Epilog => "epilog",
614        }
615    }
616}
617
618#[derive(Debug)]
619struct Cursor<'a, T> {
620    data: &'a [T],
621}
622
623impl<'a, T> Cursor<'a, T> {
624    fn new(data: &'a [T]) -> Self {
625        Self { data }
626    }
627
628    fn is_empty(&self) -> bool {
629        self.data.is_empty()
630    }
631
632    fn skip(&mut self, n: usize) {
633        if n >= self.data.len() {
634            self.data = &[]
635        } else {
636            self.data = &self.data[n..];
637        }
638    }
639
640    fn peek(&self) -> Option<&T> {
641        self.data.first()
642    }
643
644    fn take_one(&mut self) -> Option<&T> {
645        let item = self.data.first();
646        self.skip(1);
647        item
648    }
649
650    fn take_if<F, U>(&mut self, func: F) -> Option<U>
651    where
652        F: Fn(&[T]) -> Option<(usize, Option<U>)>,
653    {
654        if let Some((n, maybe_item)) = func(self.data) {
655            self.skip(n);
656            maybe_item
657        } else {
658            None
659        }
660    }
661}
662
663pub struct SynthLog {
664    msgs: Vec<SynthMessage>,
665    raw: Vec<RunLogMessage>,
666    console: Option<Vec<u8>>,
667}
668
669impl SynthLog {
670    pub fn new(log_msgs: &[RunLogMessage]) -> Self {
671        fn log_messages(n: usize, data: &[RunLogMessage]) -> Vec<SynthMessage> {
672            data.iter()
673                .take(n)
674                .map(|m| SynthMessage::from_run_log_message(m, Duration::default()))
675                .collect()
676        }
677
678        fn successful_action(data: &[RunLogMessage]) -> Option<(usize, Option<SynthMessage>)> {
679            match &data {
680                &[RunLogMessage {
681                    detail: RunLogMessageDetail::ExecuteAction(action),
682                    timestamp,
683                    log_source: source,
684                }, RunLogMessage {
685                    detail: RunLogMessageDetail::ActionSucceeded(_),
686                    ..
687                }, ..] => {
688                    let n = 2;
689                    let msg = SynthMessage {
690                        detail: SynthDetail::SuccessfulAction {
691                            action: action.clone(),
692                            log_messages: log_messages(n, data),
693                        },
694                        timestamp: *timestamp,
695                        offset: Duration::default(),
696                        source: *source,
697                    };
698                    Some((n, Some(msg)))
699                }
700                _ => None,
701            }
702        }
703
704        fn successful_action_runs_program(
705            data: &[RunLogMessage],
706        ) -> Option<(usize, Option<SynthMessage>)> {
707            match &data {
708                &[RunLogMessage {
709                    detail: RunLogMessageDetail::ExecuteAction(action),
710                    timestamp,
711                    log_source: source,
712                }, RunLogMessage {
713                    detail: RunLogMessageDetail::StartProgram { .. },
714                    ..
715                }, RunLogMessage {
716                    detail: RunLogMessageDetail::ProgramSucceeded { .. },
717                    ..
718                }, RunLogMessage {
719                    detail: RunLogMessageDetail::ActionSucceeded(_),
720                    ..
721                }, ..] => {
722                    let n = 4;
723                    let msg = SynthMessage {
724                        detail: SynthDetail::SuccessfulAction {
725                            action: action.clone(),
726                            log_messages: log_messages(n, data),
727                        },
728                        timestamp: *timestamp,
729                        offset: Duration::default(),
730                        source: *source,
731                    };
732                    Some((n, Some(msg)))
733                }
734                _ => None,
735            }
736        }
737
738        fn successful_action_runs_four_programs(
739            data: &[RunLogMessage],
740        ) -> Option<(usize, Option<SynthMessage>)> {
741            match &data {
742                &[RunLogMessage {
743                    detail: RunLogMessageDetail::ExecuteAction(action),
744                    timestamp,
745                    log_source: source,
746                }, RunLogMessage {
747                    detail: RunLogMessageDetail::StartProgram { .. },
748                    ..
749                }, RunLogMessage {
750                    detail: RunLogMessageDetail::ProgramSucceeded { .. },
751                    ..
752                }, RunLogMessage {
753                    detail: RunLogMessageDetail::StartProgram { .. },
754                    ..
755                }, RunLogMessage {
756                    detail: RunLogMessageDetail::ProgramSucceeded { .. },
757                    ..
758                }, RunLogMessage {
759                    detail: RunLogMessageDetail::StartProgram { .. },
760                    ..
761                }, RunLogMessage {
762                    detail: RunLogMessageDetail::ProgramSucceeded { .. },
763                    ..
764                }, RunLogMessage {
765                    detail: RunLogMessageDetail::StartProgram { .. },
766                    ..
767                }, RunLogMessage {
768                    detail: RunLogMessageDetail::ProgramSucceeded { .. },
769                    ..
770                }, RunLogMessage {
771                    detail: RunLogMessageDetail::ActionSucceeded(_),
772                    ..
773                }, ..] => {
774                    let n = 10;
775                    let msg = SynthMessage {
776                        detail: SynthDetail::SuccessfulAction {
777                            action: action.clone(),
778                            log_messages: log_messages(n, data),
779                        },
780                        timestamp: *timestamp,
781                        offset: Duration::default(),
782                        source: *source,
783                    };
784                    Some((n, Some(msg)))
785                }
786                _ => None,
787            }
788        }
789
790        fn successful_custom_action(
791            data: &[RunLogMessage],
792        ) -> Option<(usize, Option<SynthMessage>)> {
793            match &data {
794                &[RunLogMessage {
795                    detail: RunLogMessageDetail::ExecuteAction(action),
796                    timestamp,
797                    log_source: source,
798                }, RunLogMessage {
799                    detail: RunLogMessageDetail::CustomActionStarts { .. },
800                    ..
801                }, RunLogMessage {
802                    detail: RunLogMessageDetail::CustomActionOutput { .. },
803                    ..
804                }, RunLogMessage {
805                    detail: RunLogMessageDetail::ActionSucceeded(_),
806                    ..
807                }, ..] => {
808                    let n = 4;
809                    let msg = SynthMessage {
810                        detail: SynthDetail::SuccessfulAction {
811                            action: action.clone(),
812                            log_messages: log_messages(n, data),
813                        },
814                        timestamp: *timestamp,
815                        offset: Duration::default(),
816                        source: *source,
817                    };
818                    Some((n, Some(msg)))
819                }
820                _ => None,
821            }
822        }
823
824        fn skip_uninteresting(data: &[RunLogMessage]) -> Option<(usize, Option<SynthMessage>)> {
825            let first = data.first()?;
826
827            #[allow(clippy::match_like_matches_macro)]
828            let skip = match &first.detail {
829                &RunLogMessageDetail::RunCi { .. }
830                | RunLogMessageDetail::ExecutorEndsSuccessfully
831                | RunLogMessageDetail::ExecutorEndsInFailure { .. } => true,
832                _ => false,
833            };
834
835            if skip {
836                Some((1, None))
837            } else {
838                None
839            }
840        }
841
842        fn just_convert(data: &[RunLogMessage]) -> Option<(usize, Option<SynthMessage>)> {
843            if let Some(orig) = data.first() {
844                let synth = match &orig.detail {
845                    RunLogMessageDetail::StartQemu { .. } => {
846                        SynthMessage::from_run_log_message(orig, Duration::default())
847                    }
848                    _ => return None,
849                };
850                Some((1, Some(synth)))
851            } else {
852                None
853            }
854        }
855
856        let mut msgs: Vec<SynthMessage> = vec![];
857
858        if let Some(first) = log_msgs.first() {
859            let mut cursor = Cursor::new(log_msgs);
860
861            while !cursor.is_empty() {
862                let offset: Duration = cursor
863                    .peek()
864                    .map(|m| {
865                        m.timestamp
866                            .duration_since(first.timestamp)
867                            .unwrap_or_default()
868                    })
869                    .unwrap_or_default();
870
871                if cursor.take_if(skip_uninteresting).is_some() {
872                    // Do nothing.
873                } else if let Some(mut msg) = cursor.take_if(just_convert) {
874                    msg.offset = offset;
875                    msgs.push(msg);
876                } else if let Some(mut msg) = cursor.take_if(successful_custom_action) {
877                    msg.offset = offset;
878                    msgs.push(msg);
879                } else if let Some(mut msg) = cursor.take_if(successful_action) {
880                    msg.offset = offset;
881                    msgs.push(msg);
882                } else if let Some(mut msg) = cursor.take_if(successful_action_runs_program) {
883                    msg.offset = offset;
884                    msgs.push(msg);
885                } else if let Some(mut msg) = cursor.take_if(successful_action_runs_four_programs) {
886                    msg.offset = offset;
887                    msgs.push(msg);
888                } else if let Some(log_msg) = cursor.take_one() {
889                    msgs.push(SynthMessage::from_run_log_message(log_msg, offset));
890                }
891            }
892        }
893
894        Self {
895            msgs,
896            raw: log_msgs.to_vec(),
897            console: None,
898        }
899    }
900
901    pub fn set_console_log(&mut self, console_log: impl Into<Vec<u8>>) {
902        self.console = Some(console_log.into());
903    }
904
905    pub fn to_html(&self) -> HtmlPage {
906        let title = "Ambient run log";
907        let mut page = HtmlPage::default()
908            .with_head_element(Element::new(Tag::Meta).with_attribute("charset", "utf-8"))
909            .with_head_element(Element::new(Tag::Title).with_text(title))
910            .with_head_element(Element::new(Tag::Style).with_text(CSS))
911            .with_head_element(
912                Element::new(Tag::Link)
913                    .with_attribute("href", "local.css")
914                    .with_attribute("rel", "stylesheet")
915                    .with_attribute("type", "text/css"),
916            )
917            .with_body_element(Element::new(Tag::H1).with_text(title));
918
919        page.push_to_body(self.to_html_messages());
920        page
921    }
922
923    pub fn to_html_messages(&self) -> Element {
924        let mut e = Element::new(Tag::Div);
925        let mut prev_source = None;
926        let mut first_failure = true;
927        for (i, msg) in self.msgs.iter().enumerate() {
928            if Some(msg.source) != prev_source {
929                let h = match msg.source {
930                    RunLogSource::Prelude => "Prelude, before pre-plan starts",
931                    RunLogSource::PrePlan => "Pre-plan, before VM starts",
932                    RunLogSource::Plan => "Plan, inside VM without network",
933                    RunLogSource::PostPlan => "Post-plan, after VM stops",
934                    RunLogSource::Epilog => "Epilog, aft3er post-plan is done",
935                };
936                e.push_child(Element::new(Tag::H2).with_text(h));
937                prev_source = Some(msg.source);
938            }
939            let child: Element = msg.into();
940            let id = if !msg.detail.is_successful() && first_failure {
941                first_failure = false;
942                "failed".to_string()
943            } else {
944                format!("msg{i}")
945            };
946            // child.set_attribute("id", &id);
947            e.push_child(
948                Element::new(Tag::Div).with_attribute("id", &id).with_child(
949                    Element::new(Tag::A)
950                        .with_attribute("href", format!("#{id}"))
951                        .with_class("anchor")
952                        .with_child(child),
953                ),
954            );
955        }
956
957        let mut raw = Element::new(Tag::Ol);
958        for m in self.raw.iter() {
959            raw.push_child(
960                Element::new(Tag::Li).with_child(
961                    Element::new(Tag::Pre)
962                        .with_text(serde_json::to_string_pretty(m).unwrap_or(format!("{m:#?}"))),
963                ),
964            );
965        }
966
967        e.push_child(
968            Element::new(Tag::H2)
969                .with_attribute("id", "json-log")
970                .with_child(
971                    Element::new(Tag::A)
972                        .with_attribute("href", "#json-log")
973                        .with_text("Raw log messages for Ambient troubleshooting"),
974                ),
975        );
976        e.push_child(
977            Element::new(Tag::Details)
978                .with_child(Element::new(Tag::Summary).with_text("Raw log messages"))
979                .with_child(Element::new(Tag::P).with_text(
980            "These raw log messages are meant to help Ambient developers figure out problems. You can ignore them.",
981        ))
982                .with_child(Element::new(Tag::Div).with_child(raw)),
983        );
984
985        if let Some(console_log) = &self.console {
986            e.push_child(
987                Element::new(Tag::H2)
988                    .with_attribute("id", "console-log")
989                    .with_child(
990                        Element::new(Tag::A)
991                            .with_attribute("href", "#console-log")
992                            .with_text("Raw console log from virtual machine, for troubleshoting"),
993                    ),
994            );
995            e.push_child(
996            Element::new(Tag::Details)
997                .with_child(Element::new(Tag::Summary).with_text("Raw log messages"))
998                .with_child(Element::new(Tag::P).with_text(
999            "This raw console log from the Ambient virtual machine is meant to help you and Ambient developers figure out problems. You can ignore this if everything works.",
1000        ))
1001                .with_child(Element::new(Tag::Pre).with_text(String::from_utf8_lossy(console_log).to_string())),
1002        );
1003        }
1004
1005        e
1006    }
1007}
1008
1009#[derive(Debug, Serialize)]
1010struct SynthMessage {
1011    #[serde(flatten)]
1012    detail: SynthDetail,
1013    timestamp: SystemTime,
1014    offset: Duration,
1015    source: RunLogSource,
1016}
1017
1018impl SynthMessage {
1019    fn from_run_log_message(log_msg: &RunLogMessage, offset: Duration) -> Self {
1020        if let Ok(detail) = SynthDetail::try_from(&log_msg.detail) {
1021            Self {
1022                detail,
1023                timestamp: log_msg.timestamp,
1024                offset,
1025                source: log_msg.log_source,
1026            }
1027        } else {
1028            Self {
1029                detail: SynthDetail::Other(log_msg.clone()),
1030                timestamp: log_msg.timestamp,
1031                offset,
1032                source: log_msg.log_source,
1033            }
1034        }
1035    }
1036}
1037
1038#[derive(Debug, Serialize)]
1039#[allow(clippy::large_enum_variant)]
1040enum SynthDetail {
1041    AmbientStarts {
1042        name: String,
1043        version: String,
1044    },
1045    AmbientEndsSuccssfully,
1046    AmbientEndsInFailure,
1047    AmbientRuntimeConfig(Config),
1048    StartQemu {
1049        argv: Vec<OsString>,
1050    },
1051    QemuSucceeded {
1052        exit_code: i32,
1053        stdout: String,
1054        stderr: String,
1055    },
1056    QemuFailed {
1057        exit_code: Option<i32>,
1058        stdout: String,
1059        stderr: String,
1060    },
1061    ExecutorStarts {
1062        name: String,
1063        version: String,
1064    },
1065    RunnablePlan(RunnablePlan),
1066    SuccessfulAction {
1067        action: RunnableAction,
1068        log_messages: Vec<SynthMessage>,
1069    },
1070    ExecuteAction(RunnableAction),
1071    ActionSucceeded(RunnableAction),
1072    ActionFailed(RunnableAction),
1073    DebGet {
1074        packages: Vec<Needed>,
1075    },
1076    NpmGetSucceded,
1077    NpmGetFailed2(String),
1078    SuccessfulCustomAction {
1079        custom: Custom,
1080        log_messages: Vec<SynthMessage>,
1081    },
1082    CustomActionStarts {
1083        source: PathBuf,
1084        custom: Custom,
1085        exe: PathBuf,
1086        exe_exists: bool,
1087    },
1088    CustomActionOutput {
1089        stdout: Vec<u8>,
1090        stderr: Vec<u8>,
1091    },
1092    StartProgram {
1093        argv: Vec<OsString>,
1094    },
1095    ProgramSucceeded {
1096        exit_code: i32,
1097        stdout: String,
1098        stderr: String,
1099    },
1100    ProgramFailed {
1101        exit_code: Option<i32>,
1102        stdout: String,
1103        stderr: String,
1104    },
1105    PlanSucceeded,
1106    Other(RunLogMessage),
1107}
1108
1109impl SynthDetail {
1110    fn is_successful(&self) -> bool {
1111        !matches!(
1112            self,
1113            Self::AmbientEndsInFailure
1114                | Self::QemuFailed { .. }
1115                | Self::ActionFailed(_)
1116                | Self::ProgramFailed { .. }
1117        )
1118    }
1119}
1120
1121impl TryFrom<&RunLogMessageDetail> for SynthDetail {
1122    type Error = ();
1123    fn try_from(log_detail: &RunLogMessageDetail) -> Result<Self, Self::Error> {
1124        let detail = match log_detail {
1125            RunLogMessageDetail::AmbientStarts { name, version } => SynthDetail::AmbientStarts {
1126                name: name.to_string(),
1127                version: version.to_string(),
1128            },
1129            RunLogMessageDetail::AmbientRuntimeConfig(config) => {
1130                SynthDetail::AmbientRuntimeConfig(config.clone())
1131            }
1132            RunLogMessageDetail::AmbientEndsSuccssfully => SynthDetail::AmbientEndsSuccssfully,
1133            RunLogMessageDetail::AmbientEndsInFailure => SynthDetail::AmbientEndsInFailure,
1134            RunLogMessageDetail::StartQemu { argv } => SynthDetail::StartQemu {
1135                argv: argv.to_vec(),
1136            },
1137            RunLogMessageDetail::QemuSucceeded {
1138                exit_code,
1139                stdout,
1140                stderr,
1141            } => SynthDetail::QemuSucceeded {
1142                exit_code: *exit_code,
1143                stdout: stdout.to_string(),
1144                stderr: stderr.to_string(),
1145            },
1146            RunLogMessageDetail::ExecutorStarts { name, version } => SynthDetail::ExecutorStarts {
1147                name: name.to_string(),
1148                version: version.to_string(),
1149            },
1150            RunLogMessageDetail::RunnablePlan(plan) => SynthDetail::RunnablePlan(plan.clone()),
1151            RunLogMessageDetail::PlanSucceeded => SynthDetail::PlanSucceeded,
1152            RunLogMessageDetail::ExecuteAction(action) => {
1153                SynthDetail::ExecuteAction(action.clone())
1154            }
1155            RunLogMessageDetail::ActionSucceeded(action) => {
1156                SynthDetail::ActionSucceeded(action.clone())
1157            }
1158            RunLogMessageDetail::ActionFailed(action) => SynthDetail::ActionFailed(action.clone()),
1159            RunLogMessageDetail::StartProgram { argv } => SynthDetail::StartProgram {
1160                argv: argv.to_vec(),
1161            },
1162            RunLogMessageDetail::ProgramSucceeded {
1163                exit_code,
1164                stdout,
1165                stderr,
1166            } => SynthDetail::ProgramSucceeded {
1167                exit_code: *exit_code,
1168                stdout: stdout.to_string(),
1169                stderr: stderr.to_string(),
1170            },
1171            RunLogMessageDetail::ProgramFailed {
1172                exit_code,
1173                stdout,
1174                stderr,
1175            } => SynthDetail::ProgramFailed {
1176                exit_code: *exit_code,
1177                stdout: stdout.to_string(),
1178                stderr: stderr.to_string(),
1179            },
1180            RunLogMessageDetail::DebGet { packages } => SynthDetail::DebGet {
1181                packages: packages.clone(),
1182            },
1183            RunLogMessageDetail::NpmGetSucceded => SynthDetail::NpmGetSucceded,
1184            RunLogMessageDetail::NpmGetFailed1 { error } => {
1185                SynthDetail::NpmGetFailed2(error.to_string())
1186            }
1187            RunLogMessageDetail::CustomActionStarts {
1188                source,
1189                custom,
1190                exe,
1191                exe_exists,
1192            } => SynthDetail::CustomActionStarts {
1193                source: source.into(),
1194                custom: custom.clone(),
1195                exe: exe.to_path_buf(),
1196                exe_exists: *exe_exists,
1197            },
1198            RunLogMessageDetail::CustomActionOutput { stdout, stderr } => {
1199                SynthDetail::CustomActionOutput {
1200                    stdout: stdout.clone(),
1201                    stderr: stderr.clone(),
1202                }
1203            }
1204            RunLogMessageDetail::RunCi { .. }
1205            | RunLogMessageDetail::SkipCi { .. }
1206            | RunLogMessageDetail::Debug { .. }
1207            | RunLogMessageDetail::QemuFailed { .. }
1208            | RunLogMessageDetail::ExecutorEndsSuccessfully
1209            | &RunLogMessageDetail::ExecutorEndsInFailure { .. } => return Err(()),
1210        };
1211        Ok(detail)
1212    }
1213}
1214
1215impl From<&SynthMessage> for Element {
1216    fn from(msg: &SynthMessage) -> Self {
1217        fn stream(label: &'static str, text: &str) -> Element {
1218            Element::new(Tag::Div)
1219                .with_text(label)
1220                .with_child(Element::new(Tag::Pre).with_text(text))
1221        }
1222
1223        fn program_result(exit_code: Option<i32>, stdout: &str, stderr: &str) -> Element {
1224            let mut e = Element::new(Tag::Div).with_text(format!(
1225                "Exit code: {}",
1226                exit_code
1227                    .map(|code| code.to_string())
1228                    .unwrap_or("Killed by signal".to_string())
1229            ));
1230
1231            if !stdout.is_empty() {
1232                e.push_child(stream("Stdout:", stdout));
1233            }
1234            if !stderr.is_empty() {
1235                e.push_child(stream("Stderr:", stderr));
1236            }
1237
1238            e
1239        }
1240
1241        let mut e = Element::new(Tag::Details).with_class(msg.source.class());
1242
1243        let mut summary = Element::new(Tag::Summary)
1244            .with_text(msg.source.class())
1245            .with_text(": ");
1246
1247        let mut more = Element::new(Tag::Div)
1248            .with_class("more")
1249            .with_child(
1250                Element::new(Tag::Span)
1251                    .with_class("duration")
1252                    .with_text("After ")
1253                    .with_child(
1254                        Element::new(Tag::Span)
1255                            .with_class("time-offset")
1256                            .with_text(format!("{:.02} seconds", msg.offset.as_secs_f64())),
1257                    )
1258                    .with_text(" "),
1259            )
1260            .with_child(
1261                Element::new(Tag::Span)
1262                    .with_class("timestamp")
1263                    .with_text(" at ")
1264                    .with_child(
1265                        Element::new(Tag::Span)
1266                            .with_class("exact-timestamp")
1267                            .with_text(
1268                                format_timestamp(msg.timestamp)
1269                                    .unwrap_or("broken timestamp".to_string()),
1270                            ),
1271                    ),
1272            );
1273
1274        match &msg.detail {
1275            SynthDetail::SuccessfulAction {
1276                action,
1277                log_messages,
1278            } => {
1279                summary.push_text("Successful action ");
1280                summary.push_child(
1281                    Element::new(Tag::Span)
1282                        .with_class("action-name")
1283                        .with_text(action.summary()),
1284                );
1285                let mut details = Element::new(Tag::Ul);
1286                for underlying in log_messages {
1287                    details.push_child(Element::new(Tag::Li).with_child(underlying.into()));
1288                }
1289                more.push_child(
1290                    Element::new(Tag::Div)
1291                        .with_class("action-details")
1292                        .with_child(details),
1293                );
1294            }
1295            SynthDetail::DebGet { packages } => {
1296                summary.push_text("Download deb packages");
1297                let mut list = Element::p();
1298                for needed in packages.iter() {
1299                    list.push_child(Element::new(Tag::Li).with_text(needed.package_name()));
1300                }
1301                more.push_child(Element::div().with_child(list));
1302            }
1303            SynthDetail::NpmGetSucceded => {
1304                summary.push_text("Download of npm packages succeeded");
1305            }
1306            SynthDetail::NpmGetFailed2(msg) => {
1307                summary.push_text("Download of npm packages failed");
1308                more.push_child(Element::new(Tag::Pre).with_text(msg));
1309            }
1310            SynthDetail::SuccessfulCustomAction {
1311                custom,
1312                log_messages,
1313            } => {
1314                summary.push_text("Successful action ");
1315                summary.push_child(
1316                    Element::new(Tag::Span)
1317                        .with_class("action-name")
1318                        .with_text("custom: ")
1319                        .with_text(&custom.name),
1320                );
1321                let mut details = Element::new(Tag::Ul);
1322                for underlying in log_messages {
1323                    details.push_child(Element::new(Tag::Li).with_child(underlying.into()));
1324                }
1325                more.push_child(
1326                    Element::new(Tag::Div)
1327                        .with_class("action-details")
1328                        .with_child(details),
1329                );
1330            }
1331            SynthDetail::AmbientStarts { name, version } => {
1332                summary.push_text("Ambient starts");
1333                more.push_child(
1334                    Element::new(Tag::P).with_child(
1335                        Element::new(Tag::Span).with_text("Program: ").with_child(
1336                            Element::new(Tag::Span)
1337                                .with_class("program-name")
1338                                .with_text(name),
1339                        ),
1340                    ),
1341                );
1342                more.push_child(
1343                    Element::new(Tag::P).with_child(
1344                        Element::new(Tag::Span).with_text("Version: ").with_child(
1345                            Element::new(Tag::Span)
1346                                .with_class("program-version")
1347                                .with_text(version),
1348                        ),
1349                    ),
1350                );
1351            }
1352            SynthDetail::AmbientEndsSuccssfully => {
1353                summary.push_text("Ambient ends, success");
1354                more.push_text("Everything is fine.");
1355            }
1356            SynthDetail::AmbientEndsInFailure => {
1357                summary.push_text("Ambient ends, failure");
1358                more.push_text("Woe be us!");
1359            }
1360            SynthDetail::AmbientRuntimeConfig(config) => {
1361                summary.push_text("Ambient configuration");
1362                let config = serde_norway::to_string(config)
1363                    .unwrap_or("Error serializing configuration to YAML".to_string());
1364                more.push_child(
1365                    Element::new(Tag::Pre)
1366                        .with_class("ambient-config")
1367                        .with_text(&config),
1368                );
1369            }
1370            SynthDetail::StartQemu { argv } => {
1371                summary.push_text("Start QEMU");
1372
1373                let mut args = Element::new(Tag::Ul).with_class("argv");
1374                for arg in argv.iter() {
1375                    args.push_child(
1376                        Element::new(Tag::Li).with_class("arg").with_child(
1377                            Element::new(Tag::Span)
1378                                .with_class("program-arg")
1379                                .with_text(String::from_utf8_lossy(arg.as_encoded_bytes())),
1380                        ),
1381                    );
1382                }
1383                more.push_child(args);
1384            }
1385            SynthDetail::QemuSucceeded {
1386                exit_code,
1387                stdout,
1388                stderr,
1389            } => {
1390                summary.push_text("QEMU succeeded");
1391                more.push_child(program_result(Some(*exit_code), stdout, stderr));
1392            }
1393            SynthDetail::QemuFailed {
1394                exit_code,
1395                stdout,
1396                stderr,
1397            } => {
1398                summary.push_text("QEMU failed");
1399                more.push_child(program_result(*exit_code, stdout, stderr));
1400            }
1401            SynthDetail::ExecutorStarts { name, version } => {
1402                summary.push_text("Executor starts");
1403                more.push_child(
1404                    Element::new(Tag::Span).with_text("Program: ").with_child(
1405                        Element::new(Tag::Span)
1406                            .with_class("program-name")
1407                            .with_text(name),
1408                    ),
1409                );
1410                more.push_child(Element::new(Tag::Br));
1411                more.push_child(
1412                    Element::new(Tag::Span).with_text("Version: ").with_child(
1413                        Element::new(Tag::Span)
1414                            .with_class("program-version")
1415                            .with_text(version),
1416                    ),
1417                );
1418            }
1419            SynthDetail::RunnablePlan(plan) => {
1420                summary.push_text("Runnable plan");
1421
1422                let yaml =
1423                    serde_norway::to_string(plan).unwrap_or("plan to YAML failed".to_string());
1424                more.push_child(
1425                    Element::new(Tag::Pre)
1426                        .with_class("runnable-plan")
1427                        .with_text(&yaml),
1428                );
1429            }
1430            SynthDetail::PlanSucceeded => {
1431                summary.push_text("Plan succeeded");
1432                more.push_text("Hopefully all is good.");
1433            }
1434            SynthDetail::ExecuteAction(action) => {
1435                summary.push_text("Start action ");
1436                summary.push_text(action.summary());
1437                more.push_child(Element::new(Tag::Pre).with_text(format!("{:#?}", action)));
1438            }
1439            SynthDetail::ActionSucceeded(action) => {
1440                summary.push_text("Action succeeded ");
1441                summary.push_text(action.summary());
1442                more.push_child(Element::new(Tag::Pre).with_text(format!("{:#?}", action)));
1443            }
1444            SynthDetail::ActionFailed(action) => {
1445                summary.push_text("Action failed: ");
1446                summary.push_text(action.summary());
1447                more.push_child(Element::new(Tag::Pre).with_text(format!("{:#?}", action)));
1448            }
1449            SynthDetail::CustomActionStarts { custom, .. } => {
1450                summary.push_text("Start action custom: ");
1451                summary.push_text(&custom.name);
1452                more.push_child(Element::new(Tag::Pre).with_text(format!("{:#?}", custom)));
1453            }
1454            SynthDetail::CustomActionOutput { stdout, stderr } => {
1455                summary.push_text("Custom action output");
1456                more.push_child(stream("Stdout:", &String::from_utf8_lossy(stdout)));
1457                more.push_child(stream("Stderr:", &String::from_utf8_lossy(stderr)));
1458            }
1459            SynthDetail::StartProgram { argv } => {
1460                summary.push_text("Start program ");
1461                summary.push_text(
1462                    argv.first()
1463                        .map(|s| String::from_utf8_lossy(s.as_encoded_bytes()).to_string())
1464                        .unwrap_or("unrepresentable string".to_string()),
1465                );
1466
1467                let mut args = Element::new(Tag::Ul).with_class("argv");
1468                for arg in argv.iter() {
1469                    args.push_child(
1470                        Element::new(Tag::Li).with_class("arg").with_child(
1471                            Element::new(Tag::Span)
1472                                .with_class("program-arg")
1473                                .with_text(String::from_utf8_lossy(arg.as_encoded_bytes())),
1474                        ),
1475                    );
1476                }
1477                more.push_child(args);
1478            }
1479            SynthDetail::ProgramSucceeded {
1480                exit_code,
1481                stdout,
1482                stderr,
1483            } => {
1484                summary.push_text("Program succeeded");
1485                more.push_child(program_result(Some(*exit_code), stdout, stderr));
1486            }
1487            SynthDetail::ProgramFailed {
1488                exit_code,
1489                stdout,
1490                stderr,
1491            } => {
1492                summary.push_text("ERROR: Program failed");
1493                more.push_child(program_result(*exit_code, stdout, stderr));
1494            }
1495            SynthDetail::Other(log_msg) => {
1496                summary.push_text(format!("{log_msg:?}"));
1497            }
1498        }
1499        if msg.detail.is_successful() {
1500            e.add_class("succeeded");
1501        } else {
1502            e.add_class("failed");
1503        }
1504        e.push_child(summary);
1505        e.push_child(more);
1506        e
1507    }
1508}
1509
1510#[derive(Debug, thiserror::Error)]
1511pub enum RunLogError {
1512    #[error("line in log is not JSON: {0:?}")]
1513    Json(String, #[source] serde_json::Error),
1514
1515    #[error("failed to read log file")]
1516    ReadLog(#[source] std::io::Error),
1517
1518    #[error("log file is not UTF8")]
1519    Utf8(#[source] std::string::FromUtf8Error),
1520
1521    #[error("run log does not contain BEGIN marker")]
1522    NoBegin,
1523
1524    #[error("failed to create file or open it for writing: {0}")]
1525    Create(PathBuf, #[source] std::io::Error),
1526}