crates/installer/nordisk-narrate/src/boxside.rs
//! **A product's whole DEV-NARRATE start, in one call** (features `dev-narrate`
//! and `bootlog`) — shared by gunnar-server, gunnar-front, holger and
//! holger-estate-front (holger RULE 1: no product copies another's).
//!
//! Called first thing in the product's `main`, right after its boot log
//! captured the ESP's `BOOTLOG.TXT`:
//!
//! 1. korp-start's RAM hand-off when the ESP could not be read
//! ([`crate::korp_start_fallback`]);
//! 2. the kernel's records, followed into the log ([`crate::kmsg::follow`]);
//! 3. on its own thread: the seed's `installer_pubkey` (leniently, waiting for
//! the seed up to [`SEED_WAIT`]) and the sender bound on `0.0.0.0:`[`crate::PORT`]
//! at once — before any NIC has an address — streaming THE SAME log.
//!
//! Nothing here is ever fatal: this is diagnostics.
use crate::{kmsg, sender, seedkey};
use nordisk_pairing::bootlog::BootLog;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
/// How long the seed may take to appear (korp-start stages the data root
/// before the exec, so it is normally there at once).
pub const SEED_WAIT: Duration = Duration::from_secs(120);
/// **Start narrating `log` as `product`**; returns at once. `data_root` holds
/// `pairing/seed.toml`; `console` writes one line where a person sees it (the
/// product's own console writer — never fd 2 when fd 2 is captured into `log`).
pub fn start(log: &'static BootLog, data_root: PathBuf, product: &'static str, console: fn(&str)) {
let say = move |l: &str| {
log.push(l);
console(l);
};
if crate::korp_start_fallback(Path::new(nordisk_pairing::bootlog::ESP_BOOTLOG), Path::new(crate::KORP_START_HANDOFF), product, &|l| log.push(l)) {
say("dev-narrate: the ESP's BOOTLOG.TXT could not be read — korp-start's narration was taken from its RAM hand-off instead");
}
if kmsg::follow(move |l| log.push(l)) {
say("dev-narrate: the kernel's records (/dev/kmsg, from the first one the ring holds) are followed into the boot log");
}
let spawned = std::thread::Builder::new().name("dev-narrate-start".into()).spawn(move || {
let bind = SocketAddr::from(([0, 0, 0, 0], crate::PORT));
match start_at(log, &data_root, product, bind, SEED_WAIT) {
Ok(at) => say(&format!("{} — bound {at}, product {product}", crate::STARTED)),
Err(e) => say(&format!("dev-narrate: OFF — {e} (the console and BOOTLOG.TXT on the ESP still have every line)")),
}
});
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:
/// wait up to `wait` for the seed, read the installer's public key out of it
/// (a seed that fails its own check still narrates), start the sender.
pub fn start_at(log: &'static BootLog, root: &Path, product: &str, bind: SocketAddr, wait: Duration) -> Result<SocketAddr, String> {
let path = root.join(seedkey::SEED_REL);
let t0 = Instant::now();
let text = loop {
match std::fs::read_to_string(&path) {
Ok(t) => break t,
Err(e) if t0.elapsed() >= wait => return Err(format!("no seed at {}: {e}", path.display())),
Err(_) => std::thread::sleep(Duration::from_millis(500)),
}
};
let installer_pub = seedkey::installer_pubkey(&text).map_err(|e| format!("{}: {e}", path.display()))?;
let cfg = sender::Config { installer_pub, bind, product: product.to_string(), mac: sender::mac_of("eth0") };
sender::start(cfg, log).map_err(|e| format!("udp {bind}: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::UdpSocket;
/// **A DEV box answers a knock with a WHO sealed to its SEED's installer
/// key**; a data root with no seed narrates nothing and says why.
#[test]
fn a_box_narrates_to_the_installer_key_its_seed_names() {
static LOG: BootLog = BootLog::new(100);
let installer = crate::wire::generate();
let root = std::env::temp_dir().join(format!("narrate-boxside-{}", std::process::id()));
std::fs::create_dir_all(root.join("pairing")).unwrap();
std::fs::write(root.join(seedkey::SEED_REL), format!("estate = \"e\"\n[installer_link]\ninstaller_pubkey = \"{}\"\nvm_private = \"broken\"\n", seedkey::b64_of_key(installer.public_key().as_bytes()))).unwrap();
LOG.push("korp-start: STAGE exec");
let at = start_at(&LOG, &root, "holger", "127.0.0.1:0".parse().unwrap(), Duration::from_secs(1)).unwrap();
let probe = UdpSocket::bind("127.0.0.1:0").unwrap();
probe.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
probe.send_to(&crate::wire::knock([5; 16]), at).unwrap();
let mut buf = [0u8; 2048];
let (n, _) = probe.recv_from(&mut buf).unwrap();
let (_, up) = crate::wire::open_up(&installer, &buf[..n]).expect("sealed to the seed's installer key");
let crate::wire::Up::Who { id, echo } = up else { panic!("{up:?}") };
assert_eq!((id.product.as_str(), echo), ("holger", [5; 16]));
let none = std::env::temp_dir().join(format!("narrate-boxside-none-{}", std::process::id()));
assert!(start_at(&LOG, &none, "holger", "127.0.0.1:0".parse().unwrap(), Duration::from_millis(10)).unwrap_err().contains("no seed at"));
let _ = std::fs::remove_dir_all(&root);
}
}