use microsandbox_protocol::core::InitFailureReason;
use microsandbox_runtime::boot_error::{BootError, BootErrorStage};
use crate::ui::{self, ErrorLine};
pub fn render(name: &str, err: &BootError) {
let header = format!("failed to start {:?}", name);
let stage = stage_label(err.stage);
let cause = format!("{}: {}", stage, err.message);
let hint_text = stage_hint(err);
let log_pointer = format!("run `msb logs --source system {name}` for full diagnostics");
let mut lines: Vec<ErrorLine<'_>> = Vec::with_capacity(3);
lines.push(ErrorLine::Cause(&cause));
if let Some(ref h) = hint_text {
lines.push(ErrorLine::Hint(h));
}
lines.push(ErrorLine::Hint(&log_pointer));
ui::error_with_lines(&header, &lines);
}
fn stage_label(stage: BootErrorStage) -> &'static str {
match stage {
BootErrorStage::Mount => "mount",
BootErrorStage::BuildVm => "build_vm",
BootErrorStage::Config => "config",
BootErrorStage::Network => "network",
BootErrorStage::Image => "image",
BootErrorStage::Other => "other",
}
}
fn stage_hint(err: &BootError) -> Option<String> {
match (err.reason, err.stage, err.errno) {
(Some(InitFailureReason::UserNotFound), _, _) => Some(
"user selects an existing account; remove the user \
override to use the image default, or create that account \
in the image"
.into(),
),
(Some(InitFailureReason::GroupNotFound), _, _) => Some(
"the requested group must exist in the image; correct the group \
override or create that group in the image"
.into(),
),
(_, BootErrorStage::Mount, Some(2)) => Some(extract_mount_hint(&err.message)),
(_, BootErrorStage::Mount, Some(13)) => {
Some("the host path is not readable by msb (check permissions)".into())
}
(_, BootErrorStage::Image, Some(2)) => {
Some("rootfs not found — try `msb pull <image>` first".into())
}
(_, BootErrorStage::Network, Some(48)) | (_, BootErrorStage::Network, Some(98)) => Some(
"a port is already bound — try a different host port or stop the other process".into(),
),
_ => None,
}
}
fn extract_mount_hint(_message: &str) -> String {
"the host path for one of the mounts does not exist".into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn guest_hints_use_typed_reasons_not_diagnostic_wording() {
for (reason, expected_hint) in [
(
serde_json::json!("user_not_found"),
Some("user selects an existing account"),
),
(
serde_json::json!("group_not_found"),
Some("the requested group must exist"),
),
(serde_json::json!("a_future_reason"), None),
(serde_json::Value::Null, None),
] {
for message in [
"new diagnostic wording",
"guest initialization failed: guest user not found: iggy",
] {
let mut record = serde_json::json!({
"t": "2026-10-02T00:00:00Z",
"stage": "other",
"errno": null,
"message": message,
});
if !reason.is_null() {
record["reason"] = reason.clone();
}
let error: BootError = serde_json::from_value(record).unwrap();
let hint = stage_hint(&error);
match expected_hint {
Some(expected) => assert!(hint.unwrap().starts_with(expected)),
None => assert!(hint.is_none()),
}
}
}
}
#[test]
fn mount_enoent_has_hint() {
let err = BootError {
t: "2026-04-30T20:32:59.690Z".into(),
stage: BootErrorStage::Mount,
reason: None,
errno: Some(2),
message: "mount foo: No such file or directory (os error 2)".into(),
};
assert!(stage_hint(&err).is_some());
}
#[test]
fn other_with_no_errno_has_no_hint() {
let err = BootError {
t: "2026-04-30T20:32:59.690Z".into(),
stage: BootErrorStage::Other,
reason: None,
errno: None,
message: "weird".into(),
};
assert!(stage_hint(&err).is_none());
}
#[test]
fn network_eaddrinuse_macos_and_linux() {
let err = BootError {
t: "x".into(),
stage: BootErrorStage::Network,
reason: None,
errno: Some(48), message: "bind: in use".into(),
};
assert!(stage_hint(&err).is_some());
let err2 = BootError {
t: "x".into(),
stage: BootErrorStage::Network,
reason: None,
errno: Some(98), message: "bind: in use".into(),
};
assert!(stage_hint(&err2).is_some());
}
}