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

nordisk/holger

server/dev-narrate/src/lib.rs

//! **DEV-NARRATE, Holger's half** (owner 2026-10-19: "you must build NOT --release so DEBUG
//! is on EXACTLY like gunnar").
//!
//! A DEV medium's box streams its boot log — the ESP's `BOOTLOG.TXT` (the install phase,
//! korp-start), the kernel's records and every line this process logs (early boot, pairing,
//! certify, twin follow, report) — as SEALED UDP datagrams on udp/[`nordisk_narrate::PORT`]
//! to the installer whose authenticated hello reaches it, from the moment the NIC has an
//! address and before the installer's WireGuard link exists. The sender is Skidbladnir's
//! `nordisk_narrate::boxside`, the same one gunnar-server and gunnar-front start; the ear is
//! the same `nordisk_narrate::receiver` the installer runs. Nothing of it is in a RELEASE
//! build: the sender exists only under the `dev-narrate` feature, which only the DEV medium's
//! build (`cargo xtask estate-media`, the default kind) asks for.
//!
//! One thing is Holger's own: a medium the ONE box lays from carries no seed, only the
//! metadata marker, until the box has read its seed off the provider's metadata
//! (Skidbladnir `nordisk_pairing::seed::metadata`). `boxside` reads the seed once, so it would read the marker and stay
//! OFF. [`start`] waits for the marker to be replaced first ([`MARKER_WAIT`]).
//!
//! Std plus Skidbladnir only: holger-server (main, twin) and holger-estate-front (the front,
//! its own workspace) depend on this crate (A28 L12: no `#[path]` into another crate). Nothing
//! here is ever fatal.

/// The marker every stage line carries (gunnar's `narrate::STAGE_MARKER`): the installer's
/// ear says a line with `STAGE ` in it out loud the moment it arrives.
pub const STAGE_MARKER: &str = "STAGE";

/// Whether this binary carries the sender (a DEV build).
pub const ENABLED: bool = cfg!(feature = "dev-narrate");

/// Pure: **one boundary of the boot, as a line**: `STAGE <token> <detail>`. Logged by the
/// caller through its own logger, which writes the boot log the sender streams.
pub fn stage_line(token: &str, detail: &str) -> String {
    let detail = detail.replace(['\r', '\n'], " ");
    let detail = detail.trim();
    if detail.is_empty() {
        format!("{STAGE_MARKER} {token}")
    } else {
        format!("{STAGE_MARKER} {token} {detail}")
    }
}

/// How long [`start`] waits for the metadata marker to become the shop's seed (the box asks
/// the metadata for three minutes, `nordisk_pairing::seed::metadata::{METADATA_TRIES, METADATA_EVERY}`).
pub const MARKER_WAIT: std::time::Duration = std::time::Duration::from_secs(600);

/// Pure: whether a staged seed file is still a release medium's metadata marker (the box has
/// not read its seed yet): the ONE parser, Skidbladnir nordisk-pairing `seed` (lane B4), the
/// same one the seed reader (`nordisk_pairing::seed::metadata`) asks.
pub fn is_marker(text: &str) -> bool {
    nordisk_pairing::seed::is_metadata_marker(text)
}

#[cfg(any(feature = "dev-narrate", test))]
pub use on::*;

#[cfg(any(feature = "dev-narrate", test))]
mod on {
    use nordisk_narrate::seedkey::SEED_REL;
    use nordisk_pairing::bootlog::BootLog;
    use std::net::SocketAddr;
    use std::path::{Path, PathBuf};
    use std::time::{Duration, Instant};

