1#![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#[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#[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#[derive(Default)]
157pub struct RunLog {
158 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 pub fn stdout(&mut self) {
189 self.output = Some(Box::new(std::io::stdout()));
190 self.flush();
191 }
192
193 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 pub fn msgs(&self) -> &[RunLogMessage] {
208 &self.msgs
209 }
210
211 pub fn push(&mut self, msg: RunLogMessage) {
213 assert!(self.output.is_none());
214 self.msgs.push(msg);
215 }
216
217 #[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 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 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 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 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
275impl 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 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 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 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 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 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 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 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 pub fn npm_get_succeeded(&mut self, source: RunLogSource) {
417 self.write(&RunLogMessage::new(
418 source,
419 RunLogMessageDetail::NpmGetSucceded,
420 ));
421 }
422
423 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 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 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 pub fn plan_succeeded(&mut self, source: RunLogSource) {
463 self.write(&RunLogMessage::new(
464 source,
465 RunLogMessageDetail::PlanSucceeded,
466 ));
467 }
468
469 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 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 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 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 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 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#[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 } 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 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}