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;