holger-estate-front/src/pid1_fault.rs
//! **A panic on the front ends as a fault** (lane G6, Holger's parity with Gunnar's GN-99
//! hook, `gunnar-server/src/boot.rs` 7e33c0f7): edda's `fault::install_panic_fault` with
//! holger-server's code, `HL-99` (`server/cli/src/paired.rs`), the crash record on the data
//! disk with the boot log's tail, then a serving front restarts (unless it faulted within
//! 30 min) and a booting one powers off.
use std::path::PathBuf;
/// Holger's panic code (holger-server's `paired::PANIC_CODE`).
pub const PANIC_CODE: &str = "HL-99";
/// What it means.
pub const PANIC_MEANING: &str = "the app panicked";
/// The panic as edda's fault: Holger's code, the crash record under `data`.
pub fn panic_fault(data: Option<PathBuf>) -> appliance_pid1::fault::PanicFault {
appliance_pid1::fault::PanicFault {
code: PANIC_CODE,
meaning: PANIC_MEANING,
data,
}
}
/// **Install the front's panic hook**: the crash record under `data` with what `narration`
/// answers (the boot log's tail), every line through `say` (the console). Armed only on the
/// appliance (PID 1 booted by korp), never in a test, on a laptop or in a container.
pub fn install_panic_fault(data: PathBuf, narration: fn() -> Vec<u8>, say: fn(&str)) {
appliance_pid1::fault::narration_from(narration);
appliance_pid1::fault::install_panic_fault(
panic_fault(Some(data)),
appliance_pid1::stop::on_appliance,
move |l: &str| {
say(&format!("holger-estate-front: {l}\n"));
tracing::error!("{l}");
},
);
}
/// **The front's late lease** (edda `net::LateLease`): its line through `narrate` (the boot
/// log), renewed on the server's clock (the front has no T1 seam).
pub fn late_lease(narrate: fn(&str)) -> appliance_pid1::net::LateLease {
appliance_pid1::net::LateLease {
narrate,
t1_override_var: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The front's code is holger-server's.
#[test]
fn the_fronts_panic_code_is_holger_servers() {
let server = include_str!("../../server/cli/src/paired.rs");
assert!(
server.contains(&format!("pub const PANIC_CODE: &str = \"{PANIC_CODE}\";"))
&& server.contains(&format!(
"pub const PANIC_MEANING: &str = \"{PANIC_MEANING}\";"
))
);
}
/// ★ **A panic on a serving front restarts it, under the 30-minute loop guard** (lane
/// G6): the front's hook only shouted, so as PID 1 a panic was `Attempted to kill
/// init!` and `panic=10` booted the box into the same fault, with no code and no record.
/// Now it is edda's fault with Holger's panic code: the crash record holds the boot
/// log's tail, a serving box restarts, a second panic within 30 min powers off.
#[test]
fn a_panic_on_a_serving_front_restarts_it_unless_it_loops() {
assert_eq!(PANIC_CODE, "HL-99");
let d = tempfile::tempdir().unwrap();
let p = panic_fault(Some(d.path().to_path_buf()));
assert_eq!((p.code, p.meaning), ("HL-99", "the app panicked"));
appliance_pid1::fault::narration_from(|| {
b"holger-estate-front: STAGE serving g6-front\n".to_vec()
});
let first = appliance_pid1::fault::panic_ending(
&p,
"panicked at src/main.rs:1: boom",
("serving", true),
5,
);
assert_eq!(
first.end,
appliance_pid1::fault::End::Restart,
"{:?}",
first.said
);
let record = std::fs::read_to_string(d.path().join("crash/fault.txt")).unwrap();
assert!(
record.contains("HL-99: the app panicked") && record.contains("STAGE serving g6-front"),
"{record}"
);
let again = appliance_pid1::fault::panic_ending(&p, "panicked again", ("serving", true), 6);
assert_eq!(
again.end,
appliance_pid1::fault::End::PowerOff,
"a panic within 30 min of the last is a loop"
);
let boot = appliance_pid1::fault::panic_ending(
&panic_fault(None),
"panicked at boot",
("pairing", false),
7,
);
assert_eq!(
boot.end,
appliance_pid1::fault::End::PowerOff,
"a panic during the boot powers off"
);
}
/// ★ **G6 follow-up (lane B4): the front rides edda's late lease.** Its utility NIC's late
/// DHCP lease is narrated into the boot log (`net::bring_up_network_with` with a
/// `LateLease`), not taken quietly.
#[test]
fn the_front_rides_eddas_late_lease() {
let main = include_str!("main.rs");
assert!(
main.contains(
"appliance_pid1::net::bring_up_network_with(plans, &how, front_late_lease())"
),
"the front's bring-up"
);
assert!(
!main.contains("appliance_pid1::net::bring_up_network(plans"),
"never the quiet late lease"
);
let late = crate::pid1_fault::late_lease(|_| {});
assert_eq!(late.t1_override_var, None, "the front has no T1 seam");
}
/// The front's `main` installs this hook (never a bare shout), reports its stages to
/// edda's fault progress (so a panic knows the box served), and arms on the appliance.
#[test]
fn the_front_installs_the_fault_hook_and_reports_its_stages() {
let main = include_str!("main.rs");
assert!(
main.contains("front::pid1_fault::install_panic_fault("),
"main installs the hook"
);
assert!(
!main.contains("std::panic::set_hook"),
"no bare shout-only hook"
);
assert!(
main.contains("appliance_pid1::fault::reached(\"network\")")
&& main.contains("appliance_pid1::fault::reached(\"pairing\")"),
"the boot stages"
);
assert!(
main.contains("appliance_pid1::fault::serving_now()"),
"serving"
);
let src = include_str!("pid1_fault.rs");
let f = &src[src.find("pub fn install_panic_fault").unwrap()..];
assert!(
f[..f.find("\n}\n").unwrap()].contains("appliance_pid1::stop::on_appliance"),
"armed only on the appliance (never a test, a laptop or a container)"
);
}
/// ★ **The clean stop powers off on the appliance only** (lane G6, G4's note): it was
/// gated on `is_init()`, so a front that is PID 1 in a container powered "off" — a
/// refused `reboot(2)` that parks the container. A clean stop ends by
/// `after_clean_stop(on_appliance())`: the power on the appliance, an exit elsewhere.
#[test]
fn the_clean_stop_powers_off_on_the_appliance_only() {
let main = include_str!("main.rs");
let stop = &main[main
.find("let why = appliance_pid1::runtime::shutdown_request().await;")
.unwrap()..];
let stop = &stop[..stop.find("\n loop {").unwrap()];
// Read with the whitespace out: rustfmt wraps the comparison over lines.
let flat: String = stop.split_whitespace().collect();
assert!(flat.contains("appliance_pid1::stop::after_clean_stop(appliance_pid1::stop::on_appliance())==appliance_pid1::stop::AfterStop::PowerOff"), "{stop}");
assert!(
stop.contains("std::process::exit(0)"),
"off the appliance the process exits: {stop}"
);
}
}