#![allow(dead_code)]
#![allow(missing_docs)]
use std::{
ffi::{OsStr, OsString},
fs::OpenOptions,
path::{Path, PathBuf},
process::{Command, Output},
time::{Duration, SystemTime},
};
use clingwrap::runner::CommandError;
use html_page::{Element, HtmlPage, Tag};
use serde::{Deserialize, Serialize};
use crate::{
action::RunnableAction,
action_impl::{debian::repo::Needed, Custom, NpmError},
config::Config,
plan::RunnablePlan,
util::format_timestamp,
};
const CSS: &str = include_str!("style.css");
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum RunLogMessageDetail {
AmbientStarts {
name: String,
version: String,
},
AmbientEndsSuccssfully,
AmbientEndsInFailure,
AmbientRuntimeConfig(Config),
ExecutorStarts {
name: String,
version: String,
},
ExecutorEndsSuccessfully,
ExecutorEndsInFailure {
exit_code: i32,
},
RunnablePlan(RunnablePlan),
RunCi {
project_name: String,
},
SkipCi {
project_name: String,
},
Debug {
debug: String,
},
ExecuteAction(RunnableAction),
ActionSucceeded(RunnableAction),
ActionFailed(RunnableAction),
DebGet {
packages: Vec<Needed>,
},
NpmGetSucceded,
NpmGetFailed1 {
error: String,
},
CustomActionStarts {
source: PathBuf,
custom: Custom,
exe: PathBuf,
exe_exists: bool,
},
CustomActionOutput {
stdout: Vec<u8>,
stderr: Vec<u8>,
},
PlanSucceeded,
StartProgram {
argv: Vec<OsString>,
},
ProgramSucceeded {
exit_code: i32,
stdout: String,
stderr: String,
},
ProgramFailed {
exit_code: Option<i32>,
stdout: String,
stderr: String,
},
StartQemu {
argv: Vec<OsString>,
},
QemuSucceeded {
exit_code: i32,
stdout: String,
stderr: String,
},
QemuFailed {
exit_code: Option<i32>,
stdout: String,
stderr: String,
},
}
impl RunLogMessageDetail {
fn was_successful(&self) -> bool {
!matches!(
self,
Self::ProgramFailed { .. }
| Self::ActionFailed(_)
| Self::NpmGetFailed1 { .. }
| Self::AmbientEndsInFailure
| Self::QemuFailed { .. }
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunLogMessage {
#[serde(flatten)]
detail: RunLogMessageDetail,
timestamp: SystemTime,
log_source: RunLogSource,
}
#[allow(missing_docs)]
impl RunLogMessage {
fn new(source: RunLogSource, detail: RunLogMessageDetail) -> Self {
Self {
detail,
timestamp: SystemTime::now(),
log_source: source,
}
}
fn format_timestamp(&self) -> String {
format_timestamp(self.timestamp).unwrap_or("time stamp error".into())
}
pub fn to_json(&self) -> String {
match serde_json::to_string(self) {
Ok(json) => json,
Err(err) => {
format!(r#"{{"error":"failed to convert RunLogMessage to JSON: {err}""#)
}
}
}
}
#[derive(Default)]
pub struct RunLog {
msgs: Vec<RunLogMessage>,
output: Option<Box<dyn std::io::Write>>,
}
impl std::fmt::Debug for RunLog {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "RunLog.msgs={:?}", self.msgs)
}
}
impl RunLog {
fn flush(&mut self) {
if !self.msgs.is_empty() {
while !self.msgs.is_empty() {
let msg = self.msgs.remove(0);
self.write_to_output(&msg);
}
}
}
fn write_to_output(&mut self, msg: &RunLogMessage) {
if let Some(output) = &mut self.output {
let buf = format!("{}\n", msg.to_json());
output.write_all(buf.as_bytes()).ok();
}
}
pub fn stdout(&mut self) {
self.output = Some(Box::new(std::io::stdout()));
self.flush();
}
pub fn to_named_file(&mut self, filename: impl AsRef<Path>) -> Result<(), RunLogError> {
let filename = filename.as_ref();
let file = OpenOptions::new()
.append(true)
.open(filename)
.map_err(|err| RunLogError::Create(filename.to_path_buf(), err))?;
self.output = Some(Box::new(file));
self.flush();
Ok(())
}
pub fn msgs(&self) -> &[RunLogMessage] {
&self.msgs
}
pub fn push(&mut self, msg: RunLogMessage) {
assert!(self.output.is_none());
self.msgs.push(msg);
}
#[allow(clippy::unwrap_used)]
pub fn write(&mut self, msg: &RunLogMessage) {
if self.output.is_none() {
self.push(msg.clone());
} else {
self.write_to_output(msg);
}
}
pub fn read_raw(mut reader: impl std::io::Read) -> Result<Self, RunLogError> {
let mut buf = vec![];
reader.read_to_end(&mut buf).map_err(RunLogError::ReadLog)?;
Self::from_raw(buf)
}
pub fn from_raw(buf: Vec<u8>) -> Result<Self, RunLogError> {
let buf = String::from_utf8(buf).map_err(RunLogError::Utf8)?;
const BEGIN: &str = "====================== BEGIN ======================";
if let Some((_, suffix)) = buf.split_once(BEGIN) {
Self::from_lines(suffix.lines().filter(|line| line.starts_with("{")))
} else {
Err(RunLogError::NoBegin)
}
}
pub fn read_jsonl(mut reader: impl std::io::Read) -> Result<Self, RunLogError> {
let mut buf = vec![];
reader.read_to_end(&mut buf).map_err(RunLogError::ReadLog)?;
Self::parse_jsonl(buf)
}
pub fn parse_jsonl(data: Vec<u8>) -> Result<Self, RunLogError> {
let buf = String::from_utf8(data).map_err(RunLogError::Utf8)?;
Self::from_lines(buf.lines())
}
fn from_lines<'a>(lines: impl Iterator<Item = &'a str>) -> Result<Self, RunLogError> {
let mut runlog = Self::default();
for line in lines {
let msg: RunLogMessage = serde_json::from_str(line).map_err(|err| {
let prefix = line.split_at_checked(40).map(|(a, _)| a).unwrap_or("");
RunLogError::Json(prefix.to_string(), err)
})?;
runlog.push(msg);
}
Ok(runlog)
}
}
impl RunLog {
pub fn debug(&mut self, source: RunLogSource, msg: impl Into<String>) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::Debug { debug: msg.into() },
));
}
pub fn ambient_starts<N: Into<String>, V: Into<String>>(
&mut self,
source: RunLogSource,
name: N,
version: V,
) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::AmbientStarts {
name: name.into(),
version: version.into(),
},
))
}
pub fn ambient_ends_successfully(&mut self, source: RunLogSource) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::AmbientEndsSuccssfully,
))
}
pub fn ambient_ends_in_failure(&mut self, source: RunLogSource) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::AmbientEndsInFailure,
))
}
pub fn ambient_runtime_config(&mut self, source: RunLogSource, config: &Config) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::AmbientRuntimeConfig(config.clone()),
))
}
pub fn run_ci(&mut self, source: RunLogSource, project_name: impl Into<String>) {
let project_name = project_name.into();
eprintln!("run CI for {project_name}");
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::RunCi { project_name },
))
}
pub fn skip_ci(&mut self, source: RunLogSource, project_name: impl Into<String>) {
let project_name = project_name.into();
eprintln!("skip CI for {project_name}");
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::SkipCi { project_name },
))
}
pub fn executor_starts(
&mut self,
source: RunLogSource,
name: impl Into<String>,
version: impl Into<String>,
) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::ExecutorStarts {
name: name.into(),
version: version.into(),
},
))
}
pub fn executor_ends_successfully(&mut self, source: RunLogSource) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::ExecutorEndsSuccessfully,
))
}
pub fn executor_ends_in_failure(&mut self, source: RunLogSource, exit_code: i32) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::ExecutorEndsInFailure { exit_code },
))
}
pub fn runnable_plan(&mut self, source: RunLogSource, plan: &RunnablePlan) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::RunnablePlan(plan.clone()),
))
}
pub fn execute_action(&mut self, source: RunLogSource, action: &RunnableAction) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::ExecuteAction(action.clone()),
));
}
pub fn action_succeeded(&mut self, source: RunLogSource, action: &RunnableAction) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::ActionSucceeded(action.clone()),
));
}
pub fn action_failed(&mut self, source: RunLogSource, action: &RunnableAction) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::ActionFailed(action.clone()),
));
}
pub fn deb_get(&mut self, source: RunLogSource, packages: &[Needed]) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::DebGet {
packages: packages.to_vec(),
},
));
}
pub fn npm_get_succeeded(&mut self, source: RunLogSource) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::NpmGetSucceded,
));
}
pub fn npm_get_failed(&mut self, log_source: RunLogSource, err: &NpmError) {
self.write(&RunLogMessage::new(
log_source,
RunLogMessageDetail::NpmGetFailed1 {
error: err.to_string(),
},
));
}
pub fn custom_action_starts(
&mut self,
log_source: RunLogSource,
source: PathBuf,
custom: Custom,
exe: PathBuf,
exe_exists: bool,
) {
self.write(&RunLogMessage::new(
log_source,
RunLogMessageDetail::CustomActionStarts {
source,
custom,
exe,
exe_exists,
},
));
}
pub fn custom_action_output(&mut self, source: RunLogSource, stdout: Vec<u8>, stderr: Vec<u8>) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::CustomActionOutput { stdout, stderr },
));
}
pub fn plan_succeeded(&mut self, source: RunLogSource) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::PlanSucceeded,
));
}
pub fn start_program(&mut self, source: RunLogSource, cmd: &Command) {
fn oss(os: &OsStr) -> OsString {
os.to_os_string()
}
let mut argv = vec![oss(cmd.get_program())];
for arg in cmd.get_args() {
argv.push(oss(arg));
}
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::StartProgram { argv },
));
}
pub fn program_succeeded(&mut self, source: RunLogSource, output: &Output) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::ProgramSucceeded {
#[allow(clippy::unwrap_used)]
exit_code: output.status.code().unwrap(),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
},
));
}
pub fn program_failed(&mut self, source: RunLogSource, err: &CommandError) {
let (exit_code, stdout, stderr) = match err {
CommandError::CommandFailed {
exit_code, output, ..
} => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
(Some(exit_code), stdout, stderr)
}
CommandError::KilledBySignal { .. }
| CommandError::NoSuchCommand(_)
| CommandError::NoPermission(_)
| CommandError::Other { .. }
| CommandError::Stdin(_)
| CommandError::PipeCapture(_)
| CommandError::PipeClone(_)
| CommandError::ReadCombined(_) => {
let stdout = String::new();
let stderr = err.to_string();
(None, stdout, stderr)
}
};
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::ProgramFailed {
exit_code: exit_code.copied(),
stdout,
stderr,
},
));
}
pub fn start_qemu(&mut self, source: RunLogSource, cmd: &Command) {
fn oss(os: &OsStr) -> OsString {
os.to_os_string()
}
let mut argv = vec![oss(cmd.get_program())];
for arg in cmd.get_args() {
argv.push(oss(arg));
}
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::StartQemu { argv },
));
}
pub fn qemu_succeeded(&mut self, source: RunLogSource, output: &Output) {
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::QemuSucceeded {
#[allow(clippy::unwrap_used)]
exit_code: output.status.code().unwrap(),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
},
));
}
pub fn qemu_failed(&mut self, source: RunLogSource, err: &CommandError) {
let (exit_code, stdout, stderr) = match err {
CommandError::CommandFailed {
exit_code, output, ..
} => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
(Some(exit_code), stdout, stderr)
}
CommandError::KilledBySignal { .. }
| CommandError::NoSuchCommand(_)
| CommandError::NoPermission(_)
| CommandError::Other { .. }
| CommandError::Stdin(_)
| CommandError::PipeCapture(_)
| CommandError::PipeClone(_)
| CommandError::ReadCombined(_) => {
let stdout = String::new();
let stderr = err.to_string();
(None, stdout, stderr)
}
};
self.write(&RunLogMessage::new(
source,
RunLogMessageDetail::QemuFailed {
exit_code: exit_code.copied(),
stdout,
stderr,
},
));
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum RunLogSource {
Prelude,
PrePlan,
Plan,
PostPlan,
Epilog,
}
impl RunLogSource {
fn class(&self) -> &'static str {
match self {
Self::Prelude => "prelude",
Self::PrePlan => "pre-plan",
Self::Plan => "plan",
Self::PostPlan => "post-plan",
Self::Epilog => "epilog",
}
}
}
#[derive(Debug)]
struct Cursor<'a, T> {
data: &'a [T],
}
impl<'a, T> Cursor<'a, T> {
fn new(data: &'a [T]) -> Self {
Self { data }
}
fn is_empty(&self) -> bool {
self.data.is_empty()
}
fn skip(&mut self, n: usize) {
if n >= self.data.len() {
self.data = &[]
} else {
self.data = &self.data[n..];
}
}
fn peek(&self) -> Option<&T> {
self.data.first()
}
fn take_one(&mut self) -> Option<&T> {
let item = self.data.first();
self.skip(1);
item
}
fn take_if<F, U>(&mut self, func: F) -> Option<U>
where
F: Fn(&[T]) -> Option<(usize, Option<U>)>,
{
if let Some((n, maybe_item)) = func(self.data) {
self.skip(n);
maybe_item
} else {
None
}
}
}
pub struct SynthLog {
msgs: Vec<SynthMessage>,
raw: Vec<RunLogMessage>,
console: Option<Vec<u8>>,
}
impl SynthLog {
pub fn new(log_msgs: &[RunLogMessage]) -> Self {
fn log_messages(n: usize, data: &[RunLogMessage]) -> Vec<SynthMessage> {
data.iter()
.take(n)
.map(|m| SynthMessage::from_run_log_message(m, Duration::default()))
.collect()
}
fn successful_action(data: &[RunLogMessage]) -> Option<(usize, Option<SynthMessage>)> {
match &data {
&[RunLogMessage {
detail: RunLogMessageDetail::ExecuteAction(action),
timestamp,
log_source: source,
}, RunLogMessage {
detail: RunLogMessageDetail::ActionSucceeded(_),
..
}, ..] => {
let n = 2;
let msg = SynthMessage {
detail: SynthDetail::SuccessfulAction {
action: action.clone(),
log_messages: log_messages(n, data),
},
timestamp: *timestamp,
offset: Duration::default(),
source: *source,
};
Some((n, Some(msg)))
}
_ => None,
}
}
fn successful_action_runs_program(
data: &[RunLogMessage],
) -> Option<(usize, Option<SynthMessage>)> {
match &data {
&[RunLogMessage {
detail: RunLogMessageDetail::ExecuteAction(action),
timestamp,
log_source: source,
}, RunLogMessage {
detail: RunLogMessageDetail::StartProgram { .. },
..
}, RunLogMessage {
detail: RunLogMessageDetail::ProgramSucceeded { .. },
..
}, RunLogMessage {
detail: RunLogMessageDetail::ActionSucceeded(_),
..
}, ..] => {
let n = 4;
let msg = SynthMessage {
detail: SynthDetail::SuccessfulAction {
action: action.clone(),
log_messages: log_messages(n, data),
},
timestamp: *timestamp,
offset: Duration::default(),
source: *source,
};
Some((n, Some(msg)))
}
_ => None,
}
}
fn successful_action_runs_four_programs(
data: &[RunLogMessage],
) -> Option<(usize, Option<SynthMessage>)> {
match &data {
&[RunLogMessage {
detail: RunLogMessageDetail::ExecuteAction(action),
timestamp,
log_source: source,
}, RunLogMessage {
detail: RunLogMessageDetail::StartProgram { .. },
..
}, RunLogMessage {
detail: RunLogMessageDetail::ProgramSucceeded { .. },
..
}, RunLogMessage {
detail: RunLogMessageDetail::StartProgram { .. },
..
}, RunLogMessage {
detail: RunLogMessageDetail::ProgramSucceeded { .. },
..
}, RunLogMessage {
detail: RunLogMessageDetail::StartProgram { .. },
..
}, RunLogMessage {
detail: RunLogMessageDetail::ProgramSucceeded { .. },
..
}, RunLogMessage {
detail: RunLogMessageDetail::StartProgram { .. },
..
}, RunLogMessage {
detail: RunLogMessageDetail::ProgramSucceeded { .. },
..
}, RunLogMessage {
detail: RunLogMessageDetail::ActionSucceeded(_),
..
}, ..] => {
let n = 10;
let msg = SynthMessage {
detail: SynthDetail::SuccessfulAction {
action: action.clone(),
log_messages: log_messages(n, data),
},
timestamp: *timestamp,
offset: Duration::default(),
source: *source,
};
Some((n, Some(msg)))
}
_ => None,
}
}
fn successful_custom_action(
data: &[RunLogMessage],
) -> Option<(usize, Option<SynthMessage>)> {
match &data {
&[RunLogMessage {
detail: RunLogMessageDetail::ExecuteAction(action),
timestamp,
log_source: source,
}, RunLogMessage {
detail: RunLogMessageDetail::CustomActionStarts { .. },
..
}, RunLogMessage {
detail: RunLogMessageDetail::CustomActionOutput { .. },
..
}, RunLogMessage {
detail: RunLogMessageDetail::ActionSucceeded(_),
..
}, ..] => {
let n = 4;
let msg = SynthMessage {
detail: SynthDetail::SuccessfulAction {
action: action.clone(),
log_messages: log_messages(n, data),
},
timestamp: *timestamp,
offset: Duration::default(),
source: *source,
};
Some((n, Some(msg)))
}
_ => None,
}
}
fn skip_uninteresting(data: &[RunLogMessage]) -> Option<(usize, Option<SynthMessage>)> {
let first = data.first()?;
#[allow(clippy::match_like_matches_macro)]
let skip = match &first.detail {
&RunLogMessageDetail::RunCi { .. }
| RunLogMessageDetail::ExecutorEndsSuccessfully
| RunLogMessageDetail::ExecutorEndsInFailure { .. } => true,
_ => false,
};
if skip {
Some((1, None))
} else {
None
}
}
fn just_convert(data: &[RunLogMessage]) -> Option<(usize, Option<SynthMessage>)> {
if let Some(orig) = data.first() {
let synth = match &orig.detail {
RunLogMessageDetail::StartQemu { .. } => {
SynthMessage::from_run_log_message(orig, Duration::default())
}
_ => return None,
};
Some((1, Some(synth)))
} else {
None
}
}
let mut msgs: Vec<SynthMessage> = vec![];
if let Some(first) = log_msgs.first() {
let mut cursor = Cursor::new(log_msgs);
while !cursor.is_empty() {
let offset: Duration = cursor
.peek()
.map(|m| {
m.timestamp
.duration_since(first.timestamp)
.unwrap_or_default()
})
.unwrap_or_default();
if cursor.take_if(skip_uninteresting).is_some() {
} else if let Some(mut msg) = cursor.take_if(just_convert) {
msg.offset = offset;
msgs.push(msg);
} else if let Some(mut msg) = cursor.take_if(successful_custom_action) {
msg.offset = offset;
msgs.push(msg);
} else if let Some(mut msg) = cursor.take_if(successful_action) {
msg.offset = offset;
msgs.push(msg);
} else if let Some(mut msg) = cursor.take_if(successful_action_runs_program) {
msg.offset = offset;
msgs.push(msg);
} else if let Some(mut msg) = cursor.take_if(successful_action_runs_four_programs) {
msg.offset = offset;
msgs.push(msg);
} else if let Some(log_msg) = cursor.take_one() {
msgs.push(SynthMessage::from_run_log_message(log_msg, offset));
}
}
}
Self {
msgs,
raw: log_msgs.to_vec(),
console: None,
}
}
pub fn set_console_log(&mut self, console_log: impl Into<Vec<u8>>) {
self.console = Some(console_log.into());
}
pub fn to_html(&self) -> HtmlPage {
let title = "Ambient run log";
let mut page = HtmlPage::default()
.with_head_element(Element::new(Tag::Meta).with_attribute("charset", "utf-8"))
.with_head_element(Element::new(Tag::Title).with_text(title))
.with_head_element(Element::new(Tag::Style).with_text(CSS))
.with_head_element(
Element::new(Tag::Link)
.with_attribute("href", "local.css")
.with_attribute("rel", "stylesheet")
.with_attribute("type", "text/css"),
)
.with_body_element(Element::new(Tag::H1).with_text(title));
page.push_to_body(self.to_html_messages());
page
}
pub fn to_html_messages(&self) -> Element {
let mut e = Element::new(Tag::Div);
let mut prev_source = None;
let mut first_failure = true;
for (i, msg) in self.msgs.iter().enumerate() {
if Some(msg.source) != prev_source {
let h = match msg.source {
RunLogSource::Prelude => "Prelude, before pre-plan starts",
RunLogSource::PrePlan => "Pre-plan, before VM starts",
RunLogSource::Plan => "Plan, inside VM without network",
RunLogSource::PostPlan => "Post-plan, after VM stops",
RunLogSource::Epilog => "Epilog, aft3er post-plan is done",
};
e.push_child(Element::new(Tag::H2).with_text(h));
prev_source = Some(msg.source);
}
let child: Element = msg.into();
let id = if !msg.detail.is_successful() && first_failure {
first_failure = false;
"failed".to_string()
} else {
format!("msg{i}")
};
e.push_child(
Element::new(Tag::Div).with_attribute("id", &id).with_child(
Element::new(Tag::A)
.with_attribute("href", format!("#{id}"))
.with_class("anchor")
.with_child(child),
),
);
}
let mut raw = Element::new(Tag::Ol);
for m in self.raw.iter() {
raw.push_child(
Element::new(Tag::Li).with_child(
Element::new(Tag::Pre)
.with_text(serde_json::to_string_pretty(m).unwrap_or(format!("{m:#?}"))),
),
);
}
e.push_child(
Element::new(Tag::H2)
.with_attribute("id", "json-log")
.with_child(
Element::new(Tag::A)
.with_attribute("href", "#json-log")
.with_text("Raw log messages for Ambient troubleshooting"),
),
);
e.push_child(
Element::new(Tag::Details)
.with_child(Element::new(Tag::Summary).with_text("Raw log messages"))
.with_child(Element::new(Tag::P).with_text(
"These raw log messages are meant to help Ambient developers figure out problems. You can ignore them.",
))
.with_child(Element::new(Tag::Div).with_child(raw)),
);
if let Some(console_log) = &self.console {
e.push_child(
Element::new(Tag::H2)
.with_attribute("id", "console-log")
.with_child(
Element::new(Tag::A)
.with_attribute("href", "#console-log")
.with_text("Raw console log from virtual machine, for troubleshoting"),
),
);
e.push_child(
Element::new(Tag::Details)
.with_child(Element::new(Tag::Summary).with_text("Raw log messages"))
.with_child(Element::new(Tag::P).with_text(
"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.",
))
.with_child(Element::new(Tag::Pre).with_text(String::from_utf8_lossy(console_log).to_string())),
);
}
e
}
}
#[derive(Debug, Serialize)]
struct SynthMessage {
#[serde(flatten)]
detail: SynthDetail,
timestamp: SystemTime,
offset: Duration,
source: RunLogSource,
}
impl SynthMessage {
fn from_run_log_message(log_msg: &RunLogMessage, offset: Duration) -> Self {
if let Ok(detail) = SynthDetail::try_from(&log_msg.detail) {
Self {
detail,
timestamp: log_msg.timestamp,
offset,
source: log_msg.log_source,
}
} else {
Self {
detail: SynthDetail::Other(log_msg.clone()),
timestamp: log_msg.timestamp,
offset,
source: log_msg.log_source,
}
}
}
}
#[derive(Debug, Serialize)]
#[allow(clippy::large_enum_variant)]
enum SynthDetail {
AmbientStarts {
name: String,
version: String,
},
AmbientEndsSuccssfully,
AmbientEndsInFailure,
AmbientRuntimeConfig(Config),
StartQemu {
argv: Vec<OsString>,
},
QemuSucceeded {
exit_code: i32,
stdout: String,
stderr: String,
},
QemuFailed {
exit_code: Option<i32>,
stdout: String,
stderr: String,
},
ExecutorStarts {
name: String,
version: String,
},
RunnablePlan(RunnablePlan),
SuccessfulAction {
action: RunnableAction,
log_messages: Vec<SynthMessage>,
},
ExecuteAction(RunnableAction),
ActionSucceeded(RunnableAction),
ActionFailed(RunnableAction),
DebGet {
packages: Vec<Needed>,
},
NpmGetSucceded,
NpmGetFailed2(String),
SuccessfulCustomAction {
custom: Custom,
log_messages: Vec<SynthMessage>,
},
CustomActionStarts {
source: PathBuf,
custom: Custom,
exe: PathBuf,
exe_exists: bool,
},
CustomActionOutput {
stdout: Vec<u8>,
stderr: Vec<u8>,
},
StartProgram {
argv: Vec<OsString>,
},
ProgramSucceeded {
exit_code: i32,
stdout: String,
stderr: String,
},
ProgramFailed {
exit_code: Option<i32>,
stdout: String,
stderr: String,
},
PlanSucceeded,
Other(RunLogMessage),
}
impl SynthDetail {
fn is_successful(&self) -> bool {
!matches!(
self,
Self::AmbientEndsInFailure
| Self::QemuFailed { .. }
| Self::ActionFailed(_)
| Self::ProgramFailed { .. }
)
}
}
impl TryFrom<&RunLogMessageDetail> for SynthDetail {
type Error = ();
fn try_from(log_detail: &RunLogMessageDetail) -> Result<Self, Self::Error> {
let detail = match log_detail {
RunLogMessageDetail::AmbientStarts { name, version } => SynthDetail::AmbientStarts {
name: name.to_string(),
version: version.to_string(),
},
RunLogMessageDetail::AmbientRuntimeConfig(config) => {
SynthDetail::AmbientRuntimeConfig(config.clone())
}
RunLogMessageDetail::AmbientEndsSuccssfully => SynthDetail::AmbientEndsSuccssfully,
RunLogMessageDetail::AmbientEndsInFailure => SynthDetail::AmbientEndsInFailure,
RunLogMessageDetail::StartQemu { argv } => SynthDetail::StartQemu {
argv: argv.to_vec(),
},
RunLogMessageDetail::QemuSucceeded {
exit_code,
stdout,
stderr,
} => SynthDetail::QemuSucceeded {
exit_code: *exit_code,
stdout: stdout.to_string(),
stderr: stderr.to_string(),
},
RunLogMessageDetail::ExecutorStarts { name, version } => SynthDetail::ExecutorStarts {
name: name.to_string(),
version: version.to_string(),
},
RunLogMessageDetail::RunnablePlan(plan) => SynthDetail::RunnablePlan(plan.clone()),
RunLogMessageDetail::PlanSucceeded => SynthDetail::PlanSucceeded,
RunLogMessageDetail::ExecuteAction(action) => {
SynthDetail::ExecuteAction(action.clone())
}
RunLogMessageDetail::ActionSucceeded(action) => {
SynthDetail::ActionSucceeded(action.clone())
}
RunLogMessageDetail::ActionFailed(action) => SynthDetail::ActionFailed(action.clone()),
RunLogMessageDetail::StartProgram { argv } => SynthDetail::StartProgram {
argv: argv.to_vec(),
},
RunLogMessageDetail::ProgramSucceeded {
exit_code,
stdout,
stderr,
} => SynthDetail::ProgramSucceeded {
exit_code: *exit_code,
stdout: stdout.to_string(),
stderr: stderr.to_string(),
},
RunLogMessageDetail::ProgramFailed {
exit_code,
stdout,
stderr,
} => SynthDetail::ProgramFailed {
exit_code: *exit_code,
stdout: stdout.to_string(),
stderr: stderr.to_string(),
},
RunLogMessageDetail::DebGet { packages } => SynthDetail::DebGet {
packages: packages.clone(),
},
RunLogMessageDetail::NpmGetSucceded => SynthDetail::NpmGetSucceded,
RunLogMessageDetail::NpmGetFailed1 { error } => {
SynthDetail::NpmGetFailed2(error.to_string())
}
RunLogMessageDetail::CustomActionStarts {
source,
custom,
exe,
exe_exists,
} => SynthDetail::CustomActionStarts {
source: source.into(),
custom: custom.clone(),
exe: exe.to_path_buf(),
exe_exists: *exe_exists,
},
RunLogMessageDetail::CustomActionOutput { stdout, stderr } => {
SynthDetail::CustomActionOutput {
stdout: stdout.clone(),
stderr: stderr.clone(),
}
}
RunLogMessageDetail::RunCi { .. }
| RunLogMessageDetail::SkipCi { .. }
| RunLogMessageDetail::Debug { .. }
| RunLogMessageDetail::QemuFailed { .. }
| RunLogMessageDetail::ExecutorEndsSuccessfully
| &RunLogMessageDetail::ExecutorEndsInFailure { .. } => return Err(()),
};
Ok(detail)
}
}
impl From<&SynthMessage> for Element {
fn from(msg: &SynthMessage) -> Self {
fn stream(label: &'static str, text: &str) -> Element {
Element::new(Tag::Div)
.with_text(label)
.with_child(Element::new(Tag::Pre).with_text(text))
}
fn program_result(exit_code: Option<i32>, stdout: &str, stderr: &str) -> Element {
let mut e = Element::new(Tag::Div).with_text(format!(
"Exit code: {}",
exit_code
.map(|code| code.to_string())
.unwrap_or("Killed by signal".to_string())
));
if !stdout.is_empty() {
e.push_child(stream("Stdout:", stdout));
}
if !stderr.is_empty() {
e.push_child(stream("Stderr:", stderr));
}
e
}
let mut e = Element::new(Tag::Details).with_class(msg.source.class());
let mut summary = Element::new(Tag::Summary)
.with_text(msg.source.class())
.with_text(": ");
let mut more = Element::new(Tag::Div)
.with_class("more")
.with_child(
Element::new(Tag::Span)
.with_class("duration")
.with_text("After ")
.with_child(
Element::new(Tag::Span)
.with_class("time-offset")
.with_text(format!("{:.02} seconds", msg.offset.as_secs_f64())),
)
.with_text(" "),
)
.with_child(
Element::new(Tag::Span)
.with_class("timestamp")
.with_text(" at ")
.with_child(
Element::new(Tag::Span)
.with_class("exact-timestamp")
.with_text(
format_timestamp(msg.timestamp)
.unwrap_or("broken timestamp".to_string()),
),
),
);
match &msg.detail {
SynthDetail::SuccessfulAction {
action,
log_messages,
} => {
summary.push_text("Successful action ");
summary.push_child(
Element::new(Tag::Span)
.with_class("action-name")
.with_text(action.summary()),
);
let mut details = Element::new(Tag::Ul);
for underlying in log_messages {
details.push_child(Element::new(Tag::Li).with_child(underlying.into()));
}
more.push_child(
Element::new(Tag::Div)
.with_class("action-details")
.with_child(details),
);
}
SynthDetail::DebGet { packages } => {
summary.push_text("Download deb packages");
let mut list = Element::p();
for needed in packages.iter() {
list.push_child(Element::new(Tag::Li).with_text(needed.package_name()));
}
more.push_child(Element::div().with_child(list));
}
SynthDetail::NpmGetSucceded => {
summary.push_text("Download of npm packages succeeded");
}
SynthDetail::NpmGetFailed2(msg) => {
summary.push_text("Download of npm packages failed");
more.push_child(Element::new(Tag::Pre).with_text(msg));
}
SynthDetail::SuccessfulCustomAction {
custom,
log_messages,
} => {
summary.push_text("Successful action ");
summary.push_child(
Element::new(Tag::Span)
.with_class("action-name")
.with_text("custom: ")
.with_text(&custom.name),
);
let mut details = Element::new(Tag::Ul);
for underlying in log_messages {
details.push_child(Element::new(Tag::Li).with_child(underlying.into()));
}
more.push_child(
Element::new(Tag::Div)
.with_class("action-details")
.with_child(details),
);
}
SynthDetail::AmbientStarts { name, version } => {
summary.push_text("Ambient starts");
more.push_child(
Element::new(Tag::P).with_child(
Element::new(Tag::Span).with_text("Program: ").with_child(
Element::new(Tag::Span)
.with_class("program-name")
.with_text(name),
),
),
);
more.push_child(
Element::new(Tag::P).with_child(
Element::new(Tag::Span).with_text("Version: ").with_child(
Element::new(Tag::Span)
.with_class("program-version")
.with_text(version),
),
),
);
}
SynthDetail::AmbientEndsSuccssfully => {
summary.push_text("Ambient ends, success");
more.push_text("Everything is fine.");
}
SynthDetail::AmbientEndsInFailure => {
summary.push_text("Ambient ends, failure");
more.push_text("Woe be us!");
}
SynthDetail::AmbientRuntimeConfig(config) => {
summary.push_text("Ambient configuration");
let config = serde_norway::to_string(config)
.unwrap_or("Error serializing configuration to YAML".to_string());
more.push_child(
Element::new(Tag::Pre)
.with_class("ambient-config")
.with_text(&config),
);
}
SynthDetail::StartQemu { argv } => {
summary.push_text("Start QEMU");
let mut args = Element::new(Tag::Ul).with_class("argv");
for arg in argv.iter() {
args.push_child(
Element::new(Tag::Li).with_class("arg").with_child(
Element::new(Tag::Span)
.with_class("program-arg")
.with_text(String::from_utf8_lossy(arg.as_encoded_bytes())),
),
);
}
more.push_child(args);
}
SynthDetail::QemuSucceeded {
exit_code,
stdout,
stderr,
} => {
summary.push_text("QEMU succeeded");
more.push_child(program_result(Some(*exit_code), stdout, stderr));
}
SynthDetail::QemuFailed {
exit_code,
stdout,
stderr,
} => {
summary.push_text("QEMU failed");
more.push_child(program_result(*exit_code, stdout, stderr));
}
SynthDetail::ExecutorStarts { name, version } => {
summary.push_text("Executor starts");
more.push_child(
Element::new(Tag::Span).with_text("Program: ").with_child(
Element::new(Tag::Span)
.with_class("program-name")
.with_text(name),
),
);
more.push_child(Element::new(Tag::Br));
more.push_child(
Element::new(Tag::Span).with_text("Version: ").with_child(
Element::new(Tag::Span)
.with_class("program-version")
.with_text(version),
),
);
}
SynthDetail::RunnablePlan(plan) => {
summary.push_text("Runnable plan");
let yaml =
serde_norway::to_string(plan).unwrap_or("plan to YAML failed".to_string());
more.push_child(
Element::new(Tag::Pre)
.with_class("runnable-plan")
.with_text(&yaml),
);
}
SynthDetail::PlanSucceeded => {
summary.push_text("Plan succeeded");
more.push_text("Hopefully all is good.");
}
SynthDetail::ExecuteAction(action) => {
summary.push_text("Start action ");
summary.push_text(action.summary());
more.push_child(Element::new(Tag::Pre).with_text(format!("{:#?}", action)));
}
SynthDetail::ActionSucceeded(action) => {
summary.push_text("Action succeeded ");
summary.push_text(action.summary());
more.push_child(Element::new(Tag::Pre).with_text(format!("{:#?}", action)));
}
SynthDetail::ActionFailed(action) => {
summary.push_text("Action failed: ");
summary.push_text(action.summary());
more.push_child(Element::new(Tag::Pre).with_text(format!("{:#?}", action)));
}
SynthDetail::CustomActionStarts { custom, .. } => {
summary.push_text("Start action custom: ");
summary.push_text(&custom.name);
more.push_child(Element::new(Tag::Pre).with_text(format!("{:#?}", custom)));
}
SynthDetail::CustomActionOutput { stdout, stderr } => {
summary.push_text("Custom action output");
more.push_child(stream("Stdout:", &String::from_utf8_lossy(stdout)));
more.push_child(stream("Stderr:", &String::from_utf8_lossy(stderr)));
}
SynthDetail::StartProgram { argv } => {
summary.push_text("Start program ");
summary.push_text(
argv.first()
.map(|s| String::from_utf8_lossy(s.as_encoded_bytes()).to_string())
.unwrap_or("unrepresentable string".to_string()),
);
let mut args = Element::new(Tag::Ul).with_class("argv");
for arg in argv.iter() {
args.push_child(
Element::new(Tag::Li).with_class("arg").with_child(
Element::new(Tag::Span)
.with_class("program-arg")
.with_text(String::from_utf8_lossy(arg.as_encoded_bytes())),
),
);
}
more.push_child(args);
}
SynthDetail::ProgramSucceeded {
exit_code,
stdout,
stderr,
} => {
summary.push_text("Program succeeded");
more.push_child(program_result(Some(*exit_code), stdout, stderr));
}
SynthDetail::ProgramFailed {
exit_code,
stdout,
stderr,
} => {
summary.push_text("ERROR: Program failed");
more.push_child(program_result(*exit_code, stdout, stderr));
}
SynthDetail::Other(log_msg) => {
summary.push_text(format!("{log_msg:?}"));
}
}
if msg.detail.is_successful() {
e.add_class("succeeded");
} else {
e.add_class("failed");
}
e.push_child(summary);
e.push_child(more);
e
}
}
#[derive(Debug, thiserror::Error)]
pub enum RunLogError {
#[error("line in log is not JSON: {0:?}")]
Json(String, #[source] serde_json::Error),
#[error("failed to read log file")]
ReadLog(#[source] std::io::Error),
#[error("log file is not UTF8")]
Utf8(#[source] std::string::FromUtf8Error),
#[error("run log does not contain BEGIN marker")]
NoBegin,
#[error("failed to create file or open it for writing: {0}")]
Create(PathBuf, #[source] std::io::Error),
}