Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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}"
        );
    }
}