    /// Wait while `<root>/pairing/seed.toml` is the metadata marker, up to `wait`. `Ok` once
    /// it is anything else (or absent: `boxside` then waits for it itself and says why);
    /// `Err` naming the wait when the marker outlived it.
    pub fn await_seed(root: &Path, wait: Duration) -> Result<(), String> {
        let path = root.join(SEED_REL);
        let t0 = Instant::now();
        loop {
            match std::fs::read_to_string(&path) {
                Ok(t) if super::is_marker(&t) => {}
                _ => return Ok(()),
            }
            if t0.elapsed() >= wait {
                return Err(format!(
                    "{} is still the metadata marker after {} s: the box never read its seed",
                    path.display(),
                    wait.as_secs()
                ));
            }
            std::thread::sleep(Duration::from_millis(200));
        }
    }

    /// **Narrate `log` as `product`**, on its own thread; returns at once. `root` holds
    /// `pairing/seed.toml`; `console` writes one line where a person sees it.
    pub fn start(log: &'static BootLog, root: PathBuf, product: &'static str, console: fn(&str)) {
        // A28 M18, stays a thread: one bounded wait (MARKER_WAIT), then nordisk-narrate's own start; a start that fails is said.
        let spawned = std::thread::Builder::new().name("dev-narrate-seed".into()).spawn(move || match await_seed(&root, super::MARKER_WAIT) {
            Ok(()) => nordisk_narrate::boxside::start(log, root, product, console),
            Err(e) => {
                let l = format!("dev-narrate: OFF — {e} (the console and BOOTLOG.TXT on the ESP still have every line)");
                log.push(&l);
                console(&l);
            }
        });
        if let Err(e) = spawned {
            log.push(&format!(
                "dev-narrate: OFF — its thread could not start: {e}"
            ));
        }
    }

    /// [`start`]'s narrating half, pure over the data root and the bind address (the tests'
    /// door): the marker wait, then `boxside::start_at`.
    pub fn start_at(
        log: &'static BootLog,
        root: &Path,
        product: &str,
        bind: SocketAddr,
        wait: Duration,
    ) -> Result<SocketAddr, String> {
        await_seed(root, wait)?;
        nordisk_narrate::boxside::start_at(log, root, product, bind, wait)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_stage_is_one_line_with_the_marker_the_ear_says_out_loud() {
        assert_eq!(
            stage_line("pairing", "the installer's listener serves"),
            "STAGE pairing the installer's listener serves"
        );
        assert_eq!(stage_line("exec", " "), "STAGE exec");
        assert_eq!(stage_line("follow", "a\nb").lines().count(), 1);
    }

    #[test]
    fn the_metadata_marker_is_told_from_a_seed() {
        assert!(is_marker(
            "nordisk-pairing-seed-from = \"upcloud-metadata\"\n"
        ));
        assert!(is_marker(
            "# a release image: no seed\nnordisk-pairing-seed-from = \"upcloud-metadata\"\n"
        ));
        assert!(!is_marker(
            "estate = \"e\"\n[installer_link]\ninstaller_pubkey = \"x\"\n"
        ));
        assert!(!is_marker(""));
    }

    /// ★ **The narrator and the seed reader agree on the marker** (lane B4, A9 row 18): one
    /// parser, so a seed beside the marker line is a seed for both (the narrator's own prefix
    /// parser called it the marker and waited ten minutes), and the medium's composer stages
    /// the same line.
    #[test]
    fn the_narrator_and_the_seed_reader_share_one_marker_parser() {
        let seed_beside = format!(
            "{}\nestate = \"acme\"\n",
            nordisk_pairing::seed::SEED_FROM_METADATA
        );
        assert_eq!(
            is_marker(&seed_beside),
            nordisk_pairing::seed::is_metadata_marker(&seed_beside)
        );
        assert!(
            !is_marker(&seed_beside),
            "a seed beside the marker is a seed"
        );
        let xtask = include_str!("../../../xtask/src/estate_media.rs");
        assert!(
            xtask.contains(&format!(
                "pub const SEED_FROM_METADATA: &str = {:?};",
                format!("{}\n", nordisk_pairing::seed::SEED_FROM_METADATA)
            )),
            "estate-media stages the same marker line"
        );
    }
}

// The sender over a real UDP socket, the installer's receiver on the other end.
#[cfg(test)]
mod udp_tests;