nordisk/dwarves
dwarves-gui/src/boot_run.rs
//! **The in-pane boot: ▶ Build & Boot a `kind=vm` launch cell → live serial console.**
//!
//! Clicking Build on the korp KVM row builds the REAL install-on-boot PID-1 and boots
//! it under QEMU/OVMF/KVM through the constellation's ONE VM boot path —
//! `jera::vm::BootSpec` → `jera::vm::default_vm_runner()` → `draupnir::kvm::KvmBoot` →
//! `tunnr_vm::boot_test` (the same seam the forge's `runa_backend::run_vm` drives) —
//! streaming the live serial console INTO the pane's console view — the `installed
//! korp-demo` / `started korp-demo` / `KORP-APPLIANCE BOOT-OK` lines appear as they
//! stream, ending on a BOOT PASS chip. Reuse, never twin (L5): the boot engine is
//! tunnr's, the spec and lifecycle are jera's; this only drains the console. tunnr is
//! still linked for HOST FACTS (is QEMU/OVMF there, which kernel, which appliance),
//! never to boot.
//!
//! **Honest + fail-safe.** The real boot lives behind the `vm-boot` feature (it links
//! `jera/vm-tunnr` + `tunnr-vm`). Without it, or on a box with no QEMU/OVMF, the button surfaces the
//! missing-precondition error into the console (naming exactly what to install) and
//! ends FAILED — never a fabricated BOOT-OK. The boot source is a trait
//! ([`SerialConsole`]) produced by a [`BootLauncher`], so a robot test injects a fake
//! that replays a REAL captured korp serial and proves the pump + console path without
//! a heavy KVM boot.
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
/// The terminal-or-live phase of an in-pane boot.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BootPhase {
/// Compiling the appliance PID-1 (before QEMU starts).
Building,
/// QEMU is running; the BOOT-OK marker has not (yet) streamed past.
Booting,
/// The BOOT-OK marker appeared — the appliance reached runtime (smoke gate passed).
Passed,
/// The boot failed / was cancelled / hit a missing precondition. Carries why.
Failed(String),
}
impl BootPhase {
/// Stable label for the pane chip / `state_json`.
pub fn label(&self) -> &'static str {
match self {
BootPhase::Building => "building",
BootPhase::Booting => "booting",
BootPhase::Passed => "passed",
BootPhase::Failed(_) => "failed",
}
}
/// Whether the boot is still in flight (Building/Booting).
pub fn is_running(&self) -> bool {
matches!(self, BootPhase::Building | BootPhase::Booting)
}
}
/// **What kind of thing the RESULT console is showing** — the axis that picks the
/// console widget in the pane's result area: a `Container` streams a LOG window
/// ([`LogConsole`]), a `Vm`/`Iso` streams the QEMU SERIAL console. All three ride the
/// SAME [`SerialConsole`] sink + [`pump`], so the pane routes by this kind alone.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LaunchKind {
/// A `[[build.vm]]` KVM appliance — serial console (+ a graphical console when a
/// GPU is configured, see [`HwConfig`]).
Vm,
/// A `[[build.iso]]` bootable ISO — firmware-booted under OVMF, serial console.
Iso,
/// A `[[build.container]]` OCI image — its stdout/logs streamed into a LOG window.
Container,
}
impl LaunchKind {
/// Stable label for `state_json`.
pub fn as_str(self) -> &'static str {
match self {
LaunchKind::Vm => "vm",
LaunchKind::Iso => "iso",
LaunchKind::Container => "container",
}
}
}
/// Whether an ISO launch cell is a **live-boot scaffold** — a `*-iso` cell that is NOT
/// a disk installer, and whose bootable UEFI ESP loader does not exist in the tree yet.
///
/// A live-boot ISO would need a UEFI Linux loader inside the ESP (`EFIBOOT.IMG`) —
/// systemd-boot / GRUB EFI / an EFI-stub bzImage — to hand OVMF's firmware control to
/// the kernel+initramfs. The two efistub fixtures in the tree are terminal (a
/// serial-marker stub and an LBA-0 writer), so no such loader exists. The pane marks
/// these cells "scaffold · not bootable (needs ESP)" and the real launcher refuses them
/// HONESTLY (never a faked boot). korp-installer ISOs are NOT scaffolds — their ESP
/// carries a real, self-contained UEFI installer app; korp-kvm boots via QEMU `-kernel`.
///
/// Feature-independent (no `vm-boot`) so the pane can render the chip either way.
pub fn iso_cell_is_bootable_scaffold(cell_name: &str) -> bool {
cell_name.ends_with("-iso")
&& !cell_name.ends_with("-installer")
// korp-live-iso is NOT a scaffold: its ESP carries the self-contained korp-live UEFI
// stub (run-from-RAM), booted like the installer's self-contained ESP — no kernel
// loader needed. It routes to `boot_live_iso`, never the scaffold refusal.
&& !cell_name.contains("-live")
}
/// The honest, host-independent reason a live-boot ISO cell cannot boot yet — what the
/// real launcher surfaces into the console instead of a cryptic masterer error (and
/// never a faked BOOT-OK).
pub fn iso_scaffold_reason(cell_name: &str) -> String {
format!(
"'{cell_name}' is a SCAFFOLD ISO cell: the pure-Rust UEFI ISO masters, but \
booting the live kernel needs a UEFI Linux loader in the ESP (EFIBOOT.IMG) — \
systemd-boot / GRUB EFI / an EFI-stub bzImage — that is not in the tree yet, so \
there is nothing to hand OVMF's firmware control to the kernel+initramfs. Not \
faked. Run BYGG first — an iso-kind variant WITH a composed medium boots that \
medium (a self-contained ESP ISO) through the live leg — or boot the `-kvm` \
cell (direct QEMU -kernel) / a `-installer` ISO (real self-contained ESP)."
)
}
/// **The launch plan for an iso-kind variant** — the ONE routing rule the pane's ▶ and
/// the real launcher share, pure and feature-independent so a robot test proves it
/// without QEMU.
///
/// The rule closed gap #7 (dwarves-ui audit): a declared iso-kind variant whose BYGG
/// composed a REAL medium (isocompose output — a self-contained El Torito ESP ISO) was
/// still refused by the launcher ("no real appliance wired" / the scaffold refusal)
/// and offered the korp-installer efistub stand-in. A composed medium IS the product's
/// own bootable ISO, so it takes the live-ISO leg — firmware-boot off `-cdrom`,
/// run-from-RAM, GPU + QMP framebuffer per the hardware config — and it boots THE
/// COMPOSED FILE, never a re-mastered one. The stand-in remains ONLY for the
/// no-medium case, and keeps announcing itself.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum IsoLaunchPlan {
/// BYGG composed a real medium: firmware-boot THAT file through the live-ISO leg.
LiveComposed {
/// The composed medium, verbatim — the file the finished build named.
medium: PathBuf,
},
/// No composed medium on disk: the stand-in leg (an honest refusal or the
/// self-announcing scaffold), which never quietly boots something else.
Standin,
}
/// Decide the [`IsoLaunchPlan`] for an iso-kind variant: a medium that is really on
/// disk boots; anything else (no build yet, a failed build's empty artifact, a path
/// that has since vanished) is the stand-in leg.
pub fn plan_iso_launch(medium: Option<&Path>) -> IsoLaunchPlan {
match medium.filter(|p| p.is_file()) {
Some(p) => IsoLaunchPlan::LiveComposed {
medium: p.to_path_buf(),
},
None => IsoLaunchPlan::Standin,
}
}
/// **The serial marker a COMPOSED medium's boot gates on** — korp-start's own
/// `exec argv: ["<entry>"` line (`korp-installer/src/bin/korp-start.rs`, argv[0] from
/// `cmdline::app_argv`), printed through the `INSTALLER: `-prefixed serial sink as the
/// last thing before `fexecve` replaces the process. Reaching it proves the whole
/// composed chain ran — firmware → korp-boot → vmlinuz → korp-start → payload verified
/// — AND it names the product when the variant declares a `korp.entry=`. Shared by the
/// installer reboot leg and the iso-kind live boot so the two gates cannot drift.
pub fn composed_handoff_marker(entry: Option<&str>) -> String {
match entry {
Some(e) => format!("INSTALLER: exec argv: [\"{e}\""),
None => "INSTALLER: exec argv: [".to_string(),
}
}
// ── The live-ISO ssh forward: clone INTO the live-booted appliance (2026-08-20) ──────
/// **The guest TCP port the live-ISO ssh forward targets** — gunnar's convention: its
/// appliance sshd listens on `0.0.0.0:2222` (proven over a hostfwd by gunnar's own
/// forge test, `gunnar_appliance_iso`). korp/holger may listen elsewhere, and the
/// `[[build.iso]]`/`[[build.installer]]` descriptors declare NO per-variant
/// service-port token yet (checked 2026-08-20: name/volume_id/boot_params/params —
/// no port), so this is HONESTLY hardcoded to gunnar's convention. Follow-up: a
/// declared per-variant service-port token in the descriptor, read here.
pub const LIVE_SSH_GUEST_PORT: u16 = 2222;
/// The first host port the forward tries: `127.0.0.1:10022`.
pub const LIVE_SSH_HOST_PORT: u16 = 10022;
/// How many consecutive host ports the bounded pick-next-free walk probes
/// (10022..=10029) before failing BY NAME — never a silent rebind past the window,
/// never 0-means-anything.
pub const LIVE_SSH_PORT_TRIES: u16 = 8;
/// The planned QEMU netdev tokens for the live-ISO leg: SLIRP user networking on a
/// `virtio-net-pci` NIC — exactly the netdev the leg always had — plus a
/// LOOPBACK-bound host forward to the guest's service port, so
/// `git clone ssh://…@127.0.0.1:<host_port>:…` reaches the live-booted appliance.
///
/// Bound to `127.0.0.1` deliberately: tunnr's `NetConfig::User { hostfwd }` seam
/// spells `tcp::<h>-:<g>` (all interfaces), and a demo box must not expose the
/// guest's sshd on the LAN — so the leg suppresses tunnr's default NIC and authors
/// this netdev itself (`NetConfig::None` + these tokens). Folding a bind-address into
/// tunnr's `NetConfig` is the named follow-up. Pure and feature-independent so the
/// argv is asserted without QEMU.
pub fn live_ssh_netdev_args(host_port: u16) -> Vec<String> {
vec![
"-netdev".into(),
format!("user,id=n0,hostfwd=tcp:127.0.0.1:{host_port}-:{LIVE_SSH_GUEST_PORT}"),
"-device".into(),
"virtio-net-pci,netdev=n0".into(),
]
}
/// The honest console line naming the forward's address — emitted the moment the
/// guest really starts booting, so the operator reads `127.0.0.1:<port>` off the
/// pane, never out of the source. A pick-next-free rebind says so LOUDLY.
pub fn ssh_forward_notice(host_port: u16) -> String {
let rebound = if host_port == LIVE_SSH_HOST_PORT {
String::new()
} else {
format!(" ({LIVE_SSH_HOST_PORT} was taken)")
};
format!(
"[dwarves] ssh forward: host 127.0.0.1:{host_port} -> guest {LIVE_SSH_GUEST_PORT}{rebound}"
)
}
/// Pick the forward's host port: [`LIVE_SSH_HOST_PORT`], or the next free port in the
/// bounded [`LIVE_SSH_PORT_TRIES`] window. Exhausting the window fails BY NAME.
pub fn pick_live_ssh_host_port() -> Result<u16, String> {
pick_free_loopback_port(LIVE_SSH_HOST_PORT, LIVE_SSH_PORT_TRIES)
}
/// The bounded walk itself: probe-bind each candidate on `127.0.0.1` and take the
/// first that binds. The probe listener is dropped before QEMU starts, so a TOCTOU
/// window exists — and QEMU then fails by name on ITS bind rather than rebinding,
/// which keeps the no-silent-rebind rule either way.
pub fn pick_free_loopback_port(base: u16, tries: u16) -> Result<u16, String> {
let end = base.saturating_add(tries.saturating_sub(1));
for port in base..=end {
if std::net::TcpListener::bind(("127.0.0.1", port)).is_ok() {
return Ok(port);
}
}
Err(format!(
"no free ssh-forward host port on 127.0.0.1: {base}-{end} are all taken \
(bounded walk, {tries} tries) — free one, or cancel the boot holding it"
))
}
/// **Which console WIDGET the result area renders** — the honest mode the pane is in.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConsoleMode {
/// A text console draining serial (vm/iso) or container stdout (container) lines.
Serial,
/// A GRAPHICAL framebuffer console — a KVM cell configured WITH a GPU exposes a
/// real virtio-gpu framebuffer, so the pane shows the VM's screen, captured live via
/// QMP `screendump` (see [`FramebufferCapture`]) and uploaded as an egui texture.
/// Before the first frame the pane shows an honest placeholder over the live serial
/// (never a faked graphical frame).
Graphical,
}
impl ConsoleMode {
/// Stable label for `state_json` / the pane chip.
pub fn as_str(self) -> &'static str {
match self {
ConsoleMode::Serial => "serial",
ConsoleMode::Graphical => "graphical",
}
}
}
/// **The KVM cell's hardware configuration** — a scratch DISK (always) and an
/// OPTIONAL GPU. Threaded into the jera `BootSpec`'s `extra_qemu_args` (as extra
/// `-drive`/`-device` tokens) by [`TunnrBootLauncher`], and surfaced in the cell UI as
/// a toggle. The GPU toggle also selects the RESULT console: GPU on → a graphical
/// framebuffer console ([`ConsoleMode::Graphical`]); GPU off → the serial-text console
/// ([`ConsoleMode::Serial`]) the distroless appliance streams BOOT-OK over.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HwConfig {
/// Attach a scratch data disk (a virtio-blk `-drive`). Always on for a KVM cell —
/// a VM always has a disk device — but the size is configurable.
pub disk: bool,
/// Scratch disk size in MiB (a sparse raw file created pure-Rust, no `qemu-img`).
pub disk_mb: u32,
/// Attach a GPU (`-device virtio-gpu-pci`). OFF for the headless serial appliance;
/// ON exposes a real framebuffer and switches the result console to graphical.
pub gpu: bool,
/// For an **installer ISO** cell: after `INSTALLER: INSTALL-OK`, ACTUALLY reboot the
/// KVM guest OFF the installed disk (the owner — the reboot leg must EXECUTE, not just
/// print). The flow lays a real FAT ESP (`EFI/BOOT/BOOTX64.EFI` = the PRODUCTION
/// `korp-boot` first stage, which then chainloads `\vmlinuz` = the `korp-start`
/// second-boot payload) into the installed disk's p1 in place, then boots a SECOND QEMU
/// firmware-boot off that disk (cdrom detached), gating on `APP-ALIVE` — so the pane
/// SHOWS korp come up. `true` by default (the live demo). Set `false` for the
/// install-only fallback proof (INSTALL-OK, no second boot) so that guaranteed-green
/// leg is decoupled from second-boot flakiness. Ignored for non-installer cells.
pub reboot_into_installed: bool,
/// **The live-ISO ssh forward's host port**, already picked
/// ([`pick_live_ssh_host_port`]) by the spawn that announced it — `Some(p)` wires
/// `hostfwd=tcp:127.0.0.1:<p>-:`[`LIVE_SSH_GUEST_PORT`] onto the leg's user netdev
/// so the operator can `git clone` INTO the live-booted appliance. `None` (the
/// default, and every non-iso leg today) keeps the plain outbound-only user net.
/// LIVE-ISO ONLY for now: the installer's first boot runs the installer (no sshd),
/// and the reboot-into-installed leg would advertise gunnar's 2222 for products
/// that never declared a service port — wire those when the descriptor grows a
/// per-variant service-port token.
pub ssh_fwd_host_port: Option<u16>,
}
impl Default for HwConfig {
fn default() -> Self {
// A disk device always; GPU/graphics console ON by default (the owner) — the demo's
// gfx wow-moment is the default, not opt-in, so a presenter clicking ▶ live lands
// straight on the framebuffer. The checkbox stays toggleable (turn it off for a
// pure serial boot); the copyable serial text is always shown alongside the frame.
// reboot-into-installed ON by default so an installer boot really reboots into korp.
HwConfig {
disk: true,
disk_mb: 64,
gpu: true,
reboot_into_installed: true,
// No forward by default — the iso-kind spawn picks a real port and fills
// this in, announcing it; nothing is forwarded silently.
ssh_fwd_host_port: None,
}
}
}
impl HwConfig {
/// The console mode this hardware selects: a GPU → graphical framebuffer, else the
/// serial-text console.
pub fn console_mode(&self) -> ConsoleMode {
if self.gpu {
ConsoleMode::Graphical
} else {
ConsoleMode::Serial
}
}
}
/// **One decoded framebuffer frame** captured from a running guest's GPU — raw RGBA8
/// pixels the pane uploads as an egui texture (the OS-less appliance's real screen).
/// Pure bytes, no egui dep here (the UI does the `ColorImage` conversion). Produced by
/// the QMP `screendump` capture in [`TunnrBootLauncher`] for a GPU (graphical) boot;
/// every other console produces none (serial cells keep the honest text placeholder).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Frame {
/// Width in pixels.
pub width: usize,
/// Height in pixels.
pub height: usize,
/// Row-major RGBA8 pixels (`width * height * 4` bytes).
pub rgba: Vec<u8>,
}
/// **The one console sink** the pane's [`pump`] drains: a **cumulative** captured log
/// and a **non-blocking** phase. Output-neutral — the SAME trait backs BOTH the QEMU
/// SERIAL console (vm/iso, over tunnr's `BootHandle` in [`TunnrBootLauncher`]) AND a
/// container's STDOUT/LOGS ([`LogConsole`], over the jera→draupnir OCI engine) — so one
/// [`pump`] + one [`BootRun`] drive every launch kind, and a robot test faking either
/// proves the whole path. Faked in tests (replaying a captured serial/log).
pub trait SerialConsole: Send {
/// The serial captured SO FAR (cumulative — safe to poll repeatedly).
fn log(&self) -> String;
/// The current phase (non-blocking).
fn phase(&self) -> BootPhase;
/// Tear the guest down (idempotent).
fn kill(&mut self);
/// The latest GRAPHICAL framebuffer frame, if this console has one (a GPU boot).
/// Default `None` — serial cells, containers, and fakes produce no frame, so the
/// pane keeps its honest placeholder rather than a fabricated screen. NEVER faked.
fn frame(&self) -> Option<Frame> {
None
}
}
/// Produces a live boot for a launch cell. The real impl ([`TunnrBootLauncher`])
/// builds the appliance PID-1 and boots it through tunnr; a test injects a fake.
/// `Err(reason)` names a missing precondition — the pane surfaces it, never a fake boot.
pub trait BootLauncher: Send + Sync {
/// Build the cell's payload + start booting it, returning a live console.
fn launch(&self, cell_name: &str, repo_root: &Path) -> Result<Box<dyn SerialConsole>, String>;
/// Boot the cell WITH a hardware config (a scratch disk + an optional GPU) and,
/// for an installer or iso-kind variant, **the medium the finished build
/// produced**. The default
/// ignores both and delegates to [`launch`](Self::launch) — so a fake/replay
/// launcher keeps working unchanged; the real [`TunnrBootLauncher`] overrides this
/// to thread the disk/GPU into the jera `BootSpec`'s `extra_qemu_args` and to boot
/// `medium` itself.
///
/// `medium` is `None` for every variant that produces no bootable artifact (a vm
/// cell, a scaffold ISO) and for a boot asked for before anything was built. It is NOT a
/// hint: when it names a file, that file is what boots. When it does not, the
/// launcher says so on the console — see [`TunnrBootLauncher::boot_installer`].
fn launch_hw(
&self,
cell_name: &str,
repo_root: &Path,
_hw: &HwConfig,
_medium: Option<&Path>,
) -> Result<Box<dyn SerialConsole>, String> {
self.launch(cell_name, repo_root)
}
}
/// Produces a live LOG console for a `[[build.container]]` cell — the container mirror
/// of [`BootLauncher`]. The real impl ([`DraupnirContainerLauncher`], behind the
/// `container-run` feature) RUNS the image through jera→draupnir's ONE bollard/OCI
/// engine and follows its stdout; without the feature the honest stub reports it is not
/// wired (never fakes logs). A robot test injects a fake that replays a captured log.
/// `Err(reason)` names a missing precondition — the pane surfaces it, never a fake run.
pub trait ContainerLauncher: Send + Sync {
/// Run the cell's container + start following its logs, returning a live console.
fn launch(&self, cell_name: &str, repo_root: &Path) -> Result<Box<dyn SerialConsole>, String>;
}
/// The CODED error for a launch that could not be STARTED, routed by launch kind: a
/// container RUN that never came up is `dwarves-launch-2`; a vm/iso BOOT that never came
/// up is `dwarves-launch-5`. Both carry the launcher's own reason verbatim as the human
/// half, so the operator text is unchanged and the code is additive.
fn launch_start_error(
kind: LaunchKind,
cell: &str,
reason: &str,
) -> dwarves::errcode::DwarvesError {
match kind {
LaunchKind::Container => {
dwarves::dwarves_err!(launch, 2, "cannot start container {cell}: {reason}")
}
_ => dwarves::dwarves_err!(launch, 5, "cannot start {cell}: {reason}"),
}
}
/// The CODED error for a run that was CANCELLED (`dwarves-launch-6`) — a kill is never a
/// passed gate, and a robot tells "cancelled" from "failed to start" by the code.
pub fn cancelled_error(cell: &str) -> dwarves::errcode::DwarvesError {
dwarves::dwarves_err!(launch, 6, "{cell}: the run was cancelled")
}
/// One in-pane boot in flight (or the last that finished): a worker thread builds the
/// PID-1 then boots it, draining the live serial into `console` line by line.
pub struct BootRun {
/// The launch cell being booted/run (e.g. `korp-kvm`, `tunnr-hello-world`).
pub cell: String,
/// Which launch kind this run is — the axis the pane routes the console on.
kind: LaunchKind,
/// The KVM hardware config this run was launched with (disk + GPU); default for a
/// container/iso. Drives the [`ConsoleMode`] the result area renders.
hw: HwConfig,
console: Arc<Mutex<Vec<String>>>,
phase: Arc<Mutex<BootPhase>>,
/// The latest GPU framebuffer frame (graphical boots only), copied out of the live
/// console by [`pump`]; `None` for serial/container runs. The pane uploads it as an
/// egui texture — the appliance's real screen.
frame: Arc<Mutex<Option<Frame>>>,
kill: Arc<AtomicBool>,
_handle: Option<std::thread::JoinHandle<()>>,
}
impl BootRun {
/// Spawn the build-and-boot worker for a `kind=vm` `cell`, driven by `launcher`
/// with the DEFAULT hardware (a scratch disk, no GPU → serial console).
pub fn spawn(cell: &str, repo_root: PathBuf, launcher: Arc<dyn BootLauncher>) -> BootRun {
Self::spawn_hw(cell, repo_root, launcher, HwConfig::default())
}
/// Spawn a `kind=vm`/`kind=iso` boot worker WITH a hardware config. The intro/phase
/// wording matches the kind; the disk/GPU are threaded into the QEMU boot by the
/// launcher's [`launch_hw`](BootLauncher::launch_hw).
pub fn spawn_hw(
cell: &str,
repo_root: PathBuf,
launcher: Arc<dyn BootLauncher>,
hw: HwConfig,
) -> BootRun {
let kind = LaunchKind::Vm; // vm/iso share this ctor; iso uses spawn_iso below.
Self::spawn_with(
cell,
kind,
hw,
boot_intro(cell, kind, None),
None,
Duration::from_secs(120),
{
let (name, root, hw) = (cell.to_string(), repo_root, hw);
move || launcher.launch_hw(&name, &root, &hw, None)
},
)
}
/// **Spawn an INSTALLER boot off the medium a finished build produced.**
///
/// The one constructor that carries `medium`, and the reason it exists: until
/// 2026-08-16 an installer cell went through [`spawn_hw`](Self::spawn_hw), which
/// knows nothing about artifacts, and the launcher below therefore re-mastered a
/// korp-shaped STAND-IN into its own temp root and booted that. Driving holger, a
/// `BYGG` that failed and produced no artifact at all still reached
/// `INSTALLER: INSTALL-OK` — and gunnar's, korp's and holger's captured
/// framebuffers came home **md5-identical** (`32b65f91ac759010ecfcbd4d7ce88b11`,
/// six frames each), because it was one image every time.
///
/// `medium` is `Outcome::artifact` — the `.iso` `compose_installer_row` wrote at
/// `<repo>/.iso-out/<cell>.iso`. `None` means the operator hit ▶ with no successful
/// build behind it; the launcher then says STAND-IN on the console rather than
/// quietly booting something else.
pub fn spawn_installer(
cell: &str,
repo_root: PathBuf,
launcher: Arc<dyn BootLauncher>,
hw: HwConfig,
medium: Option<PathBuf>,
) -> BootRun {
let kind = LaunchKind::Iso;
// Composing nothing, but installing a real 64 GiB-declared target and then
// rebooting off it: the install itself is seconds, the second boot is the long
// pole, and a timeout that kills the guest mid-reboot reports a failure the
// product did not have.
Self::spawn_with(
cell,
kind,
hw,
boot_intro(cell, kind, medium.as_deref()),
// No ssh forward on the installer legs (see HwConfig::ssh_fwd_host_port):
// the install boot runs the installer, not the product's sshd.
None,
Duration::from_secs(900),
{
let (name, root, hw, m) = (cell.to_string(), repo_root, hw, medium);
move || launcher.launch_hw(&name, &root, &hw, m.as_deref())
},
)
}
/// Spawn a `kind=iso` boot worker WITH a hardware config and **the medium the
/// finished build composed**: firmware-boot it under OVMF, streaming the SAME
/// serial console as a vm — and, when the GPU toggle is on, the SAME live QMP
/// framebuffer graphics console. Threads the disk/GPU through `launch_hw` so the
/// pane's GPU toggle reaches the launcher.
///
/// `medium` is `Outcome::artifact` — the self-contained ESP `.iso` BYGG composed
/// (gap #7: until 2026-08-20 this constructor carried no medium at all, so a
/// composed iso-kind variant like gunnar-appliance-iso was refused as unwired and
/// offered the efistub stand-in). `None` means ▶ was hit with no successful build
/// behind it; the intro then announces the stand-in rather than staying silent.
pub fn spawn_iso(
cell: &str,
repo_root: PathBuf,
launcher: Arc<dyn BootLauncher>,
hw: HwConfig,
medium: Option<PathBuf>,
) -> BootRun {
let kind = LaunchKind::Iso;
// Pick the ssh-forward host port BEFORE anything boots: the forward rides the
// hardware config into the launcher's netdev (the real leg wires
// `hostfwd=tcp:127.0.0.1:<p>-:2222`, see `wire_live_ssh_forward`), and the
// console announces the address the moment the guest really starts. An
// exhausted pick window fails the run BY NAME before the launcher runs —
// never a silent rebind, never a forward-less boot pretending otherwise.
let picked = pick_live_ssh_host_port();
let (hw, notice) = match &picked {
Ok(p) => (
HwConfig {
ssh_fwd_host_port: Some(*p),
..hw
},
Some(ssh_forward_notice(*p)),
),
Err(_) => (hw, None),
};
Self::spawn_with(
cell,
kind,
hw,
boot_intro(cell, kind, medium.as_deref()),
notice,
Duration::from_secs(300),
{
let (name, root, hw, m) = (cell.to_string(), repo_root, hw, medium);
move || {
let _port = picked?; // no free port in the window → fail by name
launcher.launch_hw(&name, &root, &hw, m.as_deref())
}
},
)
}
/// Spawn a `kind=container` RUN worker: run the OCI image through the container
/// engine and follow its stdout into a [`LogConsole`], draining it via the SAME
/// [`pump`]. Ends on the container's readiness/exit (exit 0 → passed).
pub fn spawn_container(
cell: &str,
repo_root: PathBuf,
launcher: Arc<dyn ContainerLauncher>,
) -> BootRun {
let (kind, hw) = (LaunchKind::Container, HwConfig::default());
Self::spawn_with(
cell,
kind,
hw,
container_intro(cell),
None,
Duration::from_secs(120),
{
let (name, root) = (cell.to_string(), repo_root);
move || launcher.launch(&name, &root)
},
)
}
/// The shared worker: emit the intro lines, run `produce` to open a live
/// [`SerialConsole`] sink (or surface its error), then [`pump`] it to a terminal
/// phase. One body for boot/iso/container so the console contract is identical.
/// `notice` is an extra honest line pushed only once the console is REALLY open
/// (today: the iso leg's ssh-forward address) — never printed for a boot that
/// failed to start.
fn spawn_with<F>(
cell: &str,
kind: LaunchKind,
hw: HwConfig,
intro: Vec<String>,
notice: Option<String>,
timeout: Duration,
produce: F,
) -> BootRun
where
F: FnOnce() -> Result<Box<dyn SerialConsole>, String> + Send + 'static,
{
let console = Arc::new(Mutex::new(Vec::new()));
let phase = Arc::new(Mutex::new(BootPhase::Building));
let frame = Arc::new(Mutex::new(None));
let kill = Arc::new(AtomicBool::new(false));
let (c, p, fr, k, name) = (
console.clone(),
phase.clone(),
frame.clone(),
kill.clone(),
cell.to_string(),
);
let handle = std::thread::Builder::new()
.name(format!("dwarves-run-{cell}"))
.spawn(move || {
{
let mut g = c.lock().unwrap();
for line in intro {
g.push(line);
}
}
let mut src = match produce() {
Ok(s) => s,
Err(e) => {
// A launcher that cannot START is CODED (dwarves::errcode) — the
// code rides both the console line a human reads and the Failed
// phase a robot reads, so the failure is keyed on the code and not
// on this wording. `dwarves-launch-2` for a container run;
// `dwarves-launch-5` for a vm/iso boot that never came up.
let coded = launch_start_error(kind, &name, &e);
c.lock()
.unwrap()
.push(format!("[dwarves] {}", coded.log_line()));
*p.lock().unwrap() = BootPhase::Failed(coded.to_string());
return;
}
};
*p.lock().unwrap() = BootPhase::Booting;
{
let mut g = c.lock().unwrap();
g.push(running_line(&name, kind));
if let Some(n) = notice {
g.push(n);
}
}
// A GPU boot captures the live framebuffer → let the pump linger for the
// real final frame after a terminal phase (see `FRAME_GRACE`).
pump(src.as_mut(), &k, &c, &p, &fr, timeout, hw.gpu);
})
.ok();
BootRun {
cell: cell.to_string(),
kind,
hw,
console,
phase,
frame,
kill,
_handle: handle,
}
}
/// The launch kind (the pane routes its console widget on this).
pub fn kind(&self) -> LaunchKind {
self.kind
}
/// The hardware config this run was launched with (disk + GPU).
pub fn hw(&self) -> HwConfig {
self.hw
}
/// Which console widget the result area should render for this run.
pub fn console_mode(&self) -> ConsoleMode {
match self.kind {
LaunchKind::Vm | LaunchKind::Iso => self.hw.console_mode(),
// A container is always a text LOG window (no framebuffer).
LaunchKind::Container => ConsoleMode::Serial,
}
}
/// The current phase.
pub fn phase(&self) -> BootPhase {
self.phase.lock().unwrap().clone()
}
/// The console lines captured so far (the dwarves-visible serial view).
pub fn log(&self) -> Vec<String> {
self.console.lock().unwrap().clone()
}
/// The latest GPU framebuffer frame, if this is a graphical boot that has produced
/// one — the pane uploads it as an egui texture (the appliance's real screen).
/// `None` for serial/container runs and before the first frame arrives.
pub fn frame(&self) -> Option<Frame> {
self.frame.lock().unwrap().clone()
}
/// Whether the boot is still in flight.
pub fn is_running(&self) -> bool {
self.phase().is_running()
}
/// Whether the boot passed (the BOOT-OK marker streamed past).
pub fn passed(&self) -> bool {
self.phase() == BootPhase::Passed
}
/// **The stable error CODE for a failed run** ([`dwarves::errcode`]) — `None` while
/// running or once passed. Read straight out of the `Failed(reason)` phase, whose
/// reason string carries the code (every failure path in this module raises a coded
/// error), with `dwarves-launch-5` as the honest fallback for a failure that reached
/// terminal without one. This is the field a robot asserts instead of matching the
/// failure prose.
pub fn error_code(&self) -> Option<&'static str> {
match self.phase() {
BootPhase::Failed(reason) => Some(
dwarves::errcode::parse(&reason)
.map(|e| e.code)
.unwrap_or("dwarves-launch-5"),
),
_ => None,
}
}
/// The full dual-channel error for a failed run — the code AND the human reason.
pub fn error(&self) -> Option<dwarves::errcode::DwarvesError> {
let BootPhase::Failed(reason) = self.phase() else {
return None;
};
let code = self.error_code()?;
let e = dwarves::errcode::lookup(code)?;
Some(dwarves::errcode::DwarvesError::new(
e.subsystem,
e.n,
e.code,
reason,
))
}
/// Request cancellation — the guest is powered off and the run ends FAILED
/// (never mistaken for a passed smoke gate).
pub fn cancel(&self) {
self.kill.store(true, Ordering::SeqCst);
}
}
/// The console line that says, BEFORE anything boots, WHICH medium is about to boot —
/// the composed one or the stand-in.
///
/// It is a line and not a comment because silence is exactly what let the stand-in hide:
/// a pane that boots a korp-shaped scaffold and a pane that boots the product's own ISO
/// looked identical on screen for months. Whichever is true, the operator reads it here.
pub const STANDIN_NOTICE: &str = "[dwarves] STAND-IN: no composed medium — booting the \
korp-installer efistub SCAFFOLD, which is NOT this product. Run BYGG first.";
/// The intro lines a vm/iso boot worker emits before the launcher opens the console.
/// `medium` is the artifact a finished build produced, when there is one.
fn boot_intro(cell: &str, kind: LaunchKind, medium: Option<&Path>) -> Vec<String> {
match kind {
LaunchKind::Iso => {
let mut lines = vec![format!("[dwarves] ▶ Build & Boot ISO: {cell}")];
match medium.filter(|p| p.is_file()) {
Some(p) => {
let bytes = std::fs::metadata(p).map(|m| m.len()).unwrap_or(0);
lines.push(format!(
"[dwarves] medium: {} ({bytes} bytes) — the artifact BYGG composed",
p.display()
));
}
None => lines.push(STANDIN_NOTICE.to_string()),
}
lines
}
_ => vec![
format!("[dwarves] ▶ Build & Boot: {cell}"),
format!("[dwarves] building the appliance PID-1 for {cell}…"),
],
}
}
/// The intro lines a container RUN worker emits before the engine brings the image up.
fn container_intro(cell: &str) -> Vec<String> {
vec![
format!("[dwarves] ▶ Run container: {cell}"),
format!("[dwarves] bringing up the OCI image for {cell} (jera→draupnir) …"),
]
}
/// The "now streaming" line, once the console is open — worded per launch kind.
fn running_line(cell: &str, kind: LaunchKind) -> String {
match kind {
LaunchKind::Vm => format!("[dwarves] booting {cell} under QEMU/OVMF/KVM…"),
LaunchKind::Iso => format!("[dwarves] firmware-booting {cell} .iso under QEMU/OVMF…"),
LaunchKind::Container => format!("[dwarves] following {cell} container logs…"),
}
}
/// **The guest size an installer boot and its reboot get**, overriding
/// `BootSpec::smoke`'s 512 MiB.
///
/// The whole payload lives in RAM twice on the second boot — korp-start reads the
/// `.znippy` off the partition, extracts it into a memfd and `fexecve`s that — so the
/// floor is set by the product, not by the installer. MEASURED 2026-08-16 driving korp,
/// whose appliance binary is 236 518 296 B packed to 65 395 644 B: the install ran to
/// INSTALL-OK, the reboot reached korp-start's handoff, and the guest then died with
/// `Kernel panic - not syncing: System is deadlocked on memory` at 1.46 s. gunnar
/// (23 MB) and holger (32 MB) fit 512 MiB and never showed it.
///
/// 2 GiB is not a fix for an unbounded payload; it is a number large enough for every
/// appliance in this constellation with the extraction copy on top, on a box where the
/// guest is transient. A payload that outgrows it should say so, and it will — loudly,
/// on the serial, exactly as korp's did.
#[cfg(feature = "vm-boot")]
const INSTALL_GUEST_MB: u32 = 2048;
/// How long a GRAPHICAL boot's pump lingers after a terminal phase, still capturing the
/// framebuffer, to land the appliance's real final screen before returning (the guest
/// stays alive in its hlt loop; a fast INSTALL-OK can beat the QMP screendump warm-up).
const FRAME_GRACE: Duration = Duration::from_secs(8);
/// **The ONE console pump.** Drain `src`'s cumulative serial into `console` line by
/// line as it grows, until the boot passes (BOOT-OK), fails, is cancelled, or times
/// out; set `phase` terminally. Returns `true` iff the boot passed. Kept a free fn
/// over the [`SerialConsole`] trait so the real tunnr boot AND a fake replay drive
/// the SAME pump — a regression that drops a line or swallows the marker goes red.
/// `graphical` = a GPU boot: the pump lingers (up to [`FRAME_GRACE`]) after a terminal
/// phase until a real framebuffer frame is captured, so the pane shows the true screen.
pub fn pump(
src: &mut dyn SerialConsole,
kill: &AtomicBool,
console: &Arc<Mutex<Vec<String>>>,
phase: &Arc<Mutex<BootPhase>>,
frame: &Arc<Mutex<Option<Frame>>>,
timeout: Duration,
graphical: bool,
) -> bool {
let start = Instant::now();
let mut sent = 0usize;
// Copy the live framebuffer out of the console into the shared slot the pane reads,
// throttled (the guest's screen changes on human timescales, the QMP capture already
// paces itself ~2 fps) so a multi-MB frame is not memcpy'd every 20 ms serial poll.
let mut last_frame_poll = start - Duration::from_secs(1);
// Latch a terminal phase but DON'T return yet on a graphical boot until a real frame
// has been captured — a fast install (INSTALL-OK ~1.4s) can finish before the QMP
// screendump warms up, and the guest stays alive in its hlt loop afterwards, so we
// keep capturing to show the appliance's REAL final screen (never the placeholder).
let mut terminal: Option<bool> = None;
let mut terminal_at = start;
loop {
drain_tail(src, &mut sent, console);
if last_frame_poll.elapsed() >= Duration::from_millis(200) {
if let Some(fr) = src.frame() {
*frame.lock().unwrap() = Some(fr);
}
last_frame_poll = Instant::now();
}
if kill.load(Ordering::SeqCst) {
src.kill();
drain_tail(src, &mut sent, console);
*phase.lock().unwrap() = BootPhase::Failed(cancelled_error("run").to_string());
return false;
}
// Resolve (or re-confirm) the phase. Once terminal, hold the result while a
// graphical boot waits out the frame-capture grace window.
if terminal.is_none() {
match src.phase() {
BootPhase::Passed => {
drain_tail(src, &mut sent, console);
*phase.lock().unwrap() = BootPhase::Passed;
terminal = Some(true);
terminal_at = Instant::now();
}
BootPhase::Failed(e) => {
drain_tail(src, &mut sent, console);
*phase.lock().unwrap() = BootPhase::Failed(e);
terminal = Some(false);
terminal_at = Instant::now();
}
_ => {}
}
}
if let Some(passed) = terminal {
// A serial boot returns at once. A graphical boot lingers for the WHOLE grace
// window (guest still in its hlt loop, QMP still up), the loop above copying
// the latest screendump each tick — so the pane ends on the genuine FINAL
// screen (the splash), never an early mid-boot frame captured before it drew.
let grace_over = terminal_at.elapsed() > FRAME_GRACE;
if !graphical || grace_over {
return passed;
}
}
if start.elapsed() > timeout {
src.kill();
drain_tail(src, &mut sent, console);
*phase.lock().unwrap() =
BootPhase::Failed(dwarves::dwarves_err!(launch, 5, "boot timed out").to_string());
return false;
}
std::thread::sleep(Duration::from_millis(20));
}
}
/// Emit the serial lines that appeared since the last drain (the cumulative-log tail).
fn drain_tail(src: &mut dyn SerialConsole, sent: &mut usize, console: &Arc<Mutex<Vec<String>>>) {
let full = src.log();
let lines: Vec<&str> = full.lines().collect();
if lines.len() > *sent {
let mut c = console.lock().unwrap();
for l in &lines[*sent..] {
c.push((*l).to_string());
}
*sent = lines.len();
}
}
// ── The REAL launcher: boot the appliance through tunnr (feature `vm-boot`). ─────────
/// The real boot launcher — builds the appliance PID-1 and boots it under
/// QEMU/OVMF/KVM through tunnr's `boot_test` primitive, streaming its live serial.
/// Behind the `vm-boot` feature (it links `tunnr-vm`); the no-feature build ships the
/// honest stub below (Build & Boot then reports the feature is off — never a fake boot).
#[cfg(feature = "vm-boot")]
pub struct TunnrBootLauncher;
// ── The installer's PATH answers, in BOTH configurations ────────────────────────
//
// These three are pure `std` path + env math: no `tunnr_vm`, no `fatfs`, no `jera`.
// They sat inside the `vm-boot` impl below purely by placement, and that gate
// CONTRADICTED `installer_root`'s own doc, which calls it "a public seam so a robot
// e2e can read the installed target disk back afterwards" — a seam a default build
// could not reach. `robot_gunnar_installer.rs` is compiled in EVERY build (three of
// its tests are not `#[ignore]`d and are meant to run without QEMU), so the gate was
// a hard `E0599: no associated function named installer_disk_path` on a plain
// `cargo test -p dwarves-gui`, i.e. that whole test binary never compiled.
//
// Ungated `impl` on a type that is declared once per configuration: one writer for
// "where does the installer stage", answered identically whichever half is linked.
impl TunnrBootLauncher {
/// The per-cell installer staging root
/// (`<tmp>/dwarves-installer-<cell>-<pid>`) — public seam so a robot e2e can read the
/// installed target disk back afterwards. The **process id** isolates concurrent boots
/// of the SAME cell across separate test binaries (cargo runs integration tests in
/// parallel), so two e2e processes never race on the same `target.raw` (a QEMU write
/// lock). Read-back stays correct: the test and the boot share the process, so the PID
/// matches.
pub fn installer_root(cell_name: &str) -> PathBuf {
Self::installer_scratch().join(format!(
"dwarves-installer-{cell_name}-{}",
std::process::id()
))
}
/// Where the installer boot's scratch lives. `DWARVES_INSTALLER_SCRATCH` wins;
/// otherwise the temp dir, as before.
///
/// The knob exists because a composed medium installs onto the DECLARED target size
/// — gunnar declares 64 GiB — and on this box `/tmp` is a 100 GiB tmpfs, i.e. RAM.
/// The image is sparse, so only what the installer writes is ever paid for, but a
/// FAT format of a 47 GiB data partition writes real tables and there is no reason
/// to hold them in memory. Set it to a path on a real disk for a real install.
fn installer_scratch() -> PathBuf {
std::env::var_os("DWARVES_INSTALLER_SCRATCH")
.map(PathBuf::from)
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(std::env::temp_dir)
}
/// The blank target disk the installer writes its GPT + install signature onto —
/// `<installer_root>/target.raw`. A robot e2e reads this back to prove the REAL
/// on-disk install (GPT/signature), not just the serial claim.
pub fn installer_disk_path(cell_name: &str) -> PathBuf {
Self::installer_root(cell_name).join("target.raw")
}
}
#[cfg(feature = "vm-boot")]
impl TunnrBootLauncher {
/// Map a launch-cell name to its real bootable appliance (the PID-1 the boot rides).
fn appliance_for(cell_name: &str) -> Result<tunnr_vm::Appliance, String> {
Ok(match cell_name {
"korp-kvm" | "korp" | "korp-iso" => tunnr_vm::Appliance::korp(),
"demo-container-kvm" | "demo-container" | "demo-container-iso" => {
tunnr_vm::Appliance::demo_container()
}
"tunnr-osless-server" | "tunnr-osless-client" | "tunnr" => tunnr_vm::Appliance::tunnr(),
other => {
// The list SHRANK on 2026-08-20 (gap #7): korp-iso / demo-container-iso
// were named "bootable" here while the scaffold gate refused them, and
// a COMPOSED iso-kind variant no longer arrives here at all — it boots
// its own medium through the live leg. This refusal is now only the
// no-medium case, so it says what actually helps: run BYGG.
return Err(dwarves::dwarves_err!(
launch,
7,
"no real appliance wired for launch cell '{other}' \
(bootable: korp-kvm, demo-container-kvm, tunnr-osless-*; an iso-kind \
variant boots the medium BYGG composed — run BYGG first)"
)
.to_string());
}
})
}
/// Fail-safe preconditions shared by the vm + iso boot — name exactly what is
/// missing (QEMU / OVMF), never a fabricated boot.
/// **PREFLIGHT** the genuine host runtime deps for the KVM *test view* (the appliance
/// itself needs none; ISO mastering is toolchain-free via the embedded stub). Emits a
/// FAT, actionable, NAMED message — the missing tool + the EXACT install command — so
/// the user instantly knows what to install, never a raw/cryptic failure (brief
/// §FALLBACK). Checked BEFORE any master/boot work runs.
fn require_boot_host() -> Result<(), String> {
if !tunnr_vm::qemu_available() {
return Err(format!(
"PREFLIGHT: MISSING QEMU on this host (looked for `{}`) — the KVM test view \
needs it. Install with:\n Debian/Ubuntu: sudo apt install qemu-system-x86\n \
Fedora/RHEL: sudo dnf install qemu-system-x86\n(or set TUNNR_QEMU_BIN to a \
qemu-system-x86_64 binary). The mastered ISO itself needs no host toolchain.",
tunnr_vm::qemu_binary()
));
}
if tunnr_vm::locate_ovmf().is_none() {
return Err(
"PREFLIGHT: MISSING OVMF/UEFI firmware on this host — the KVM test view boots \
under UEFI. Install with:\n Debian/Ubuntu: sudo apt install ovmf\n \
Fedora/RHEL: sudo dnf install edk2-ovmf\n(expects OVMF_CODE_4M.fd + \
OVMF_VARS_4M.fd under /usr/share/OVMF or /usr/share/edk2)."
.to_string(),
);
}
Ok(())
}
/// Create a **scratch raw disk** as a sparse file (pure-Rust `set_len`, no
/// `qemu-img`, no subprocess) and return the `-drive` token attaching it as a
/// virtio-blk device — the "a disk device always" half of the KVM hardware config.
fn scratch_disk_arg(cell: &str, mb: u32) -> Result<String, String> {
let path = std::env::temp_dir().join(format!("dwarves-scratch-{cell}.raw"));
let f = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&path)
.map_err(|e| format!("scratch disk create {}: {e}", path.display()))?;
f.set_len((mb as u64) * 1024 * 1024)
.map_err(|e| format!("scratch disk size {}: {e}", path.display()))?;
Ok(format!("file={},if=virtio,format=raw", path.display()))
}
/// The extra QEMU tokens the hardware config threads into `boot_test` — a scratch
/// virtio disk (always) and an optional `virtio-gpu-pci` device (the GPU toggle).
fn hw_qemu_args(cell: &str, hw: &HwConfig) -> Result<Vec<String>, String> {
let mut args = Vec::new();
if hw.disk {
args.push("-drive".into());
args.push(Self::scratch_disk_arg(cell, hw.disk_mb)?);
}
if hw.gpu {
// A real framebuffer device — the guest gets a GPU. The pane renders its
// LIVE screen via a QMP `screendump` capture (see `boot_vm`), a genuine
// frame off this device, never a faked one.
args.push("-device".into());
args.push("virtio-gpu-pci".into());
}
Ok(args)
}
/// The per-cell QMP control-socket path (`<tmp>/dwarves-qmp-<cell>.sock`) — the
/// channel the [`FramebufferCapture`] drives `screendump` over for a GPU boot.
fn qmp_sock_path(cell: &str) -> PathBuf {
std::env::temp_dir().join(format!("dwarves-qmp-{cell}.sock"))
}
/// Boot a `kind=vm` appliance under QEMU/OVMF/KVM with the hardware config. For a
/// GPU boot a QMP control socket is attached and a live [`FramebufferCapture`]
/// renders the guest's real screen into the pane (the serial console stays live too).
fn boot_vm(cell_name: &str, hw: &HwConfig) -> Result<Box<dyn SerialConsole>, String> {
let app = Self::appliance_for(cell_name)?;
Self::require_boot_host()?;
let initramfs = app.build_artifact()?;
let kernel = tunnr_vm::locate_kernel(&std::env::temp_dir())
.ok_or("no readable kernel — set TUNNR_KERNEL or make /boot/vmlinuz readable")?;
let mut spec = Self::guest_spec(
jera::vm::BootSpec::kernel_rootfs(
Self::guest_name(cell_name),
path_str(&kernel),
path_str(&initramfs),
),
&app.boot_ok_marker,
120,
);
spec.cmdline = format!("quiet {}", app.smoke_flag);
// Thread the KVM hardware config (scratch disk + optional GPU) into the QEMU
// args — always a disk, a GPU only when the cell's toggle is on.
spec.extra_qemu_args = Self::hw_qemu_args(cell_name, hw)?;
// A GPU boot gets a QMP control socket so we can `screendump` its framebuffer.
// server=on,wait=off ⇒ QEMU creates + listens and does NOT block on a client, so
// the boot proceeds whether or not the pane is watching.
let qmp_sock = if hw.gpu {
let p = Self::qmp_sock_path(cell_name);
let _ = std::fs::remove_file(&p); // clear a stale socket from a prior run
spec.extra_qemu_args.push("-qmp".into());
spec.extra_qemu_args
.push(format!("unix:{},server=on,wait=off", p.display()));
Some(p)
} else {
None
};
let proc = Self::boot_through_jera(&spec, "boot spawn failed")?;
// Start the live framebuffer capture only for a GPU boot.
let fb = qmp_sock.map(|sock| FramebufferCapture::start(sock, cell_name.to_string()));
Ok(Box::new(HandleConsole::new(proc, fb)))
}
/// The guest name jera/draupnir know this boot by (the `Machine` id prefix).
fn guest_name(cell_name: &str) -> String {
format!("dwarves-{cell_name}")
}
/// The pane's guest defaults on a fresh jera spec: the success marker the run
/// gates on, its wall-clock budget, and the 2 vCPUs every boot here always had
/// (tunnr's smoke default; jera's `kernel_rootfs` says 1).
fn guest_spec(
mut spec: jera::vm::BootSpec,
marker: &str,
timeout_secs: u64,
) -> jera::vm::BootSpec {
spec.cores = 2;
spec.success_marker = marker.to_string();
spec.timeout_secs = timeout_secs;
spec
}
/// **The one boot call in this file**: hand the spec to jera's VM seam
/// (`default_vm_runner` = `DraupnirVmBootRunner` under `vm-tunnr` → draupnir's
/// `KvmBoot` → `tunnr_vm::boot_test`) and get back the live [`jera::boot::BootProc`]
/// the [`HandleConsole`] drains. `what` names the leg in the spawn error.
fn boot_through_jera(
spec: &jera::vm::BootSpec,
what: &str,
) -> Result<Box<dyn jera::boot::BootProc>, String> {
jera::vm::default_vm_runner()
.boot(spec)
.map_err(|e| format!("{what}: {e}"))
}
/// Master a bootable ISO around the appliance PID-1 (pure-Rust `PureRustIsoBuilder`,
/// no `xorriso`) and firmware-boot it under OVMF off `-cdrom`, streaming the SAME
/// serial console. Reuses the `nornir-build-thing` ISO masterer + tunnr's `-cdrom`
/// boot path (the proven `iso_boot.rs` primitives).
///
/// Since 2026-08-20 (gap #7) this is the NO-MEDIUM leg only: an iso-kind variant
/// with a composed medium routes through [`boot_live_iso`](Self::boot_live_iso) and
/// never arrives here.
fn boot_iso(cell_name: &str, repo_root: &Path) -> Result<Box<dyn SerialConsole>, String> {
use nornir_build_thing::iso::{IsoBuild, IsoFormat};
use nornir_build_thing::{build_iso, PureRustIsoBuilder};
let app = Self::appliance_for(cell_name)?;
// HONEST scaffold gate (host-independent, checked FIRST). A *live-boot* ISO
// (demo-container-iso, korp-iso) needs a UEFI Linux loader INSIDE the ESP
// (`EFIBOOT.IMG`) — systemd-boot / GRUB EFI / an EFI-stub bzImage — to hand
// OVMF's firmware control to the kernel+initramfs. No such loader exists in the
// tree yet: both efistub fixtures are terminal (serial-marker / LBA-0 write), so
// the pure-Rust masterer would build a UEFI ISO with nothing in the ESP to boot
// the kernel (that surfaced as the cryptic `boot image 'EFIBOOT.IMG' was not
// staged` masterer error). We refuse HONESTLY here — a SCAFFOLD, never a faked
// BOOT-OK. korp-installer ISOs DO boot (their ESP carries a real, self-contained
// UEFI installer app); korp-kvm boots via QEMU `-kernel` (no ESP needed).
if iso_cell_is_bootable_scaffold(cell_name) {
return Err(iso_scaffold_reason(cell_name));
}
Self::require_boot_host()?;
// Build the appliance PID-1 initramfs + locate a kernel, then stage them into an
// ISO tree the pure-Rust masterer seals into a real `.iso`.
let initramfs = app.build_artifact()?;
let kernel = tunnr_vm::locate_kernel(&std::env::temp_dir())
.ok_or("no readable kernel — set TUNNR_KERNEL or make /boot/vmlinuz readable")?;
let staging = std::env::temp_dir().join(format!("dwarves-iso-{cell_name}"));
std::fs::create_dir_all(&staging)
.map_err(|e| format!("iso staging {}: {e}", staging.display()))?;
std::fs::copy(&kernel, staging.join("bzImage"))
.map_err(|e| format!("stage kernel: {e}"))?;
std::fs::copy(&initramfs, staging.join("rootfs.cpio.gz"))
.map_err(|e| format!("stage initramfs: {e}"))?;
let root = if repo_root.as_os_str().is_empty() {
std::env::temp_dir()
} else {
repo_root.to_path_buf()
};
let iso = IsoBuild {
name: cell_name.to_string(),
kernel: format!("dwarves-iso-{cell_name}/bzImage"),
rootfs: format!("dwarves-iso-{cell_name}/rootfs.cpio.gz"),
staging: format!("dwarves-iso-{cell_name}"),
volume_id: cell_name.to_uppercase().replace('-', "_"),
boot_params: vec!["console=ttyS0".into(), app.smoke_flag.clone()],
// UEFI so OVMF firmware-boots it; the ESP payload assembly (efistub) is the
// feat/pure-rust-iso work — a missing ESP errors honestly rather than faking.
format: IsoFormat::Uefi,
from_variant: None,
boot_logo: None,
// A SCAFFOLD ISO takes no per-site parameters and therefore records none.
// These two fields arrived on `IsoBuild` with the `--param` work and this
// initializer did not follow, because `vm-boot` is off by default and
// nothing compiled this path — the exact rot a feature nobody builds grows.
// Empty is the honest value here, not a placeholder: this cell is assembled
// from a stock kernel + initramfs to prove the boot chain, so there is no
// declared parameter to accept and no applied value to attest. A DECLARED
// installer cell (gunnar-installer, korp-installer) goes through
// `installer_build_for`, which carries the recipe's own params.
params: Vec::new(),
applied_params: Vec::new(),
// Same story for `fixtures` (nornir-build-thing 8331ff2, the declared
// `[build.iso.fixtures]` map): a scaffold ISO declares none.
fixtures: Default::default(),
// …and for the OPT-OUT axis (nornir-build-thing 0.1.8): a scaffold
// ISO is assembled from a stock kernel + initramfs to prove the boot
// chain, so there is no capability to leave out and no toggle to
// record. Empty is the honest value, not a placeholder — and this
// initializer is why the LAW says to build through the door: the
// whole path is behind `vm-boot`, which is off by default, so a
// plain `cargo test` compiles none of it and the field would have
// gone missing here again.
options: Vec::new(),
applied_options: Vec::new(),
// Every field the model grows after this literal was written (the consumption
// edges, the kernel donor) takes its default: a live ISO scaffold consumes nothing.
..Default::default()
};
let base = std::env::temp_dir();
// Master relative to a temp root so the staging path resolves; land the .iso
// under <base>/.iso-out/<cell>.iso.
let out = build_iso(&PureRustIsoBuilder::new(), &iso, cell_name, &base).map_err(|e| {
format!(
"pure-Rust ISO master failed for '{cell_name}': {e:#} \
(a bootable UEFI ESP payload is the feat/pure-rust-iso work — not faked)"
)
})?;
let _ = root;
// `Artifact.path` is repo-root-relative (or absolute); resolve it under `base`.
let iso_path = out
.artifacts
.first()
.map(|a| {
let p = PathBuf::from(&a.path);
if p.is_absolute() {
p
} else {
base.join(p)
}
})
.unwrap_or_else(|| base.join(".iso-out").join(format!("{cell_name}.iso")));
// Firmware-boot the .iso: jera's LIVE-medium shape (no kernel, no target disk)
// is draupnir's `iso_boot`, which tunnr renders `-cdrom … -boot d`.
let spec = Self::guest_spec(
jera::vm::BootSpec::live_iso(Self::guest_name(cell_name), path_str(&iso_path)),
&app.boot_ok_marker,
180,
);
let proc = Self::boot_through_jera(&spec, "iso boot spawn failed")?;
Ok(Box::new(HandleConsole::new(proc, None)))
}
/// Whether `cell_name` names a **disk-installer** ISO cell (korp-installer). An
/// installer is NOT a live-boot ISO: it masters through
/// [`nornir_build_thing::InstallerBuild`], attaches a **blank target disk**, and
/// gates on `INSTALLER: INSTALL-OK` (the install proof) rather than an appliance's
/// `BOOT-OK`. Routed to [`boot_installer`](Self::boot_installer), never through
/// `appliance_for` (an installer has no `tunnr_vm::Appliance`).
fn is_installer_cell(cell_name: &str) -> bool {
cell_name == "korp-installer"
|| cell_name.ends_with("-installer")
|| cell_name.contains("-installer-")
}
/// The [`ContainerBackend`](nornir_build_thing::ContainerBackend) a FAT installer
/// cell selects, decoded from its name suffix (`korp-installer-fat-youki` → Youki),
/// or `None` for a **LEAN** installer (`korp-installer` / `…-installer-lean`). This
/// is the seam that makes the atom matrix REAL: each cell masters a DISTINCT,
/// content-addressed installer that bakes its own `korp.appliance=…` selector, never
/// one indistinguishable ISO for every row.
fn backend_for_cell(cell_name: &str) -> Option<nornir_build_thing::ContainerBackend> {
use nornir_build_thing::ContainerBackend as B;
let n = cell_name.to_ascii_lowercase();
if n.contains("youki") {
Some(B::Youki)
} else if n.contains("podman") {
Some(B::Podman)
} else if n.contains("containerd") {
Some(B::Containerd)
} else if n.contains("microvm") {
Some(B::MicroVm)
} else {
None // lean (or an unqualified korp-installer)
}
}
/// Materialize the [`InstallerBuild`](nornir_build_thing::InstallerBuild) for a
/// launch cell, **honoring the discovered `[[build.iso]]` descriptor** (its
/// `volume_id` + declared `boot_params`) and the FAT backend its name selects — so
/// the ⚒ pane masters the cell the repo actually declared, not a hardcoded stand-in.
/// A missing/undiscoverable descriptor falls back to name-derived defaults (keeps a
/// bare inline-TOML cell working). `target_disk_gib` stays small (1) for a fast KVM
/// proof; the GPU mirrors the cell's hardware toggle.
fn installer_for_cell(
cell_name: &str,
repo_root: &Path,
hw: &HwConfig,
) -> nornir_build_thing::InstallerBuild {
use nornir_build_thing::{BuildVariantSet, InstallerBuild};
// The variant's OWN declaration first, and WHOLE. A `[[build.installer]]` lands
// in `set.installers`, not `set.isos` — reading only the ISO list meant gunnar's
// declared `volume_id = "GUNNAR_INSTALL"` and its `boot_params` were silently
// replaced by the name-derived defaults below, and the pane booted a medium the
// recipe never described. It is CLONED rather than rebuilt field-by-field for
// the same reason: a cell reassembled from two of its fields drops
// `target_disk_gib`, `variant`, `cmd`, `env` and every future one silently, and
// the drop looks exactly like a declaration nobody made. The `[[build.iso]]`
// lookup stays as the second door (an installer declared as an ISO row), and the
// derived default stays as the third, for a bare inline-TOML row.
let set = BuildVariantSet::discover(repo_root).ok().flatten();
let declared: Option<InstallerBuild> = set.as_ref().and_then(|s| {
s.installers
.iter()
.find(|i| i.name == cell_name)
.cloned()
.or_else(|| {
s.isos
.iter()
.find(|i| i.name == cell_name)
.map(|i| InstallerBuild {
name: i.name.clone(),
volume_id: i.volume_id.clone(),
boot_params: i.boot_params.clone(),
..Default::default()
})
})
});
let mut cell = declared.unwrap_or_else(|| InstallerBuild {
name: cell_name.to_string(),
volume_id: cell_name.to_uppercase().replace('-', "_"),
boot_params: vec!["console=ttyS0".to_string()],
..Default::default()
});
cell.container_backend = cell
.container_backend
.or_else(|| Self::backend_for_cell(cell_name));
cell.gpu = hw.gpu;
cell
}
/// **The product the installed disk will start** — the `korp.entry=` token off the
/// cell's own effective boot params, which is what `korp-start` resolves inside the
/// payload and what it prints as `argv[0]` right before `fexecve`. `None` for a cell
/// that declares none (the scaffold; `compose_installer_row` refuses to compose one).
fn entry_of(cell: &nornir_build_thing::InstallerBuild) -> Option<String> {
cell.effective_boot_params()
.iter()
.find_map(|t| t.strip_prefix("korp.entry=").map(str::to_string))
}
/// **Master the korp INSTALLER ISO and boot it under QEMU/OVMF with a BLANK target
/// disk attached, gating on `INSTALLER: INSTALL-OK`.** The in-pane peer of
/// korp-installer's `tests/kvm_install.rs`, reusing the SAME seams (never twinning
/// their logic): the ISO is mastered 100% pure-Rust through
/// [`InstallerBuild::installer_iso`](nornir_build_thing::InstallerBuild::installer_iso)
/// → [`PureRustIsoBuilder`](nornir_build_thing::PureRustIsoBuilder); the blank disk
/// is attached via [`InstallerBuild::kvm_disk_args`]; the boot rides tunnr's
/// `boot_test` primitive (empty kernel ⇒ OVMF firmware-boots the `-cdrom`); and the
/// success marker is [`InstallerBuild::success_marker`] (`INSTALLER: INSTALL-OK`),
/// so the SAME [`pump`] latches BOOT PASS the instant the installer reports a done
/// install. The bootable ESP payload is korp-installer's own UEFI stub fixture,
/// compiled by the Rust compiler for `x86_64-unknown-uefi` — a missing fixture /
/// target / QEMU / OVMF surfaces honestly (never a fabricated INSTALL-OK).
fn boot_installer(
cell_name: &str,
repo_root: &Path,
hw: &HwConfig,
medium: Option<&Path>,
) -> Result<Box<dyn SerialConsole>, String> {
Self::require_boot_host()?;
// A per-cell staging root (mirrors kvm_install.rs's tempdir). Deterministic per
// cell so a robot test can read the installed target disk back.
let root = Self::installer_root(cell_name);
std::fs::create_dir_all(&root)
.map_err(|e| format!("installer staging {}: {e}", root.display()))?;
let installer = Self::installer_for_cell(cell_name, repo_root, hw);
// ── WHICH MEDIUM ────────────────────────────────────────────────────────────
// The one the build composed, when there is one. It is opened here and nowhere
// else re-mastered: `compose_installer_row` already built the real appliance
// binary, packed it with `pack_znippy` under the cell's `korp.entry=`, bundled a
// real kernel, built `korp-boot` + both musl PID-1s and sealed the ISO. Booting
// anything else after that is booting a different product.
let composed = medium.filter(|p| p.is_file());
let (iso_path, disk_bytes) = match composed {
Some(p) => (p.to_path_buf(), installer.target_disk_bytes()),
// The SCAFFOLD, and it says so on the console (`STANDIN_NOTICE`) before it
// boots. Kept reachable because a `[[build.iso]]` scaffold cell and a ▶
// pressed before any BYGG have nothing else to show — but never silent.
None => (
Self::master_standin_iso(cell_name, &root, &installer)?,
Self::STANDIN_DISK_BYTES_INNER,
),
};
// A BLANK writable target disk at the DECLARED size (raw sparse, pure Rust — no
// qemu-img). gunnar declares 64 GiB; the file is sparse, so only the bytes the
// installer actually writes cost anything.
let disk_path = Self::installer_disk_path(cell_name);
Self::create_blank_disk(&disk_path, disk_bytes)?;
// Boot the ISO with the blank disk attached; gate on INSTALL-OK. jera's INSTALL
// shape (iso + disk, no kernel) is draupnir's `kvm_install_from_medium`: the
// blank target is the spec's own image — first virtio drive, so it is `vda` —
// with `persist_disk` (the installer's writes SURVIVE), and the medium rides
// `-cdrom … -boot d`. Only the GPU half of the installer's QEMU args is still
// ours to add (the disk half would be a duplicate `-drive`).
let disk_str = disk_path
.to_str()
.ok_or("target disk path is not valid UTF-8")?;
let mut spec = Self::guest_spec(
jera::vm::BootSpec::install_from_medium(
Self::guest_name(cell_name),
path_str(&iso_path),
disk_str,
),
installer.success_marker(),
300,
);
spec.mem_mb = INSTALL_GUEST_MB;
spec.extra_qemu_args = installer.kvm_gpu_args();
// **THE COMPOSED MEDIUM RIDES VIRTIO TOO, and that is not belt-and-braces.**
// OVMF boots El Torito off the `-cdrom`, but the composed medium's PID-1 finds
// its payloads by probing block devices for the ISO-9660 `CD001` magic, and the
// appliance kernel carries no ATA/AHCI/SCSI at all — there is no `sr0` in that
// guest. MEASURED in gunnar's own drive (`installtest/tests/installer_drive.rs`,
// 2026-08-15): cdrom-only died with `INSTALLER: volume: no block device carries
// the ISO-9660 magic (CD001) … (probed: loop…, vda)`. The target is attached
// FIRST (vda) and the ISO second (vdb), read-only, so `hwdetect` can never pick
// the medium as the install target. Same shape, same writer, as
// `korp_installer::isoproof::attach_iso_as_virtio` — that harness is a test-only
// dev-dependency of the products, not something the pane links.
if composed.is_some() {
spec.extra_qemu_args.push("-drive".into());
spec.extra_qemu_args.push(format!(
"file={},if=virtio,format=raw,readonly=on",
iso_path.display()
));
}
// A GPU boot attaches a QMP control socket + a live FramebufferCapture — the SAME
// graphics-console wiring as `boot_vm`, so the medium's REAL screen renders in the
// pane, not just serial text. server=on,wait=off ⇒ the boot never blocks on a QMP
// client.
let fb = if hw.gpu {
let sock = Self::qmp_sock_path(cell_name);
let _ = std::fs::remove_file(&sock);
spec.extra_qemu_args.push("-qmp".into());
spec.extra_qemu_args
.push(format!("unix:{},server=on,wait=off", sock.display()));
Some(sock)
} else {
None
};
let proc = Self::boot_through_jera(&spec, "installer boot spawn failed")?;
let fb = fb.map(|sock| FramebufferCapture::start(sock, cell_name.to_string()));
let install_console = HandleConsole::new(proc, fb);
// ★ the owner: after INSTALL-OK, ACTUALLY reboot the guest OFF the installed disk
// and watch the product come up (default on). The install-only path (reboot off)
// is the guaranteed-green fallback leg — INSTALL-OK, no second boot, decoupled
// from any second-boot flakiness.
if hw.reboot_into_installed {
let plan = RebootPlan::for_medium(composed.is_some(), &installer);
Ok(Box::new(RebootConsole::start(
cell_name,
install_console,
disk_path,
*hw,
plan,
)))
} else {
Ok(Box::new(install_console))
}
}
/// **Master the korp-installer efistub SCAFFOLD** — the medium there is when the
/// build produced none.
///
/// It is a real, self-contained UEFI ISO: the committed `korp-installer-efistub`
/// blob, which EMBEDS its own demo `app.znippy`, streams it onto p2 via Block I/O
/// and blake3-verifies the read-back. What it is not is any product: it lays korp's
/// demo payload down whatever cell asked for it, which is why every caller of this
/// function announces [`STANDIN_NOTICE`] on the console first.
fn master_standin_iso(
cell_name: &str,
root: &Path,
installer: &nornir_build_thing::InstallerBuild,
) -> Result<PathBuf, String> {
use nornir_build_thing::{build_iso, iso_output_path, PureRustIsoBuilder};
let efi_app = Self::build_installer_stub(root)?;
let staging = root.join("iso-root");
std::fs::create_dir_all(&staging)
.map_err(|e| format!("installer iso-root {}: {e}", staging.display()))?;
Self::build_esp(&staging.join("efiboot.img"), &efi_app)?;
let iso = installer.installer_iso();
build_iso(&PureRustIsoBuilder::new(), &iso, "korp", root)
.map_err(|e| format!("pure-Rust installer ISO master failed: {e:#}"))?;
let iso_path = iso_output_path(root, &iso);
if !iso_path.is_file() {
return Err(format!(
"mastered installer ISO not found at {}",
iso_path.display()
));
}
let _ = cell_name;
Ok(iso_path)
}
// ── reboot-into-korp: the SECOND boot off the installed disk (the owner, ★P0) ──────────
/// The **PRODUCTION first-stage loader** `korp-boot`
/// (`tunnr/crates/korp-installer/boot/korp-boot-efi`), embedded via `include_bytes!`
/// (regenerable with `cargo xtask gen-boot-stage1`).
///
/// This is what the reboot flow writes as the installed ESP's `EFI/BOOT/BOOTX64.EFI`,
/// and it is the whole point of the second boot: until it existed, dwarves laid a
/// *terminal* stub there — a PE that printed a marker and halted — so the "reboot into
/// korp" leg proved only that the firmware can start ONE image. The stand-in WAS the
/// second boot; the chainload it was supposed to stand in for never happened.
///
/// `korp-boot` is the real chain: it locates its own ESP through
/// `EFI_LOADED_IMAGE_PROTOCOL` (no NVRAM `Boot####` entry needed), reads `\korp\cmdline`,
/// `LoadImage`s `\vmlinuz` on a device path rooted at THAT ESP (the form that keeps
/// `DeviceHandle` on the ESP, so a Linux EFI stub can still resolve `initrd=`), installs
/// the command line as UCS-2 `LoadOptions`, prints `KORP-BOOT: HANDOFF -> kernel` and
/// `StartImage`s. Every failure is a loud `KORP-BOOT: FAILED …` + halt — it never falls
/// through to something that resembles success.
const EMBEDDED_BOOT_STAGE1: &'static [u8] = include_bytes!("embedded/korp-boot-stage1.efi");
/// The `korp-start` second-boot UEFI payload, **embedded** into the dwarves binary via
/// `include_bytes!` (regenerable via `cargo xtask gen-start-stub`). The reboot flow lays
/// THESE bytes onto the installed disk's FAT ESP as **`\vmlinuz`** — the image the
/// first stage chainloads, standing in for the appliance kernel — so the second boot
/// needs **zero host toolchain** (the same self-contained charter as the installer
/// stub). Once started it reads the installed app.znippy back off korp-data, checks the
/// root-of-trust payload, and emits `APP-ALIVE`.
const EMBEDDED_START_STUB: &'static [u8] = include_bytes!("embedded/korp-start-bootx64.efi");
/// The `korp-live` run-from-RAM UEFI stub, **embedded** (regen: `cargo xtask
/// gen-live-stub`). The live-boot flow lays THESE bytes on a bootable ISO's ESP so the
/// ISO runs korp straight from RAM (install NOTHING) — deployment type #3 (live-ISO /
/// airgap / tamper-evident). Same zero-host-toolchain charter as the other stubs.
const EMBEDDED_LIVE_STUB: &'static [u8] = include_bytes!("embedded/korp-live-bootx64.efi");
/// The blake3 root-of-trust pin of the demo `app.znippy` the installer streams onto p2
/// (= `blake3(korp-installer-efistub::APP_ZNIPPY)`; verified equal in the gfx e2e). The
/// reboot flow writes this as the `korp/app.blake3` FILE on the FAT ESP (the pin the
/// real korp-start reads on every boot) so the installed ESP carries the honest layout.
const DEMO_APP_BLAKE3_HEX: &'static str =
"4236942f847ab29eb0a0f37ce16a3e3f7dbf0622381aeddcf644693771d99442";
/// The installed ESP's on-disk span, matching the installer efistub's fixed geometry:
/// p1 (ESP) starts at LBA 34 (after the protective MBR + GPT header + 128-entry array)
/// and is 1 MiB (2048 × 512-byte sectors). The reboot flow overlays a real FAT ESP over
/// exactly this span, leaving LBA 0/1/2..33 (MBR/GPT) and p2 (korp-data) intact.
const ESP_FIRST_LBA: u64 = 34;
const ESP_SPAN_BYTES: u64 = 1024 * 1024;
/// Cheap "is this really a UEFI PE?" check (`MZ` … `PE\0\0`) — mirrors
/// `korp_installer::firststage::is_uefi_pe`. A firmware handed a non-PE simply refuses
/// to boot with no diagnostic at all, so a corrupt embedded blob must fail LOUDLY here,
/// on the host, rather than as a black screen in the guest.
fn is_uefi_pe(bytes: &[u8]) -> bool {
if bytes.len() < 0x40 || &bytes[..2] != b"MZ" {
return false;
}
let off = u32::from_le_bytes([bytes[0x3C], bytes[0x3D], bytes[0x3E], bytes[0x3F]]) as usize;
bytes.len() >= off + 4 && &bytes[off..off + 4] == b"PE\0\0"
}
/// Build the **FAT EFI System Partition image** the second boot boots off — the
/// `korp_installer::esp::korp_esp_files` layout, laid pure-Rust with `fatfs`, in
/// memory, zero shell:
///
/// ```text
/// /EFI/BOOT/BOOTX64.EFI ← korp-boot, the PRODUCTION first stage (what firmware runs)
/// /vmlinuz ← the image the first stage chainloads
/// /korp/cmdline ← the command line it installs as UCS-2 LoadOptions
/// /korp/app.blake3 ← the root-of-trust pin korp-start verifies against
/// ```
///
/// **The fake green this closes.** BOOTX64.EFI used to be the `korp-start` stub itself:
/// a terminal PE that printed `APP-ALIVE` and halted. The e2e went green on a chain that
/// was one link long — the stand-in *was* the whole second boot, and the loader it stood
/// in for did not exist. Now the firmware runs `korp-boot`, and `korp-start` sits BEHIND
/// it as `\vmlinuz`, so the second boot exercises the real `LoadImage`/`StartImage`
/// chainload: `KORP-BOOT: chainload \vmlinuz via=device-path` → `HANDOFF -> kernel` →
/// `APP-ALIVE`. On a real appliance `\vmlinuz` is a Linux EFI-stub kernel whose initramfs
/// runs `/korp-start`; here the KVM stand-in occupies the kernel slot directly (a second
/// 755 KiB copy under `/korp-start` would not fit the 1 MiB ESP and would prove nothing
/// extra — the *chainload*, not the payload, is what was missing).
fn build_reboot_esp_bytes() -> Result<Vec<u8>, String> {
use std::io::{Cursor, Write as _};
let stage1 = Self::EMBEDDED_BOOT_STAGE1;
if !Self::is_uefi_pe(stage1) {
return Err(
"embedded korp-boot-stage1.efi is not a UEFI PE image — the committed \
first-stage loader is corrupt; regenerate with `cargo xtask gen-boot-stage1`"
.to_string(),
);
}
let start_efi = Self::EMBEDDED_START_STUB;
if !Self::is_uefi_pe(start_efi) {
return Err(
"embedded korp-start bootx64.efi is not a UEFI PE image — the committed stub \
is corrupt; regenerate with `cargo xtask gen-start-stub`"
.to_string(),
);
}
let need = stage1.len() + start_efi.len();
if need as u64 >= Self::ESP_SPAN_BYTES {
return Err(format!(
"the first stage ({} B) + \\vmlinuz ({} B) do not fit the installed ESP's \
{} B span — the installer's p1 geometry and these payloads have drifted",
stage1.len(),
start_efi.len(),
Self::ESP_SPAN_BYTES
));
}
let mut image = Cursor::new(vec![0u8; Self::ESP_SPAN_BYTES as usize]);
fatfs::format_volume(
&mut image,
fatfs::FormatVolumeOptions::new().volume_label(*b"KORP-ESP "),
)
.map_err(|e| format!("format reboot ESP FAT: {e}"))?;
{
let fs = fatfs::FileSystem::new(&mut image, fatfs::FsOptions::new())
.map_err(|e| format!("mount reboot ESP: {e}"))?;
{
let root = fs.root_dir();
let boot = root
.create_dir("EFI")
.and_then(|d| d.create_dir("BOOT"))
.map_err(|e| format!("reboot ESP EFI/BOOT: {e}"))?;
// The firmware's removable-media fallback runs THIS: the production first
// stage, not the payload. It is what makes the next line matter.
let mut f = boot
.create_file("BOOTX64.EFI")
.map_err(|e| format!("reboot ESP BOOTX64.EFI: {e}"))?;
f.write_all(stage1)
.map_err(|e| format!("write BOOTX64.EFI: {e}"))?;
f.flush().ok();
// …and THIS is what the first stage chainloads off the same ESP.
let mut k = root
.create_file("vmlinuz")
.map_err(|e| format!("reboot ESP vmlinuz: {e}"))?;
k.write_all(start_efi)
.map_err(|e| format!("write vmlinuz: {e}"))?;
k.flush().ok();
// The pin the real korp-start reads (honest installed-ESP layout).
let korp = root
.create_dir("korp")
.map_err(|e| format!("reboot ESP korp/: {e}"))?;
let mut pin = korp
.create_file("app.blake3")
.map_err(|e| format!("reboot ESP korp/app.blake3: {e}"))?;
pin.write_all(Self::DEMO_APP_BLAKE3_HEX.as_bytes())
.map_err(|e| format!("write app.blake3: {e}"))?;
pin.flush().ok();
let mut cmd = korp
.create_file("cmdline")
.map_err(|e| format!("reboot ESP korp/cmdline: {e}"))?;
cmd.write_all(b"console=ttyS0 korp.appliance=lean")
.map_err(|e| format!("write cmdline: {e}"))?;
cmd.flush().ok();
}
fs.unmount()
.map_err(|e| format!("unmount reboot ESP: {e}"))?;
}
Ok(image.into_inner())
}
/// Overlay the FAT ESP image over the installed disk's p1 span IN PLACE — a seek+write
/// at `ESP_FIRST_LBA × 512`, leaving the protective MBR/GPT and p2 (korp-data) intact.
/// After this the disk is a genuinely bootable korp appliance: OVMF's removable-media
/// fallback (`\EFI\BOOT\BOOTX64.EFI`) runs the `korp-boot` first stage on the next boot,
/// which chainloads `\vmlinuz` off the same ESP.
fn overlay_reboot_esp(disk_path: &Path) -> Result<(), String> {
use std::io::{Seek, SeekFrom, Write as _};
let esp = Self::build_reboot_esp_bytes()?;
let mut f = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(disk_path)
.map_err(|e| format!("open installed disk {}: {e}", disk_path.display()))?;
f.seek(SeekFrom::Start(Self::ESP_FIRST_LBA * 512))
.map_err(|e| format!("seek to p1 ESP offset: {e}"))?;
f.write_all(&esp)
.map_err(|e| format!("overlay FAT ESP into p1: {e}"))?;
f.flush()
.map_err(|e| format!("flush installed disk: {e}"))?;
Ok(())
}
/// Boot a SECOND QEMU firmware-boot OFF the installed disk (no cdrom): OVMF runs the
/// `korp-boot` first stage off the FAT ESP, which chainloads `\vmlinuz` → korp comes up
/// → `APP-ALIVE`. Mirrors the GPU +
/// QMP wiring of the installer boot so the pane's graphics console shows korp's running
/// splash. A distinct QMP socket avoids colliding with the (now dead) installer boot.
fn boot_installed_disk(
cell_name: &str,
disk_path: &Path,
hw: &HwConfig,
marker: &str,
) -> Result<HandleConsole, String> {
// Empty kernel ⇒ firmware boot; `rootfs_or_disk` is a .raw disk ⇒ build_qemu_args
// emits ONE `-drive …,if=virtio,format=raw` and OVMF firmware-boots the ESP off it
// (the removable-media fallback \EFI\BOOT\BOOTX64.EFI = the korp-boot first stage,
// which then LoadImage/StartImage's \vmlinuz off that same ESP). No cdrom is
// attached, so this is genuinely "boot off the installed disk".
// jera's INSTALLED-disk shape (disk, no kernel, no medium) is draupnir's
// `kvm_boot_installed_disk`: persisted, `-boot c`, no cdrom.
let mut spec = Self::guest_spec(
jera::vm::BootSpec::installed_disk(
format!("{}-reboot", Self::guest_name(cell_name)),
path_str(disk_path),
),
marker,
240,
);
spec.mem_mb = INSTALL_GUEST_MB;
let fb_sock = if hw.gpu {
let sock = Self::qmp_sock_path(&format!("{cell_name}-reboot"));
let _ = std::fs::remove_file(&sock);
spec.extra_qemu_args.push("-device".into());
spec.extra_qemu_args.push("virtio-gpu-pci".into());
spec.extra_qemu_args.push("-qmp".into());
spec.extra_qemu_args
.push(format!("unix:{},server=on,wait=off", sock.display()));
Some(sock)
} else {
None
};
let proc = Self::boot_through_jera(&spec, "reboot-into-installed boot spawn failed")?;
let fb = fb_sock.map(|sock| FramebufferCapture::start(sock, format!("{cell_name}-reboot")));
Ok(HandleConsole::new(proc, fb))
}
// ── live-ISO run-from-RAM (deployment type #3, the owner 2b) ───────────────────────────
/// Whether a cell is a **live-ISO** run-from-RAM cell (`korp-live`, `*-live-iso`) — it
/// boots the ISO's ESP payload straight from RAM and installs NOTHING (no target disk),
/// the ephemeral / airgap / tamper-evident deployment. Distinct from an installer cell.
fn is_live_cell(cell_name: &str) -> bool {
cell_name.contains("-live")
}
/// **Firmware-boot a LIVE bootable ISO run-from-RAM.** The self-contained twin of
/// [`boot_installer`](Self::boot_installer), and since 2026-08-20 (gap #7) ALSO the
/// leg every composed iso-kind variant boots through:
///
/// - `medium = Some(file)` — **THE COMPOSED MEDIUM boots, verbatim.** BYGG's
/// isocompose output is already a self-contained El Torito ESP ISO (app.znippy
/// pinned, kernel bundled, ESP mastered), so nothing is re-mastered here — booting
/// anything else would be booting a different product. The gate is korp-start's
/// own exec-argv handoff ([`composed_handoff_marker`]), naming the declared
/// `korp.entry=` when there is one.
/// - `medium = None` — the stand-in: stage the embedded `korp-live` UEFI stub as
/// the ISO's ESP (`build_esp` → `efiboot.img`), master the SAME pure-Rust UEFI
/// ISO ([`InstallerBuild::installer_iso`] is generic over the ESP contents), and
/// gate on `APP-ALIVE` (korp came up, live).
///
/// Either way it firmware-boots under OVMF off `-cdrom` with **NO target disk** —
/// nothing is installed — with the SAME optional virtio-gpu + QMP graphics console.
/// A missing QEMU/OVMF surfaces via the preflight.
/// Wire the live-ISO leg's guest networking for the picked ssh forward: suppress
/// tunnr's default plain user NIC (its `NetConfig::User { hostfwd }` seam spells
/// `tcp::<h>-:<g>` — all interfaces, no bind address) and author the SAME
/// user-mode virtio-net netdev with a LOOPBACK-bound hostfwd instead
/// ([`live_ssh_netdev_args`]). Folding a bind address into tunnr's `NetConfig` is
/// the named follow-up; until then this leg owns its netdev line. The seam the
/// wiring test pins without QEMU.
fn wire_live_ssh_forward(spec: &mut jera::vm::BootSpec, host_port: u16) {
spec.net_none = true;
spec.extra_qemu_args.extend(live_ssh_netdev_args(host_port));
}
fn boot_live_iso(
cell_name: &str,
repo_root: &Path,
hw: &HwConfig,
medium: Option<&Path>,
) -> Result<Box<dyn SerialConsole>, String> {
use nornir_build_thing::{build_iso, iso_output_path, PureRustIsoBuilder};
Self::require_boot_host()?;
let (iso_path, marker) = match medium.filter(|p| p.is_file()) {
Some(composed) => {
// The medium BYGG composed — opened here and nowhere re-mastered. The
// declaration's `korp.entry=` (read off the variant's own descriptor)
// names what korp-start must hand control to.
let cell = Self::installer_for_cell(cell_name, repo_root, hw);
let marker = composed_handoff_marker(Self::entry_of(&cell).as_deref());
(composed.to_path_buf(), marker)
}
None => {
let root = Self::installer_root(cell_name);
std::fs::create_dir_all(&root)
.map_err(|e| format!("live iso staging {}: {e}", root.display()))?;
// 1. Materialize the embedded korp-live stub as the ESP boot payload
// (MZ-checked).
let bytes = Self::EMBEDDED_LIVE_STUB;
if bytes.len() < 2 || &bytes[0..2] != b"MZ" {
return Err(
"embedded korp-live bootx64.efi is not a PE (MZ) image — regenerate \
with `cargo xtask gen-live-stub`"
.to_string(),
);
}
let efi_app = root.join("bootx64.efi");
std::fs::write(&efi_app, bytes).map_err(|e| {
format!("write embedded live stub to {}: {e}", efi_app.display())
})?;
// 2. Stage the ISO tree: the ESP boot payload (no app.znippy — run-from-RAM).
let staging = root.join("iso-root");
std::fs::create_dir_all(&staging)
.map_err(|e| format!("live iso-root {}: {e}", staging.display()))?;
Self::build_esp(&staging.join("efiboot.img"), &efi_app)?;
// 3. Master the UEFI ISO for the discovered variant (its boot_params bake
// korp.mode=live).
let installer = Self::installer_for_cell(cell_name, repo_root, hw);
let iso = installer.installer_iso();
build_iso(&PureRustIsoBuilder::new(), &iso, "korp", &root)
.map_err(|e| format!("pure-Rust live ISO master failed: {e:#}"))?;
let iso_path = iso_output_path(&root, &iso);
if !iso_path.is_file() {
return Err(format!(
"mastered live ISO not found at {}",
iso_path.display()
));
}
(iso_path, "APP-ALIVE".to_string())
}
};
// 4. Firmware-boot the ISO off -cdrom with NO target disk; gate on the marker
// (jera's LIVE-medium shape → draupnir `iso_boot`).
let mut spec = Self::guest_spec(
jera::vm::BootSpec::live_iso(
format!("{}-live", Self::guest_name(cell_name)),
path_str(&iso_path),
),
&marker,
120,
);
// The ssh forward the spawn picked and announced: the guest's sshd (gunnar's
// convention, 0.0.0.0:2222) becomes dialable at 127.0.0.1:<p> on the host, so
// the operator can `git clone` INTO the live-booted appliance off the pane.
if let Some(p) = hw.ssh_fwd_host_port {
Self::wire_live_ssh_forward(&mut spec, p);
}
if medium.is_some() {
// A composed medium runs the product from RAM — the packed payload plus its
// memfd extraction copy — so it gets the same guest floor as the installer
// legs (see INSTALL_GUEST_MB: korp panicked at 512 MiB, MEASURED 2026-08-16).
spec.mem_mb = INSTALL_GUEST_MB;
}
let fb = if hw.gpu {
let sock = Self::qmp_sock_path(&format!("{cell_name}-live"));
let _ = std::fs::remove_file(&sock);
spec.extra_qemu_args.push("-device".into());
spec.extra_qemu_args.push("virtio-gpu-pci".into());
spec.extra_qemu_args.push("-qmp".into());
spec.extra_qemu_args
.push(format!("unix:{},server=on,wait=off", sock.display()));
Some(sock)
} else {
None
};
let proc = Self::boot_through_jera(&spec, "live iso boot spawn failed")?;
let fb = fb.map(|sock| FramebufferCapture::start(sock, cell_name.to_string()));
Ok(Box::new(HandleConsole::new(proc, fb)))
}
/// The blank target the SCAFFOLD installs onto. The efistub stub writes a GPT +
/// install signature and nothing else, so a declared 64 GiB would be 64 GiB of
/// nothing; the composed medium uses `target_disk_bytes()` instead.
const STANDIN_DISK_BYTES_INNER: u64 = 256 * 1024 * 1024;
/// The korp-installer UEFI stub, **embedded** into the dwarves binary via
/// `include_bytes!` — a `bootx64.efi` (PE32+ EFI application) pre-built ONCE from the
/// `korp-installer-efistub` fixture and committed, regenerable via
/// `cargo xtask gen-installer-stub`. Mastering the installer ISO writes THESE BYTES —
/// so it needs **zero host toolchain**: no `rustup target add x86_64-unknown-uefi`, no
/// `cargo build --target`, no shell, on ANY box (incl. the customer's). This is the
/// self-contained charter (brief §SELF-CONTAINED): the only genuine runtime deps are
/// QEMU + OVMF for the KVM *test view*, checked by an actionable preflight, never a
/// raw compiler error.
const EMBEDDED_INSTALLER_STUB: &'static [u8] =
include_bytes!("embedded/korp-installer-bootx64.efi");
/// Materialize the embedded installer stub as a `bootx64.efi` file under `root` (so
/// [`build_esp`](Self::build_esp) can place it on the ESP). Pure byte-write — no
/// toolchain, no cargo, no shell. The embedded blob is a valid PE32+ EFI application
/// (`MZ` header), verified here so a corrupt embed fails loudly, never a faked boot.
fn build_installer_stub(root: &Path) -> Result<PathBuf, String> {
let bytes = Self::EMBEDDED_INSTALLER_STUB;
if bytes.len() < 2 || &bytes[0..2] != b"MZ" {
return Err(
"embedded korp-installer bootx64.efi is not a PE (MZ) image — the committed \
stub is corrupt; regenerate with `cargo xtask gen-installer-stub`"
.to_string(),
);
}
let efi = root.join("bootx64.efi");
std::fs::write(&efi, bytes)
.map_err(|e| format!("write embedded installer stub to {}: {e}", efi.display()))?;
Ok(efi)
}
/// Format a FAT EFI System Partition carrying `EFI/BOOT/BOOTX64.EFI` — pure-Rust
/// `fatfs`, zero shell (mirrors `kvm_install.rs::build_esp`). This is the ESP the
/// UEFI ISO's `efiboot.img` boot image points at.
fn build_esp(path: &Path, efi_app: &Path) -> Result<(), String> {
use std::io::Write as _;
let f = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(|e| format!("create ESP {}: {e}", path.display()))?;
f.set_len(1440 * 1024)
.map_err(|e| format!("size ESP: {e}"))?;
fatfs::format_volume(
&f,
fatfs::FormatVolumeOptions::new().volume_label(*b"EFIESP "),
)
.map_err(|e| format!("format FAT ESP: {e}"))?;
let fs = fatfs::FileSystem::new(&f, fatfs::FsOptions::new())
.map_err(|e| format!("mount ESP: {e}"))?;
let boot = fs
.root_dir()
.create_dir("EFI")
.and_then(|d| d.create_dir("BOOT"))
.map_err(|e| format!("ESP EFI/BOOT: {e}"))?;
let mut out = boot
.create_file("BOOTX64.EFI")
.map_err(|e| format!("ESP BOOTX64.EFI: {e}"))?;
let bytes = std::fs::read(efi_app).map_err(|e| format!("read efi payload: {e}"))?;
out.write_all(&bytes)
.map_err(|e| format!("write BOOTX64.EFI: {e}"))?;
out.flush().map_err(|e| format!("flush BOOTX64.EFI: {e}"))?;
Ok(())
}
/// Create a blank, sparse, writable raw disk image of `size` bytes — pure-Rust
/// (`File::set_len`), no `qemu-img` (mirrors `kvm_install.rs::create_blank_disk`).
fn create_blank_disk(path: &Path, size: u64) -> Result<(), String> {
let f = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(|e| format!("create blank disk {}: {e}", path.display()))?;
f.set_len(size)
.map_err(|e| format!("size blank disk: {e}"))?;
Ok(())
}
}
#[cfg(feature = "vm-boot")]
impl BootLauncher for TunnrBootLauncher {
fn launch(&self, cell_name: &str, repo_root: &Path) -> Result<Box<dyn SerialConsole>, String> {
// The one router is `launch_hw` — this is the default-hardware, no-medium door.
self.launch_hw(cell_name, repo_root, &HwConfig::default(), None)
}
fn launch_hw(
&self,
cell_name: &str,
repo_root: &Path,
hw: &HwConfig,
medium: Option<&Path>,
) -> Result<Box<dyn SerialConsole>, String> {
// A LIVE cell (korp-live) firmware-boots run-from-RAM (no disk); an INSTALLER
// variant boots its composed medium (or the self-announcing stand-in) with a
// BLANK target disk, gating on INSTALL-OK; an ISO-KIND variant boots the medium
// BYGG composed through the SAME live leg ([`plan_iso_launch`], gap #7) and only
// falls to the honest stand-in refusal when there is no medium; everything else
// is a vm boot.
if Self::is_live_cell(cell_name) {
Self::boot_live_iso(cell_name, repo_root, hw, medium)
} else if Self::is_installer_cell(cell_name) {
Self::boot_installer(cell_name, repo_root, hw, medium)
} else if cell_name.ends_with("-iso") {
match plan_iso_launch(medium) {
IsoLaunchPlan::LiveComposed { medium } => {
Self::boot_live_iso(cell_name, repo_root, hw, Some(&medium))
}
IsoLaunchPlan::Standin => Self::boot_iso(cell_name, repo_root),
}
} else {
Self::boot_vm(cell_name, hw)
}
}
}
/// Adapts jera's live [`jera::boot::BootProc`] (the draupnir→tunnr KVM proc) to a
/// [`SerialConsole`] (feature `vm-boot`): `drain_log` is pumped into ONE cumulative
/// console string on every read, `poll` is the phase. For a GPU (graphical) boot it
/// also owns a live [`FramebufferCapture`] — a QMP `screendump` loop rendering the
/// guest's real screen — surfaced through [`frame`](SerialConsole::frame).
#[cfg(feature = "vm-boot")]
struct HandleConsole {
/// The live boot, behind a mutex because [`SerialConsole`] reads through `&self`
/// while jera's proc polls/drains through `&mut`.
proc: Mutex<Box<dyn jera::boot::BootProc>>,
/// The console so far — every line `drain_log` has handed out, newline-joined.
log: Mutex<String>,
/// The GPU framebuffer capture (graphical boots only); `None` for serial cells.
fb: Option<FramebufferCapture>,
}
#[cfg(feature = "vm-boot")]
impl HandleConsole {
fn new(proc: Box<dyn jera::boot::BootProc>, fb: Option<FramebufferCapture>) -> Self {
HandleConsole {
proc: Mutex::new(proc),
log: Mutex::new(String::new()),
fb,
}
}
/// Move the fresh console lines off the proc into the cumulative log.
fn pump(&self) {
let lines = self.proc.lock().unwrap().drain_log();
if lines.is_empty() {
return;
}
let mut log = self.log.lock().unwrap();
for line in lines {
log.push_str(&line);
log.push('\n');
}
}
/// **Let the guest finish writing before anything reads its disk.**
///
/// MEASURED 2026-08-16, gunnar, and it is the reason this exists: the install ran
/// to INSTALL-OK and streamed the payload onto p2 with `blake3=e3b150c78915…`, the
/// installer QEMU was SIGKILLed 4 s later, and the SECOND boot staged the same
/// 8 627 789 bytes back off `/dev/vda2` and hashed them to `b92123d1…` — same
/// length, different bytes, and the startup self-test refused the disk. QEMU's raw
/// drive is `cache=writeback`: guest writes sit in QEMU's own memory and a SIGKILL
/// throws them away. Three earlier runs were simply lucky.
///
/// So the install guest is asked to STOP rather than shot. `shutdown` returns as
/// soon as the process is gone — which for an installer is at once, because it
/// reboots itself after INSTALL-OK and tunnr passes `-no-reboot`, so QEMU exits and
/// flushes on its own — and falls back to ACPI power-down and then SIGKILL if the
/// guest is wedged. The KVM is always gone when this returns.
fn settle(&self, grace: Duration) {
if let Some(fb) = &self.fb {
fb.stop();
}
self.pump();
self.proc.lock().unwrap().shutdown(grace);
self.pump();
}
}
#[cfg(feature = "vm-boot")]
impl SerialConsole for HandleConsole {
fn log(&self) -> String {
self.pump();
self.log.lock().unwrap().clone()
}
fn phase(&self) -> BootPhase {
use jera::boot::BootStatus;
self.pump();
match self.proc.lock().unwrap().poll() {
BootStatus::BootedOk => BootPhase::Passed,
BootStatus::Booting | BootStatus::Running => BootPhase::Booting,
// tunnr's own words ride through draupnir → jera: "boot timed out" for the
// wall-clock backstop, the failure reason for a failed boot.
BootStatus::Failed(e) => BootPhase::Failed(e),
BootStatus::Killed => BootPhase::Failed("boot killed".to_string()),
}
}
fn kill(&mut self) {
if let Some(fb) = &self.fb {
fb.stop();
}
self.pump();
self.proc.lock().unwrap().kill();
self.pump();
}
fn frame(&self) -> Option<Frame> {
self.fb.as_ref().and_then(|fb| fb.latest())
}
}
// ── reboot-into-korp: the two-phase console (install → reboot → APP-ALIVE) ────────────
/// How long the reboot worker keeps the second (installed) boot ALIVE + capturing frames
/// after it reaches `APP-ALIVE`, so the pane lands korp's running splash (overlaps the
/// outer pump's [`FRAME_GRACE`]). Torn down early on cancel/drop.
#[cfg(feature = "vm-boot")]
const REBOOT_POST_PASS_LINGER: Duration = Duration::from_secs(22);
/// **The reboot-into-korp console** (the owner, ★P0): a [`SerialConsole`] that drives the
/// WHOLE two-phase story on its own worker thread — (1) the installer boot to
/// `INSTALLER: INSTALL-OK`, then (2) lay a real FAT ESP (`korp-start`) onto the installed
/// disk and boot a SECOND QEMU firmware-boot OFF that disk, gating on `APP-ALIVE` — while
/// presenting ONE cumulative console (install log → reboot narration → second-boot log)
/// and ONE phase that latches `Passed` only when korp actually comes up. The pane/pump
/// drain it exactly like any other console; a robot e2e latches the `APP-ALIVE` heartbeat.
#[cfg(feature = "vm-boot")]
struct RebootConsole {
log: Arc<Mutex<String>>,
phase: Arc<Mutex<BootPhase>>,
frame: Arc<Mutex<Option<Frame>>>,
kill: Arc<AtomicBool>,
_worker: Option<std::thread::JoinHandle<()>>,
}
/// **What the SECOND boot is, and it is two different things.**
///
/// A composed medium installs a whole boot chain of its own: a real `korp-boot` as
/// `\EFI\BOOT\BOOTX64.EFI`, a real EFI-stub `vmlinuz`, a real `korp-start`, the packed
/// payload on p2, the cmdline carrying `korp.entry=<product>`. Booting off that disk is
/// the thing the owner asked to be verified, and the ONLY thing the second boot has to do
/// is get out of the way.
///
/// The scaffold installs korp's demo payload, and dwarves lays its own embedded ESP over
/// p1 to give the firmware something to chainload. That overlay is a raw seek+write over
/// LBA 34 for 1 MiB — the efistub's fixed geometry — so pointing it at a REAL installed
/// disk destroys the ESP the installer just wrote and boots dwarves' own korp fixture
/// instead, which is how `APP-ALIVE {"app":"korp",…}` came back for gunnar and for
/// holger alike. The overlay is therefore bound to the scaffold and to nothing else.
#[cfg(feature = "vm-boot")]
#[derive(Clone, Debug, PartialEq, Eq)]
struct RebootPlan {
/// Lay dwarves' embedded FAT ESP over p1 before booting. Scaffold only.
overlay_standin_esp: bool,
/// The marker the second boot must reach for the run to pass.
marker: String,
/// The console line naming what is about to boot.
narration: String,
}
#[cfg(feature = "vm-boot")]
impl RebootPlan {
/// The plan for a real composed medium, or for the scaffold.
///
/// The real-medium marker is `korp-start`'s own `exec argv: ["<entry>"` line
/// (`korp-installer/src/bin/korp-start.rs:224`, argv[0] from
/// `cmdline::app_argv(entry, …)`). It is the last thing printed before `fexecve`
/// replaces the process, so reaching it proves the whole installed chain ran —
/// firmware → korp-boot → vmlinuz → korp-start → payload verified — AND it NAMES the
/// product. `APP-ALIVE` cannot: the only things in the tree that print it are korp's
/// own `korp-appliance` and the KVM fixture stubs, both with `"korp"` as a hardcoded
/// literal, so gating gunnar's reboot on it gates gunnar on korp.
fn for_medium(composed: bool, cell: &nornir_build_thing::InstallerBuild) -> RebootPlan {
if !composed {
return RebootPlan {
overlay_standin_esp: true,
marker: "APP-ALIVE".to_string(),
narration: "[dwarves] ── reboot (STAND-IN): overlaying dwarves' own korp ESP \
onto p1 and booting the korp demo fixture — NOT this product ──"
.to_string(),
};
}
// The marker itself is [`composed_handoff_marker`] — one spelling, shared with
// the iso-kind live boot, so the two composed gates cannot drift.
match TunnrBootLauncher::entry_of(cell) {
Some(entry) => RebootPlan {
overlay_standin_esp: false,
marker: composed_handoff_marker(Some(&entry)),
narration: format!(
"[dwarves] ── reboot: booting OFF the installed disk (cdrom detached, \
firmware walks to the ESP the installer wrote); waiting for korp-start \
to hand off to `{entry}` ──"
),
},
// A composed medium always declares `korp.entry=` (compose_installer_row
// refuses without it). If one ever does not, gate on the handoff itself
// rather than on a product name we would have to invent.
None => RebootPlan {
overlay_standin_esp: false,
marker: composed_handoff_marker(None),
narration: "[dwarves] ── reboot: booting OFF the installed disk; the cell \
declares no korp.entry=, so the gate is the handoff itself ──"
.to_string(),
},
}
}
}
#[cfg(feature = "vm-boot")]
impl RebootConsole {
/// Take the live installer boot + start the reboot worker. Non-blocking: returns at
/// once with a console the pump immediately begins draining (phase `Booting`).
fn start(
cell: &str,
install: HandleConsole,
disk_path: PathBuf,
hw: HwConfig,
plan: RebootPlan,
) -> RebootConsole {
let log = Arc::new(Mutex::new(String::new()));
let phase = Arc::new(Mutex::new(BootPhase::Booting));
let frame = Arc::new(Mutex::new(None));
let kill = Arc::new(AtomicBool::new(false));
let (l, p, fr, k, cell_s) = (
log.clone(),
phase.clone(),
frame.clone(),
kill.clone(),
cell.to_string(),
);
let worker = std::thread::Builder::new()
.name(format!("dwarves-reboot-{cell}"))
.spawn(move || reboot_worker(&cell_s, install, disk_path, hw, plan, &l, &p, &fr, &k))
.ok();
RebootConsole {
log,
phase,
frame,
kill,
_worker: worker,
}
}
}
#[cfg(feature = "vm-boot")]
impl SerialConsole for RebootConsole {
fn log(&self) -> String {
self.log.lock().unwrap().clone()
}
fn phase(&self) -> BootPhase {
self.phase.lock().unwrap().clone()
}
fn kill(&mut self) {
self.kill.store(true, Ordering::SeqCst);
}
fn frame(&self) -> Option<Frame> {
self.frame.lock().unwrap().clone()
}
}
#[cfg(feature = "vm-boot")]
impl Drop for RebootConsole {
fn drop(&mut self) {
// Signal the worker to tear down whichever QEMU is live (never leak a guest).
self.kill.store(true, Ordering::SeqCst);
}
}
/// A path as the `String` jera's spec carries (lossy only for a non-UTF-8 path, which
/// no staging dir here ever is).
#[cfg(feature = "vm-boot")]
fn path_str(p: &Path) -> String {
p.to_string_lossy().into_owned()
}
/// Poll a live [`HandleConsole`] until its guest is terminally gone (Killed/Failed/etc.),
/// up to `budget` — so the file lock on the installed disk is released before the reboot
/// overlays a FAT ESP into it / a second QEMU opens it.
#[cfg(feature = "vm-boot")]
fn wait_terminated(con: &HandleConsole, budget: Duration) {
let deadline = Instant::now() + budget;
while Instant::now() < deadline {
if !con.phase().is_running() {
break;
}
std::thread::sleep(Duration::from_millis(80));
}
// A small settle so the kernel finishes reaping + releasing the OFD file lock.
std::thread::sleep(Duration::from_millis(300));
}
/// The reboot worker: phase-1 installer boot → INSTALL-OK, then phase-2 boot off the
/// installed disk → APP-ALIVE, streaming ONE cumulative log. Feature `vm-boot`.
#[cfg(feature = "vm-boot")]
#[allow(clippy::too_many_arguments)]
fn reboot_worker(
cell: &str,
mut install: HandleConsole,
disk_path: PathBuf,
hw: HwConfig,
plan: RebootPlan,
log: &Arc<Mutex<String>>,
phase: &Arc<Mutex<BootPhase>>,
frame: &Arc<Mutex<Option<Frame>>>,
kill: &Arc<AtomicBool>,
) {
// ── phase 1: drain the installer boot until INSTALL-OK (Passed) or failure. ──
let install_deadline = Instant::now() + Duration::from_secs(420);
loop {
if kill.load(Ordering::SeqCst) {
install.kill();
*phase.lock().unwrap() = BootPhase::Failed(cancelled_error("run").to_string());
return;
}
*log.lock().unwrap() = install.log();
if let Some(f) = install.frame() {
*frame.lock().unwrap() = Some(f);
}
match install.phase() {
BootPhase::Passed => break, // INSTALLER: INSTALL-OK reached
BootPhase::Failed(e) => {
*phase.lock().unwrap() = BootPhase::Failed(e);
return;
}
_ => {}
}
if Instant::now() > install_deadline {
install.kill();
*phase.lock().unwrap() = BootPhase::Failed(
dwarves::dwarves_err!(launch, 5, "install timed out").to_string(),
);
return;
}
std::thread::sleep(Duration::from_millis(50));
}
// Freeze the install log, tear down the installer QEMU (release the disk lock), and
// narrate the reboot. The cumulative log from here = install_log + reboot lines + boot.
let mut prelude = install.log();
if !prelude.ends_with('\n') {
prelude.push('\n');
}
install.settle(Duration::from_secs(30));
wait_terminated(&install, Duration::from_secs(4));
prelude.push('\n');
prelude.push_str(&plan.narration);
prelude.push('\n');
*log.lock().unwrap() = prelude.clone();
// Overlay dwarves' own FAT ESP — SCAFFOLD ONLY. Over a real installed disk this
// would destroy the ESP the installer just wrote (see `RebootPlan`).
if plan.overlay_standin_esp {
prelude.push_str(
"[dwarves] laying the stand-in FAT ESP onto the installed disk's p1 \
(EFI/BOOT/BOOTX64.EFI = korp-boot stage 1 → chainloads \\vmlinuz) …\n",
);
if let Err(e) = TunnrBootLauncher::overlay_reboot_esp(&disk_path) {
prelude.push_str(&format!("[dwarves] reboot FAILED: {e}\n"));
*log.lock().unwrap() = prelude;
*phase.lock().unwrap() = BootPhase::Failed(format!("reboot: {e}"));
return;
}
}
// The gate is NOT echoed here. The console log is what an operator and a robot both
// read, and a narration line that quotes the marker makes every gate look already
// reached — MEASURED 2026-08-16: the drive latched "the machine rebooted" off THIS
// line, on a run whose second boot never happened.
prelude.push_str("[dwarves] booting the installed disk under QEMU/OVMF (firmware boot) …\n");
*log.lock().unwrap() = prelude.clone();
// ── phase 2: boot off the installed disk, gate on the plan's marker. ──
let mut boot = match TunnrBootLauncher::boot_installed_disk(cell, &disk_path, &hw, &plan.marker)
{
Ok(b) => b,
Err(e) => {
prelude.push_str(&format!("[dwarves] reboot boot spawn FAILED: {e}\n"));
*log.lock().unwrap() = prelude;
*phase.lock().unwrap() = BootPhase::Failed(format!("reboot: {e}"));
return;
}
};
let boot_deadline = Instant::now() + Duration::from_secs(260);
let mut resolved_at: Option<Instant> = None;
loop {
if kill.load(Ordering::SeqCst) {
boot.kill();
if phase.lock().unwrap().is_running() {
*phase.lock().unwrap() = BootPhase::Failed(cancelled_error("run").to_string());
}
return;
}
*log.lock().unwrap() = format!("{prelude}{}", boot.log());
if let Some(f) = boot.frame() {
*frame.lock().unwrap() = Some(f);
}
match resolved_at {
None => match boot.phase() {
BootPhase::Passed => {
*phase.lock().unwrap() = BootPhase::Passed;
resolved_at = Some(Instant::now());
}
BootPhase::Failed(e) => {
boot.kill();
*phase.lock().unwrap() = BootPhase::Failed(format!("reboot: {e}"));
return;
}
_ => {
if Instant::now() > boot_deadline {
boot.kill();
*phase.lock().unwrap() = BootPhase::Failed(format!(
"reboot: the second boot never reached `{}`",
plan.marker
));
return;
}
}
},
Some(t) => {
// korp is up (APP-ALIVE). Keep the guest alive + capturing frames a while so
// the pane lands the running splash, then tear it down (or on cancel/drop).
if t.elapsed() > REBOOT_POST_PASS_LINGER {
boot.kill();
return;
}
}
}
std::thread::sleep(Duration::from_millis(50));
}
}
// ── The LIVE GPU framebuffer: QMP `screendump` → egui texture (feature `vm-boot`). ────
/// A live GPU framebuffer capture: connects to a booting guest's **QMP** control socket
/// and runs `screendump` on a loop, decoding each PPM into a [`Frame`] the pane uploads
/// as an egui texture — the OS-less appliance's REAL screen. Pure-Rust (std `UnixStream`
/// + `serde_json` + a P6 PPM parser); no VNC/RFB crate, no new deps. A genuine capture
/// off the guest's virtio-gpu (the OVMF firmware screen at minimum, then whatever the
/// guest draws) — never a fabricated frame. The serial console keeps streaming in
/// parallel, so a graphical capture that never starts degrades to the honest placeholder
/// + live serial, it never blocks the boot.
#[cfg(feature = "vm-boot")]
struct FramebufferCapture {
frame: Arc<Mutex<Option<Frame>>>,
stop: Arc<AtomicBool>,
_handle: Option<std::thread::JoinHandle<()>>,
}
#[cfg(feature = "vm-boot")]
impl FramebufferCapture {
/// Spawn the capture thread against `qmp_sock` (created by QEMU with
/// `-qmp unix:…,server=on,wait=off`). Non-blocking: returns immediately with an
/// empty frame slot the loop fills once the first `screendump` decodes.
fn start(qmp_sock: PathBuf, cell: String) -> FramebufferCapture {
let frame = Arc::new(Mutex::new(None));
let stop = Arc::new(AtomicBool::new(false));
let (f, s) = (frame.clone(), stop.clone());
let _handle = std::thread::Builder::new()
.name(format!("dwarves-fb-{cell}"))
.spawn(move || fb_capture_loop(&qmp_sock, &cell, &f, &s))
.ok();
FramebufferCapture {
frame,
stop,
_handle,
}
}
/// The latest decoded frame (a clone), or `None` before the first capture.
fn latest(&self) -> Option<Frame> {
self.frame.lock().unwrap().clone()
}
/// Ask the capture loop to stop (idempotent) — called on boot teardown.
fn stop(&self) {
self.stop.store(true, Ordering::SeqCst);
}
}
#[cfg(feature = "vm-boot")]
impl Drop for FramebufferCapture {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
}
}
/// The capture loop: connect (with retries — QEMU creates the socket a beat after
/// spawn), do the QMP handshake, then `screendump` → decode PPM → publish, ~2 fps, until
/// asked to stop or the socket closes (guest gone). Every failure path is silent + safe:
/// no frame is published, so the pane keeps its honest placeholder — NEVER a faked frame.
#[cfg(feature = "vm-boot")]
fn fb_capture_loop(
sock: &Path,
cell: &str,
frame: &Arc<Mutex<Option<Frame>>>,
stop: &Arc<AtomicBool>,
) {
use std::io::{BufReader, Write};
use std::os::unix::net::UnixStream;
// 1. Connect — QEMU may take a moment to create the listening socket.
let deadline = Instant::now() + Duration::from_secs(15);
let stream = loop {
if stop.load(Ordering::SeqCst) {
return;
}
match UnixStream::connect(sock) {
Ok(s) => break s,
Err(_) if Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(200));
}
Err(_) => return, // never came up — degrade to placeholder, no fake frame
}
};
let _ = stream.set_read_timeout(Some(Duration::from_secs(5)));
let mut writer = match stream.try_clone() {
Ok(w) => w,
Err(_) => return,
};
let mut reader = BufReader::new(stream);
// 2. QMP handshake: greeting is skipped by `read_qmp_reply` (it waits for a
// return/error), so sending capabilities and reading its ack is enough.
if writeln!(writer, r#"{{"execute":"qmp_capabilities"}}"#).is_err() {
return;
}
if read_qmp_reply(&mut reader).is_none() {
return;
}
// 3. screendump → decode → publish, paced ~30 fps.
//
// It was ~2 fps, on the reasoning that "the guest's screen changes on human
// timescales". That is true of an appliance sitting in its hlt loop and FALSE of the
// thing this console exists to show: MEASURED 2026-08-16 on t14s, a gunnar-installer
// install is 1.4 s end to end, and the splash the installer draws to the GOP
// framebuffer is on screen for about one of those seconds before the guest resets.
// At 500 ms per capture — with the connect retry and the QMP handshake eaten out of
// the same window — the drive landed a 640×480 "Display output is not active" (the
// pre-GOP console) and then a 1280×800 frame that was black, because by then the
// guest was already rebooting. Both are honest captures of nothing. 100 ms puts ~10
// captures inside the window the logo is actually up. 100 ms was still not enough:
// at that pace the drive caught the firmware's own TianoCore splash and then nothing
// until the guest had already reset, so the product's OWN splash — the one the serial
// reports as `drew nordisk/KORP boot splash to GOP framebuffer - OK` — fell BETWEEN
// two captures every time. 30 ms is not a human-timescale assumption at all, which is
// the point; the loop only lives as long as a guest is booting, so it is bounded by
// the boot rather than left running against an idle appliance.
let ppm = std::env::temp_dir().join(format!("dwarves-fb-{cell}.ppm"));
while !stop.load(Ordering::SeqCst) {
let cmd = format!(
r#"{{"execute":"screendump","arguments":{{"filename":"{}"}}}}"#,
ppm.display()
);
if writeln!(writer, "{cmd}").is_err() {
break;
}
match read_qmp_reply(&mut reader) {
Some(v) if v.get("return").is_some() => {
if let Some(fr) = read_ppm(&ppm) {
*frame.lock().unwrap() = Some(fr);
}
}
// An error reply (e.g. no active console yet) is not fatal — keep polling.
Some(_) => {}
// Socket closed ⇒ the guest is gone; stop cleanly (no fake frame).
None => break,
}
// One short sleep rather than five: the stop flag is already checked at the top
// of every pass, and at this pace the loop is responsive without the fan-out.
if stop.load(Ordering::SeqCst) {
break;
}
std::thread::sleep(Duration::from_millis(30));
}
let _ = std::fs::remove_file(&ppm);
}
/// Read QMP newline-delimited JSON until a command RESULT (`return`/`error`) arrives,
/// skipping the greeting and async `event` lines. `None` on EOF / read error / too many
/// non-result lines (bounded, so a chatty event stream can never spin forever).
#[cfg(feature = "vm-boot")]
fn read_qmp_reply(reader: &mut impl std::io::BufRead) -> Option<serde_json::Value> {
for _ in 0..128 {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) => return None, // EOF — socket closed
Ok(_) => {}
Err(_) => return None, // timeout / error
}
let line = line.trim();
if line.is_empty() {
continue;
}
if let Ok(v) = serde_json::from_str::<serde_json::Value>(line) {
if v.get("return").is_some() || v.get("error").is_some() {
return Some(v);
}
// greeting ("QMP") or async "event" → keep reading for the result
}
}
None
}
/// Decode a binary **P6 PPM** (what QMP `screendump` writes) into an RGBA8 [`Frame`].
/// Tolerant of PPM `#` comments and arbitrary header whitespace; accepts 8-bit only
/// (maxval 255, which QEMU always emits). `None` on any malformed/short file, so a
/// half-written dump never becomes a garbage frame. Pure (no unix/QEMU deps) so it is
/// unit-tested in the default `cargo test --lib` gate, not only under `vm-boot`.
#[cfg(any(feature = "vm-boot", test))]
fn read_ppm(path: &Path) -> Option<Frame> {
let data = std::fs::read(path).ok()?;
let mut pos = 0usize;
if ppm_next_token(&data, &mut pos)? != b"P6" {
return None;
}
let width: usize = std::str::from_utf8(&ppm_next_token(&data, &mut pos)?)
.ok()?
.parse()
.ok()?;
let height: usize = std::str::from_utf8(&ppm_next_token(&data, &mut pos)?)
.ok()?
.parse()
.ok()?;
let maxval: usize = std::str::from_utf8(&ppm_next_token(&data, &mut pos)?)
.ok()?
.parse()
.ok()?;
if maxval != 255 || width == 0 || height == 0 {
return None;
}
// Exactly ONE whitespace byte separates the header from the binary raster.
pos += 1;
let need = width.checked_mul(height)?.checked_mul(3)?;
if data.len() < pos + need {
return None;
}
let rgb = &data[pos..pos + need];
let mut rgba = Vec::with_capacity(width * height * 4);
for px in rgb.chunks_exact(3) {
rgba.extend_from_slice(&[px[0], px[1], px[2], 255]);
}
Some(Frame {
width,
height,
rgba,
})
}
/// One whitespace/`#`-comment-delimited PPM header token, advancing `pos` to just past
/// it (at the following whitespace).
#[cfg(any(feature = "vm-boot", test))]
fn ppm_next_token(data: &[u8], pos: &mut usize) -> Option<Vec<u8>> {
loop {
while *pos < data.len() && data[*pos].is_ascii_whitespace() {
*pos += 1;
}
if *pos < data.len() && data[*pos] == b'#' {
while *pos < data.len() && data[*pos] != b'\n' {
*pos += 1;
}
} else {
break;
}
}
let start = *pos;
while *pos < data.len() && !data[*pos].is_ascii_whitespace() {
*pos += 1;
}
(start != *pos).then(|| data[start..*pos].to_vec())
}
/// The honest stub launcher — WITHOUT the `vm-boot` feature the tunnr KVM backend is
/// not linked, so ▶ Build & Boot reports the feature is off (rebuild `--features
/// vm-boot`) rather than a fake boot. Same type name so the pane is feature-agnostic.
#[cfg(not(feature = "vm-boot"))]
pub struct TunnrBootLauncher;
#[cfg(not(feature = "vm-boot"))]
impl BootLauncher for TunnrBootLauncher {
fn launch(&self, cell_name: &str, _repo_root: &Path) -> Result<Box<dyn SerialConsole>, String> {
// CODED `dwarves-launch-4`: a feature-off build is a NAMED, actionable state —
// never a fake boot, and a robot keys on the code, not this sentence.
Err(dwarves::dwarves_err!(
launch,
4,
"booting '{cell_name}' needs the `vm-boot` feature — rebuild dwarves-gui \
`--features vm-boot` to boot THROUGH tunnr_vm under QEMU/OVMF/KVM"
)
.to_string())
}
}
// ── The REAL edda RAID KVM engine: drive korp-installer through a genuine install+boot ──
/// **The edda RAID KVM engine** — drives the ⚒ Build Thing korp-installer cell through a
/// REAL install+boot by SHELLING OUT to edda's production acceptance harness
/// (`tunnr/crates/korp-installer/tests/raid_amd_nvme_boot.rs`, edda before 2026-09-26), the single source of truth
/// for the "multiple equal SSDs → RAID1 mirror, then boot the installed disk" story
/// (the owner's ★ mandate). That harness installs the mirror across two NVMe images via the
/// PRODUCTION [`korp_installer::raid::run_raid1_install`], power-cycles OFF the installed
/// NVMe under AMD-EPYC + OVMF, and reads back `APP-ALIVE … "verified":true` off the mirror
/// AND off a single surviving member.
///
/// This launcher streams that harness's combined stdout+stderr into the pane's console
/// VERBATIM, so the cell's PASS is set from the ACTUAL `APP-ALIVE` the guest emitted and
/// [`crate::build_thing`]'s `installer_json` parses `raid_built`/`booted_off_disk`/
/// `app_alive`/`single_member_survived` off the same real serial — never a fabricated
/// green. Reuse, never twin (L5): the KVM engine is edda's; dwarves drives it + drains it.
///
/// Scriptable exactly as the harness documents: `KORP_QEMU_CPU` (default `EPYC`) and
/// `KORP_RAID_SCRATCH` (the member-image dir the harness leaves behind) are threaded
/// through. korp-installer lives in TUNNR since 2026-09-26 (moved from edda), so the
/// checkout is located from `DWARVES_TUNNR_DIR` (the old `DWARVES_EDDA_DIR` is still read
/// if it holds the crate), else the tunnr sibling of the dwarves repo; its own cargo target
/// dir from `TUNNR_TARGET_DIR` (or the old `EDDA_TARGET_DIR`). The engine keeps its
/// `edda-raid` name so `DWARVES_INSTALLER_ENGINE=edda-raid` still selects it. Feature-INDEPENDENT: it
/// links nothing from `tunnr-vm` — it only spawns `cargo test` — so the real KVM path is
/// reachable whether or not `vm-boot` is compiled in.
pub struct EddaRaidInstallerLauncher {
/// The checkout root that contains `crates/korp-installer` (tunnr since 2026-09-26).
edda_dir: PathBuf,
/// The cargo target dir for the harness build (keeps it off `/home`), or `None` to
/// inherit the ambient `CARGO_TARGET_DIR`.
target_dir: Option<PathBuf>,
/// The guest CPU model (`-cpu …`) — `EPYC` mirrors the AMD metal.
qemu_cpu: String,
/// Where the harness writes the two NVMe member images (so a driver can read them
/// back), or `None` to use the harness's own default.
scratch: Option<PathBuf>,
}
impl Default for EddaRaidInstallerLauncher {
fn default() -> Self {
let edda_dir = ["DWARVES_TUNNR_DIR", "DWARVES_EDDA_DIR"]
.into_iter()
.filter_map(std::env::var_os)
.map(PathBuf::from)
.find(|d| d.join("crates/korp-installer/Cargo.toml").is_file())
.or_else(|| {
// Siblings of the dwarves repo (this crate is `<dwarves>/dwarves-gui`).
[
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tunnr"),
PathBuf::from("tunnr"),
PathBuf::from("../tunnr"),
]
.into_iter()
.find(|c| c.join("crates/korp-installer/Cargo.toml").is_file())
})
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tunnr"));
EddaRaidInstallerLauncher {
edda_dir,
target_dir: std::env::var_os("TUNNR_TARGET_DIR")
.or_else(|| std::env::var_os("EDDA_TARGET_DIR"))
.map(PathBuf::from)
.or_else(|| {
Some(PathBuf::from(format!(
"/scratch/{}/cargo-targets/tunnr",
std::env::var("USER").unwrap_or_default()
)))
}),
qemu_cpu: std::env::var("KORP_QEMU_CPU").unwrap_or_else(|_| "EPYC".to_string()),
scratch: std::env::var_os("KORP_RAID_SCRATCH").map(PathBuf::from),
}
}
}
impl EddaRaidInstallerLauncher {
/// A launcher pointed at an explicit edda checkout (tests + non-default layouts).
pub fn at(edda_dir: impl Into<PathBuf>) -> Self {
EddaRaidInstallerLauncher {
edda_dir: edda_dir.into(),
..Default::default()
}
}
/// The `cargo test` invocation that runs the real edda RAID harness (`--ignored`
/// because it is a heavy `#[ignore]` acceptance test; `--nocapture` so its narrated
/// serial reaches our console).
fn harness_command(&self) -> std::process::Command {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let mut cmd = std::process::Command::new(cargo);
cmd.current_dir(&self.edda_dir)
.args([
"test",
"-p",
"korp-installer",
"--test",
"raid_amd_nvme_boot",
"--",
"--ignored",
"--nocapture",
])
.env("KORP_QEMU_CPU", &self.qemu_cpu)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
if let Some(t) = &self.target_dir {
cmd.env("CARGO_TARGET_DIR", t);
}
if let Some(s) = &self.scratch {
cmd.env("KORP_RAID_SCRATCH", s);
}
cmd
}
}
impl BootLauncher for EddaRaidInstallerLauncher {
fn launch(&self, _cell: &str, _repo_root: &Path) -> Result<Box<dyn SerialConsole>, String> {
// Fail LOUD (never a fake green) if the edda checkout isn't where we think — the
// console then NAMES what to set, rather than a cryptic spawn error.
let manifest = self.edda_dir.join("crates/korp-installer/Cargo.toml");
if !manifest.is_file() {
return Err(format!(
"korp-installer not found at {} — set DWARVES_TUNNR_DIR to the tunnr checkout \
(korp-installer moved there from edda 2026-09-26) so the RAID KVM harness can run",
manifest.display()
));
}
ProcessConsole::spawn(self.harness_command()).map(|c| Box::new(c) as Box<dyn SerialConsole>)
}
fn launch_hw(
&self,
cell: &str,
repo_root: &Path,
_hw: &HwConfig,
_medium: Option<&Path>,
) -> Result<Box<dyn SerialConsole>, String> {
self.launch(cell, repo_root)
}
}
/// A [`SerialConsole`] over a spawned child process: merges its stdout+stderr into one
/// cumulative line buffer (a reader thread per stream) and resolves the terminal phase
/// from the child's EXIT STATUS — exit 0 → [`BootPhase::Passed`], any non-zero → `Failed`.
/// A monitor thread joins BOTH readers before reaping, so every narrated line (the
/// `APP-ALIVE` markers, the `RAID-AMD-NVME PROOF OK` line) is captured before the phase
/// goes terminal. The pane's [`pump`] drains it exactly like a QEMU serial console — so
/// the edda RAID harness streams into the ⚒ pane with no second console vocabulary.
struct ProcessConsole {
lines: Arc<Mutex<Vec<String>>>,
/// `None` while running; `Some(passed)` once the child exited.
done: Arc<Mutex<Option<bool>>>,
/// The live child (taken by the monitor thread when it reaps); `kill()` reaches it here.
child: Arc<Mutex<Option<std::process::Child>>>,
}
impl ProcessConsole {
fn spawn(mut cmd: std::process::Command) -> Result<ProcessConsole, String> {
let mut child = cmd
.spawn()
.map_err(|e| format!("spawn edda RAID harness failed: {e}"))?;
let lines = Arc::new(Mutex::new(Vec::new()));
let done = Arc::new(Mutex::new(None));
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let mut readers = Vec::new();
for stream in [stdout.map(StdStream::Out), stderr.map(StdStream::Err)]
.into_iter()
.flatten()
{
let buf = lines.clone();
readers.push(std::thread::spawn(move || stream.read_into(buf)));
}
let child = Arc::new(Mutex::new(Some(child)));
let (child_w, done_w) = (child.clone(), done.clone());
std::thread::Builder::new()
.name("dwarves-edda-raid-monitor".to_string())
.spawn(move || {
for r in readers {
let _ = r.join(); // all output captured before we reap
}
let status = child_w.lock().unwrap().take().map(|mut c| c.wait());
*done_w.lock().unwrap() = Some(matches!(status, Some(Ok(s)) if s.success()));
})
.ok();
Ok(ProcessConsole { lines, done, child })
}
}
/// One of a child's two output streams, type-erased so a single reader thread body drains
/// either into the shared line buffer.
enum StdStream {
Out(std::process::ChildStdout),
Err(std::process::ChildStderr),
}
impl StdStream {
fn read_into(self, buf: Arc<Mutex<Vec<String>>>) {
use std::io::BufRead;
let reader: Box<dyn std::io::Read + Send> = match self {
StdStream::Out(o) => Box::new(o),
StdStream::Err(e) => Box::new(e),
};
for line in std::io::BufReader::new(reader).lines() {
match line {
Ok(l) => buf.lock().unwrap().push(l),
Err(_) => break,
}
}
}
}
impl SerialConsole for ProcessConsole {
fn log(&self) -> String {
self.lines.lock().unwrap().join("\n")
}
fn phase(&self) -> BootPhase {
match *self.done.lock().unwrap() {
None => BootPhase::Booting,
Some(true) => BootPhase::Passed,
Some(false) => BootPhase::Failed("edda RAID harness exited non-zero".to_string()),
}
}
fn kill(&mut self) {
if let Some(c) = self.child.lock().unwrap().as_mut() {
let _ = c.kill();
}
}
}
// ── The REAL container launcher: run + follow logs through jera→draupnir. ─────────────
/// The real container launcher — RUNS a `[[build.container]]` cell's OCI image through
/// **jera → draupnir**'s ONE bollard/podman engine and follows its stdout into a
/// [`LogConsole`]. Behind the `container-run` feature (it links jera + draupnir's
/// `backend-oci`); the no-feature build ships the honest stub below (Run then reports
/// the feature is off — never fake logs). Same type name so the pane is feature-agnostic.
#[cfg(feature = "container-run")]
pub struct DraupnirContainerLauncher;
#[cfg(feature = "container-run")]
impl DraupnirContainerLauncher {
/// Map a container launch-cell name to the OCI [`ContainerSpec`] to run. The
/// hello-world busybox echoes a deterministic line then exits 0 (→ passed).
fn spec_for(cell: &str) -> Result<jera::container::ContainerSpec, String> {
use jera::container::ContainerSpec;
let mut spec = match cell {
"tunnr-hello-world" => {
let mut s = ContainerSpec::image("docker.io/library/busybox:latest");
s.cmd = vec![
"/bin/echo".to_string(),
"hello world from the tunnr container appliance".to_string(),
];
s
}
// korp-falkordb — the fat-appliance's graph backend, run for REAL through
// draupnir→podman. The image's OWN entrypoint (no cmd override); it is a
// long-lived SERVICE (never exits), so its readiness is latched by a live
// `redis PING → PONG` probe (see [`Self::readiness_probe`]), not exit 0.
"korp-falkordb" => ContainerSpec::image("localhost/nordisk-falkordb-valkey:9"),
other => {
return Err(format!(
"no container image wired for launch cell '{other}'"
))
}
};
spec = spec.with_name(format!("dwarves-{cell}"));
Ok(spec)
}
/// The **readiness probe** for a long-lived SERVICE container cell — the argv to
/// `exec` into the running container plus the substring that proves it is serving.
/// `None` for a run-to-completion cell (the busybox hello-world, which passes on a
/// clean exit 0). `korp-falkordb` → `redis-cli ping` must answer `PONG`.
fn readiness_probe(cell: &str) -> Option<(Vec<String>, String)> {
match cell {
"korp-falkordb" => Some((vec!["redis-cli".into(), "ping".into()], "PONG".into())),
_ => None,
}
}
}
#[cfg(feature = "container-run")]
impl ContainerLauncher for DraupnirContainerLauncher {
fn launch(&self, cell_name: &str, _repo_root: &Path) -> Result<Box<dyn SerialConsole>, String> {
use jera::boot::BootTarget;
let spec = Self::spec_for(cell_name)?;
// Route THROUGH jera's `connect_default_runner` — the ONE OCI engine (draupnir's
// bollard) under `container-oci`; never a second engine, never a shell. A
// detached run hands back a long-lived proc whose logs the follow-task streams
// and whose exit code drives pass/fail.
let runner = jera::container::connect_default_runner();
let proc = runner
.spawn(&BootTarget::Container(spec))
.map_err(|e| format!("container run failed (is the OCI engine reachable?): {e}"))?;
// A long-lived SERVICE container (korp-falkordb) never exits, so latch readiness
// on a live exec probe (`redis PING → PONG`) rather than exit 0; a
// run-to-completion container (the busybox hello-world) has no probe and passes
// on a clean exit 0 through the plain [`LogConsole`].
match Self::readiness_probe(cell_name) {
Some((argv, marker)) => Ok(Box::new(ServiceLogConsole::new(proc, argv, marker))),
None => Ok(Box::new(LogConsole::new(proc))),
}
}
}
/// Adapts a running container's jera [`BootProc`] to a [`SerialConsole`] LOG window
/// (feature `container-run`): `log()` drains the follow-task's new stdout/stderr lines
/// into a cumulative buffer the [`pump`] streams; `phase()` maps the container's
/// exit-code-aware state (running → Booting, clean exit → Passed, crash → Failed).
#[cfg(feature = "container-run")]
struct LogConsole {
proc: Mutex<Box<dyn jera::boot::BootProc>>,
acc: Mutex<Vec<String>>,
}
#[cfg(feature = "container-run")]
impl LogConsole {
fn new(proc: Box<dyn jera::boot::BootProc>) -> Self {
LogConsole {
proc: Mutex::new(proc),
acc: Mutex::new(Vec::new()),
}
}
}
#[cfg(feature = "container-run")]
impl SerialConsole for LogConsole {
fn log(&self) -> String {
let new = self.proc.lock().unwrap().drain_log();
let mut acc = self.acc.lock().unwrap();
acc.extend(new);
acc.join("\n")
}
fn phase(&self) -> BootPhase {
use jera::boot::BootStatus;
match self.proc.lock().unwrap().poll() {
BootStatus::Booting | BootStatus::Running => BootPhase::Booting,
BootStatus::BootedOk => BootPhase::Passed,
BootStatus::Failed(e) => BootPhase::Failed(e),
BootStatus::Killed => BootPhase::Failed("container killed".to_string()),
}
}
fn kill(&mut self) {
self.proc.lock().unwrap().kill();
}
}
/// Adapts a long-lived **SERVICE** container (FalkorDB) to a [`SerialConsole`] whose
/// readiness is a live **exec probe**, not process exit. A server never exits on its
/// own, so [`LogConsole`]'s "exit 0 = passed" would only ever time out. This console
/// streams the container's real stdout AND, once the container is running, execs a
/// readiness command into it on a throttle (`redis-cli ping`); the first reply carrying
/// the marker (`PONG`) latches [`BootPhase::Passed`] and appends a crisp proof line. The
/// container is a real detached run THROUGH draupnir→podman; dropping the console reaps
/// it (stop + remove) so a proof run leaves nothing behind. Feature `container-run`.
#[cfg(feature = "container-run")]
struct ServiceLogConsole {
proc: Mutex<Box<dyn jera::boot::BootProc>>,
acc: Mutex<Vec<String>>,
/// The readiness exec argv (`redis-cli ping`) and the reply substring that proves
/// the service is actually serving (`PONG`).
probe: Vec<String>,
marker: String,
/// The terminal phase once latched — a passed service stays passed (and the exec
/// probe stops firing), a died-before-ready service stays failed.
latched: Mutex<Option<BootPhase>>,
/// Throttle: fire the exec probe at most ~every 300ms (a `podman exec` on every
/// 20ms pump tick would hammer the engine for nothing).
last_probe: Mutex<Instant>,
}
#[cfg(feature = "container-run")]
impl ServiceLogConsole {
fn new(proc: Box<dyn jera::boot::BootProc>, probe: Vec<String>, marker: String) -> Self {
ServiceLogConsole {
proc: Mutex::new(proc),
acc: Mutex::new(Vec::new()),
probe,
marker,
latched: Mutex::new(None),
last_probe: Mutex::new(Instant::now() - Duration::from_secs(1)),
}
}
}
#[cfg(feature = "container-run")]
impl SerialConsole for ServiceLogConsole {
fn log(&self) -> String {
let new = self.proc.lock().unwrap().drain_log();
let mut acc = self.acc.lock().unwrap();
acc.extend(new);
acc.join("\n")
}
fn phase(&self) -> BootPhase {
use jera::boot::BootStatus;
if let Some(p) = self.latched.lock().unwrap().clone() {
return p;
}
// A service that EXITS before it is ready is a failure, never a pass.
match self.proc.lock().unwrap().poll() {
BootStatus::Booting => return BootPhase::Booting,
BootStatus::Running => {}
BootStatus::BootedOk => {
let p = BootPhase::Failed("service container exited before it was ready".into());
*self.latched.lock().unwrap() = Some(p.clone());
return p;
}
BootStatus::Failed(e) => {
let p = BootPhase::Failed(e);
*self.latched.lock().unwrap() = Some(p.clone());
return p;
}
BootStatus::Killed => {
let p = BootPhase::Failed("service container killed".into());
*self.latched.lock().unwrap() = Some(p.clone());
return p;
}
}
// Running: fire the readiness exec on a throttle; the marker reply latches Passed.
{
let mut last = self.last_probe.lock().unwrap();
if last.elapsed() < Duration::from_millis(300) {
return BootPhase::Booting;
}
*last = Instant::now();
}
let argv: Vec<&str> = self.probe.iter().map(String::as_str).collect();
if let Ok(out) = self.proc.lock().unwrap().exec(&argv) {
let replied = out
.stdout
.iter()
.chain(out.stderr.iter())
.any(|l| l.contains(&self.marker));
if replied {
self.acc.lock().unwrap().push(format!(
"[dwarves] KORP-FALKORDB READY — `{}` → {} (real container, real reply)",
self.probe.join(" "),
self.marker,
));
*self.latched.lock().unwrap() = Some(BootPhase::Passed);
return BootPhase::Passed;
}
}
BootPhase::Booting
}
fn kill(&mut self) {
self.proc.lock().unwrap().kill();
}
}
#[cfg(feature = "container-run")]
impl Drop for ServiceLogConsole {
fn drop(&mut self) {
// Reap the detached service container (stop + remove) — a proof run leaves nothing.
if let Ok(mut p) = self.proc.lock() {
p.kill();
}
}
}
/// The honest stub container launcher — WITHOUT the `container-run` feature the
/// jera→draupnir OCI engine is not linked, so ▶ Run reports the feature is off (rebuild
/// `--features container-run`) rather than fake logs. Same type name (feature-agnostic pane).
#[cfg(not(feature = "container-run"))]
pub struct DraupnirContainerLauncher;
#[cfg(not(feature = "container-run"))]
impl ContainerLauncher for DraupnirContainerLauncher {
fn launch(&self, cell_name: &str, _repo_root: &Path) -> Result<Box<dyn SerialConsole>, String> {
// CODED `dwarves-launch-3`: the container engine is not linked in this build.
Err(dwarves::dwarves_err!(
launch,
3,
"running container '{cell_name}' needs the `container-run` feature — rebuild \
dwarves-gui `--features container-run` to run THROUGH jera→draupnir's OCI \
engine (bollard) and follow its logs"
)
.to_string())
}
}
// ── the Exec pane's sandbox ───────────────────────────────────────────────────
/// **How a built executable is run: inside a container, never against the host.**
///
/// Decided 2026-08-06. An `exe` cell's ▶ KÖR used to spawn the artifact as a bare child
/// process with the operator's own environment, cwd and write access — so "what does this
/// build do?" was answered by letting it do it, to this machine. Containerised, the answer
/// is the same every time and the artifact cannot write over anything outside.
///
/// # Why this is a plain struct and not a `ContainerSpec`
///
/// The OCI types live behind the `container-run` feature, so a spec-shaped plan would be
/// untestable in the default build — and the property worth guarding (the argv the
/// container runs is the CONTAINER path, and the artifact is mounted read-only) is
/// exactly the part that needs no engine to check. [`Self::to_spec`] does the conversion
/// where the feature is on; [`Self::cmd`] and [`Self::bind`] are checkable everywhere.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecSandbox {
/// The image the artifact runs in.
pub image: String,
/// The artifact on the host. Mounted read-only; never executed from here.
pub artifact: PathBuf,
/// The operator's argv, already split.
pub argv: Vec<String>,
}
impl ExecSandbox {
/// Where the artifact is mounted inside the container.
pub const MOUNT_DIR: &'static str = "/artefakt";
/// The default base image, overridable with `$DWARVES_EXEC_IMAGE`.
///
/// A `scratch` image cannot be the default: these artifacts are dynamically linked
/// against the host's glibc, and a run that dies on a missing loader is a worse
/// answer than a slightly larger image.
pub const DEFAULT_IMAGE: &'static str = "docker.io/library/debian:bookworm-slim";
/// A sandbox for `artifact`, run with `argv`.
pub fn new(artifact: impl Into<PathBuf>, argv: Vec<String>) -> Self {
let image = std::env::var("DWARVES_EXEC_IMAGE")
.ok()
.filter(|v| !v.trim().is_empty())
.unwrap_or_else(|| Self::DEFAULT_IMAGE.to_string());
ExecSandbox {
image,
artifact: artifact.into(),
argv,
}
}
/// The artifact's path INSIDE the container.
pub fn in_container(&self) -> String {
let name = self
.artifact
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "artefakt".to_string());
format!("{}/{name}", Self::MOUNT_DIR)
}
/// The command the container runs: the in-container path, then the operator's argv.
///
/// Never the host path. That distinction is the whole of the change — a spec that
/// carried `/home/…/target/release/x` would be a host path handed to a container that
/// cannot see it, and a launcher that "helpfully" fell back to the host would be the
/// bare process this replaces.
pub fn cmd(&self) -> Vec<String> {
let mut v = vec![self.in_container()];
v.extend(self.argv.iter().cloned());
v
}
/// The bind mount, in podman's `host:container:ro` spelling.
pub fn bind(&self) -> String {
format!("{}:{}:ro", self.artifact.display(), self.in_container())
}
/// The one line the pane shows so the sandbox is visible rather than implied.
pub fn banner(&self) -> String {
format!(
"kör i container {} · {} · utan nät",
self.image,
self.bind()
)
}
/// The jera spec this plan becomes.
#[cfg(feature = "container-run")]
pub fn to_spec(&self, name: &str) -> jera::container::ContainerSpec {
use jera::container::{BindMount, ContainerSpec, NetMode};
let mut spec = ContainerSpec::image(self.image.clone()).with_name(name.to_string());
spec.cmd = self.cmd();
spec.mounts = vec![BindMount {
host: self.artifact.display().to_string(),
container: self.in_container(),
ro: true,
}];
// No network. "It cannot write over anything outside" is the point, and an
// artifact nobody asked to reach the network should not be able to.
spec.net = NetMode::None;
spec
}
}
/// One per Exec run, so no two containers can ever contend for the same name. Not a
/// count of live containers — there is at most one of those, and that is
/// `DeclaredForge::take_live`'s job, not this counter's.
#[cfg(feature = "container-run")]
static EXEC_RUN_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// Runs a BUILT ARTIFACT inside a container, as the [`crate::build_scene`] Exec pane's
/// ▶ KÖR — the artifact mirror of [`DraupnirContainerLauncher`], which runs a DECLARED
/// image cell.
///
/// It is a [`ContainerLauncher`] so the whole console contract above serves it unchanged:
/// one [`BootRun`], one [`pump`], one log window, one kill path. The `cell_name` it is
/// handed is only a label for the container.
pub struct ArtifactExecLauncher {
/// What to run, and how.
pub sandbox: ExecSandbox,
}
#[cfg(feature = "container-run")]
impl ContainerLauncher for ArtifactExecLauncher {
/// # Why this does not go through `jera`'s runner
///
/// It did, and the mount silently vanished. jera's detached
/// `BootRunner::spawn` calls `draupnir::Boot::boot`, whose `BootSpec` has **no
/// bind field at all** — so a `ContainerSpec` may carry `mounts`, and that leg
/// drops every one of them without a word. Measured 2026-08-06: the container came
/// up and died on `executable file /artefakt/<x> not found`. `net` DOES flow
/// through both legs, which is what made the gap easy to miss.
///
/// `ContainerBoot::boot_with_binds` is the mount-aware twin, and it lives on
/// draupnir. Same engine, one layer down — not a second one.
fn launch(&self, cell_name: &str, _repo_root: &Path) -> Result<Box<dyn SerialConsole>, String> {
if !self.sandbox.artifact.is_file() {
// CODED `dwarves-launch-2`: nothing to mount, so nothing can run.
return Err(dwarves::dwarves_err!(
launch,
2,
"{} is not a file, so there is nothing to mount into the container",
self.sandbox.artifact.display()
)
.to_string());
}
// A UNIQUE name per run. A fixed one collides with the previous run's container
// while it is still being removed — the teardown is asked for synchronously but
// completes on the engine's own clock, so pressing KÖR twice raced the removal
// and the second run died on a name conflict. Observed as a flake before it was
// understood, which is the only warning that shape ever gives.
let n = EXEC_RUN_SEQ.fetch_add(1, Ordering::SeqCst);
let spec = self
.sandbox
.to_spec(&format!("dwarves-exec-{cell_name}-{n}"));
let binds = spec.bind_strings();
let backend = draupnir::container::ContainerBoot::new();
let machine = backend
.boot_with_binds(&spec.to_draupnir(), &binds)
.map_err(|e| format!("the container run failed (is the OCI engine reachable?): {e}"))?;
Ok(Box::new(BoundRunConsole {
backend,
machine: Some(machine),
acc: Mutex::new(Vec::new()),
}))
}
}
/// A [`SerialConsole`] over a container booted **with its bind mounts** — the console
/// half of the note above.
///
/// [`LogConsole`] wraps a jera `BootProc`, and there is no `BootProc` on this leg: the
/// mount-aware boot hands back a bare `Machine`. Everything else is the same contract,
/// drained through draupnir's own [`draupnir::container::ContainerControl`], so the pump
/// above cannot tell the two apart.
#[cfg(feature = "container-run")]
struct BoundRunConsole {
backend: draupnir::container::ContainerBoot,
/// `None` once killed, so a second kill is a no-op rather than a second stop.
machine: Option<draupnir::Machine>,
acc: Mutex<Vec<String>>,
}
#[cfg(feature = "container-run")]
impl SerialConsole for BoundRunConsole {
fn log(&self) -> String {
use draupnir::container::ContainerControl;
if let Some(m) = &self.machine {
let new = self.backend.drain_logs(m);
self.acc.lock().unwrap().extend(new);
}
self.acc.lock().unwrap().join("\n")
}
fn phase(&self) -> BootPhase {
use draupnir::container::{ContainerControl, ContainerState};
let Some(m) = &self.machine else {
return BootPhase::Failed("the run was torn down".to_string());
};
match self.backend.container_state(m) {
ContainerState::Running => BootPhase::Booting,
ContainerState::Exited(0) => BootPhase::Passed,
// A non-zero exit is the artifact's own answer, and it is not a pass.
ContainerState::Exited(code) => BootPhase::Failed(format!("exit {code}")),
ContainerState::Gone => BootPhase::Failed("the container went away".to_string()),
}
}
fn kill(&mut self) {
use draupnir::container::ContainerControl;
if let Some(m) = self.machine.take() {
// Drain what is left BEFORE the container is removed, or a fast run's whole
// output is lost to the teardown that follows it.
let last = self.backend.drain_logs(&m);
self.acc.lock().unwrap().extend(last);
self.backend.stop(&m);
}
}
}
/// The honest stub: WITHOUT `container-run` there is no OCI engine linked, and the
/// answer is that — never a fall back to running the artifact against the host, which is
/// the exact thing this type exists to stop.
#[cfg(not(feature = "container-run"))]
impl ContainerLauncher for ArtifactExecLauncher {
fn launch(
&self,
_cell_name: &str,
_repo_root: &Path,
) -> Result<Box<dyn SerialConsole>, String> {
// CODED `dwarves-launch-3`: the container engine is not linked in this build.
Err(dwarves::dwarves_err!(
launch,
3,
"an exe is run INSIDE a container, and this build has no OCI engine linked — \
rebuild dwarves-gui `--features container-run`. It will not be run against \
the host instead"
)
.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::VecDeque;
/// A fake console that replays a captured serial in growing batches, then reports
/// a terminal phase — the test double for a real KVM boot.
struct ReplayConsole {
/// Growing cumulative log (lines revealed one batch per `log()` call).
revealed: Vec<String>,
pending: VecDeque<String>,
/// The phase to report once the replay is exhausted.
terminal: BootPhase,
}
impl ReplayConsole {
fn new(serial: &str, terminal: BootPhase) -> Self {
ReplayConsole {
revealed: Vec::new(),
pending: serial.lines().map(str::to_string).collect(),
terminal,
}
}
}
impl ReplayConsole {
/// Cumulative log — every line revealed so far.
fn cumulative(&self) -> String {
let mut s = self.revealed.join("\n");
if !s.is_empty() {
s.push('\n');
}
s
}
}
/// The pump calls `log()` then `phase()` each tick; each `phase()` reveals one more
/// serial line (interior mutability behind a Mutex), reaching `terminal` once the
/// replay is exhausted — a growing cumulative log, exactly like a live boot.
struct Replay(Mutex<ReplayConsole>);
impl SerialConsole for Replay {
fn log(&self) -> String {
self.0.lock().unwrap().cumulative()
}
fn phase(&self) -> BootPhase {
let mut g = self.0.lock().unwrap();
if let Some(line) = g.pending.pop_front() {
g.revealed.push(line);
BootPhase::Booting
} else {
g.terminal.clone()
}
}
fn kill(&mut self) {}
}
const KORP_SERIAL: &str = "\
==================================================
KORP surveillance demo (KVM)
[korp] PID1 = appliance (pid=1)
[korp] firmware: UEFI/OVMF boot reached PID 1
[korp] installed korp-demo (headless pipeline + in-image FalkorDB)
[korp] placed data component korp-map -> /var/lib/korp-demo/map/europe.parquet
[korp] started korp-demo
[korp] korp-demo installed and started
KORP-APPLIANCE BOOT-OK: appliance reached runtime";
#[test]
fn pump_streams_the_korp_serial_and_ends_passed() {
let mut src = Replay(Mutex::new(ReplayConsole::new(
KORP_SERIAL,
BootPhase::Passed,
)));
let console = Arc::new(Mutex::new(Vec::new()));
let phase = Arc::new(Mutex::new(BootPhase::Booting));
let kill = AtomicBool::new(false);
let frame = Arc::new(Mutex::new(None));
let ok = pump(
&mut src,
&kill,
&console,
&phase,
&frame,
Duration::from_secs(5),
false,
);
assert!(ok, "the korp boot reached BOOT-OK (passed)");
assert_eq!(*phase.lock().unwrap(), BootPhase::Passed);
let log = console.lock().unwrap().join("\n");
assert!(
log.contains("installed korp-demo"),
"korp-demo install streamed:\n{log}"
);
assert!(
log.contains("started korp-demo"),
"korp-demo start streamed:\n{log}"
);
assert!(
log.contains("KORP-APPLIANCE BOOT-OK: appliance reached runtime"),
"the BOOT-OK marker streamed to the console:\n{log}"
);
}
/// The GPU framebuffer decode is REAL: a byte-exact P6 PPM (the exact shape QMP
/// `screendump` writes) decodes to the right dimensions and RGBA pixels — with a
/// `#` comment + irregular header whitespace to prove the parser is tolerant. This
/// is the pure half of the graphical console; a malformed/short dump yields `None`
/// (never a garbage/faked frame).
#[test]
fn read_ppm_decodes_a_real_p6_framebuffer_to_rgba() {
// 2x1 image: red pixel then green pixel. Header carries a comment + extra ws.
let mut bytes = Vec::new();
bytes.extend_from_slice(b"P6\n# dwarves screendump\n2 1\n255\n");
bytes.extend_from_slice(&[255, 0, 0, 0, 255, 0]); // RGB: red, green
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("fb.ppm");
std::fs::write(&path, &bytes).unwrap();
let frame = read_ppm(&path).expect("a valid P6 PPM decodes");
assert_eq!((frame.width, frame.height), (2, 1));
assert_eq!(
frame.rgba,
vec![255, 0, 0, 255, /* red+α */ 0, 255, 0, 255 /* green+α */],
"RGB→RGBA8 with opaque alpha, row-major"
);
// A truncated raster (header promises 2 px, only 1 present) is rejected — no
// half-frame ever reaches the texture.
let mut short = Vec::new();
short.extend_from_slice(b"P6\n2 1\n255\n");
short.extend_from_slice(&[1, 2, 3]); // only ONE pixel of the two
let sp = dir.path().join("short.ppm");
std::fs::write(&sp, &short).unwrap();
assert!(
read_ppm(&sp).is_none(),
"a short/truncated dump is rejected, never faked"
);
}
#[test]
fn a_cancelled_boot_is_failed_not_passed() {
// A console that never terminates on its own — only cancellation ends it.
let mut src = Replay(Mutex::new(ReplayConsole::new(
"[korp] booting…\n[korp] still booting…",
BootPhase::Booting, // never terminal
)));
let console = Arc::new(Mutex::new(Vec::new()));
let phase = Arc::new(Mutex::new(BootPhase::Booting));
let kill = AtomicBool::new(true); // cancelled from the start
let frame = Arc::new(Mutex::new(None));
let ok = pump(
&mut src,
&kill,
&console,
&phase,
&frame,
Duration::from_secs(5),
false,
);
assert!(!ok, "a cancelled boot is NOT a passed smoke gate");
assert!(matches!(*phase.lock().unwrap(), BootPhase::Failed(_)));
}
#[test]
fn a_launcher_error_surfaces_and_never_fakes_boot_ok() {
struct FailLauncher;
impl BootLauncher for FailLauncher {
fn launch(&self, _c: &str, _r: &Path) -> Result<Box<dyn SerialConsole>, String> {
Err("QEMU not on PATH — install qemu-system-x86".to_string())
}
}
let run = BootRun::spawn("korp-kvm", PathBuf::from("."), Arc::new(FailLauncher));
// Wait for the worker to resolve.
for _ in 0..200 {
if !run.is_running() {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
assert!(
!run.passed(),
"a missing precondition never fakes a BOOT PASS"
);
assert!(matches!(run.phase(), BootPhase::Failed(_)));
let log = run.log().join("\n");
assert!(
log.contains("QEMU not on PATH"),
"the missing precondition is surfaced: {log}"
);
assert!(!log.contains("BOOT-OK"), "no fabricated BOOT-OK");
}
/// ── gap #7: the iso-kind launch plan, pure (no QEMU, no feature) ─────────────────
///
/// The routing rule itself: an iso-kind variant WITH a composed medium selects the
/// live-ISO leg and carries THE COMPOSED FILE, verbatim; no medium (or a path no
/// longer on disk) is the stand-in leg. RED before 2026-08-20: the rule did not
/// exist — `spawn_iso` carried no medium at all and the launcher refused every
/// composed variant (see `composed_iso_routing` below for that recorded red).
#[test]
fn an_iso_variant_with_a_composed_medium_plans_the_live_leg_with_that_file() {
let dir = tempfile::tempdir().unwrap();
let iso = dir.path().join("gunnar-appliance-iso.iso");
std::fs::write(&iso, b"the composed medium").unwrap();
assert_eq!(
plan_iso_launch(Some(&iso)),
IsoLaunchPlan::LiveComposed {
medium: iso.clone()
},
"a composed medium takes the live leg, carrying the composed file itself"
);
assert_eq!(
plan_iso_launch(None),
IsoLaunchPlan::Standin,
"no build → the stand-in"
);
assert_eq!(
plan_iso_launch(Some(&dir.path().join("vanished.iso"))),
IsoLaunchPlan::Standin,
"an artifact no longer on disk is NOT booted on the build's word"
);
}
/// ── gap #7, extended (the medium-pick fix): the planned medium ends with `.iso` ──
///
/// The routing above carries whatever the pane hands it, verbatim — which is the
/// point, and also why the pick UPSTREAM must be right: on t14s (2026-08-18) an
/// iso-kind BYGG's outcome recorded the KERNEL (`/boot/vmlinuz-…`, the first
/// provenance input in the artifact list) as the artifact, the gap-7 leg
/// faithfully planned `LiveComposed` around it, and QEMU failed opening it. This
/// test drives the plan FROM the real pick — `BuildReport::medium()`, the exact
/// thing `settle()` reads into the outcome `composed_medium()` hands over — over
/// the live repro's list shape, so the planned medium is the mastered `.iso`.
/// RED before `BuildReport::medium()` existed: the pick was `artifacts.first()`
/// and this planned the kernel.
#[test]
fn the_planned_medium_for_a_composed_iso_variant_is_the_mastered_iso() {
use dwarves::build_thing::BuildReport;
use nornir_build_thing::variant::ArtifactKind;
let dir = tempfile::tempdir().unwrap();
let kernel = dir.path().join("vmlinuz-7.0.0-29-generic");
let payload = dir.path().join("gunnar");
let iso = dir.path().join("gunnar-appliance-iso.iso");
for p in [&kernel, &payload, &iso] {
std::fs::write(p, b"bytes on disk, so the plan's is_file() gate passes").unwrap();
}
// The appliance-ISO order, verbatim: inputs first, the medium last.
let report = BuildReport {
variant: "gunnar-appliance-iso".into(),
exit_ok: true,
kind: ArtifactKind::Iso,
artifacts: vec![(kernel, 51), (payload, 51), (iso.clone(), 51)],
..Default::default()
};
let medium = report
.medium()
.map(|(p, _)| p.clone())
.expect("a produced medium");
assert!(
medium.extension().is_some_and(|e| e == "iso"),
"the planned medium is the mastered image, never a provenance input: {}",
medium.display()
);
assert_eq!(
plan_iso_launch(Some(&medium)),
IsoLaunchPlan::LiveComposed { medium: iso },
"and the gap-7 leg boots THAT file"
);
}
/// The composed gate is ONE spelling for both composed legs (installer reboot +
/// iso-kind live boot): korp-start's `INSTALLER: `-prefixed exec-argv line, naming
/// the declared entry when there is one.
#[test]
fn the_composed_handoff_marker_names_the_declared_entry() {
assert_eq!(
composed_handoff_marker(Some("gunnar")),
"INSTALLER: exec argv: [\"gunnar\""
);
assert_eq!(composed_handoff_marker(None), "INSTALLER: exec argv: [");
}
/// A spy launcher proves the pane→launcher carry for an iso-kind variant: ▶ hands
/// the launcher THE composed file and a GPU hardware config, and the intro names
/// that file (never the stand-in notice). RED before 2026-08-20 in the strongest
/// sense: `spawn_iso` had no medium parameter, so the pane could not hand the
/// composed file over at all.
#[test]
fn spawn_iso_carries_the_composed_medium_and_gpu_hw_to_the_launcher() {
// spawn_iso now picks the ssh-forward port first; serialize against the test
// that deliberately exhausts the window, or the spy is never reached.
let _guard = SSH_PORT_TEST_LOCK.lock().unwrap();
type Seen = Arc<Mutex<Option<(String, HwConfig, Option<PathBuf>)>>>;
struct Spy(Seen);
impl BootLauncher for Spy {
fn launch(&self, _c: &str, _r: &Path) -> Result<Box<dyn SerialConsole>, String> {
Err("spy: launch() must not be reached — launch_hw carries the medium".into())
}
fn launch_hw(
&self,
cell: &str,
_root: &Path,
hw: &HwConfig,
medium: Option<&Path>,
) -> Result<Box<dyn SerialConsole>, String> {
*self.0.lock().unwrap() =
Some((cell.to_string(), *hw, medium.map(Path::to_path_buf)));
Err("spy stops here — the routing is recorded".into())
}
}
let dir = tempfile::tempdir().unwrap();
let iso = dir.path().join("gunnar-appliance-iso.iso");
std::fs::write(&iso, b"the composed medium").unwrap();
let seen: Seen = Arc::new(Mutex::new(None));
let run = BootRun::spawn_iso(
"gunnar-appliance-iso",
dir.path().to_path_buf(),
Arc::new(Spy(seen.clone())),
HwConfig::default(),
Some(iso.clone()),
);
for _ in 0..200 {
if !run.is_running() {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
let (cell, hw, medium) = seen
.lock()
.unwrap()
.clone()
.expect("the launcher was reached");
assert_eq!(cell, "gunnar-appliance-iso");
assert_eq!(
medium.as_deref(),
Some(iso.as_path()),
"the launcher gets THE COMPOSED FILE, never a re-mastered one"
);
assert!(
hw.gpu,
"the iso-kind ▶ carries a real hardware config with the GPU on"
);
let log = run.log().join("\n");
assert!(
log.contains(&format!("medium: {}", iso.display())),
"the intro names the medium about to boot:\n{log}"
);
assert!(
!log.contains("STAND-IN"),
"a composed boot never says STAND-IN:\n{log}"
);
}
/// …and with NO medium the stand-in still announces itself, before anything boots.
#[test]
fn spawn_iso_without_a_medium_announces_the_stand_in() {
struct Refuse;
impl BootLauncher for Refuse {
fn launch(&self, _c: &str, _r: &Path) -> Result<Box<dyn SerialConsole>, String> {
Err("no medium — refused honestly".into())
}
}
let run = BootRun::spawn_iso(
"gunnar-appliance-iso",
PathBuf::from("."),
Arc::new(Refuse),
HwConfig::default(),
None,
);
for _ in 0..200 {
if !run.is_running() {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
let log = run.log().join("\n");
assert!(
log.contains(STANDIN_NOTICE),
"the no-medium boot announces the stand-in before anything boots:\n{log}"
);
assert!(!run.passed(), "an honest refusal is never a passed gate");
}
/// Serializes the tests that probe/occupy the ssh-forward host-port window, so a
/// test that deliberately fills the window never flakes a concurrent pick.
static SSH_PORT_TEST_LOCK: Mutex<()> = Mutex::new(());
/// ── clone-into-the-appliance: the live-ISO ssh forward is ANNOUNCED ─────────────
///
/// MEASURED on the live t14s guest 2026-08-20: the ⚒ pane's live-ISO leg started
/// QEMU with `-netdev user,id=n0 -device virtio-net-pci,netdev=n0` — NO hostfwd —
/// so gunnar's sshd, listening in the guest on 0.0.0.0:2222, was unreachable from
/// the host and there was no address to `git clone` INTO the live-booted
/// appliance. gunnar's own forge test (`gunnar_appliance_iso`) proves
/// ssh-over-hostfwd works in principle; the gap is purely the pane leg's netdev
/// line.
///
/// This pins the operator half: once the guest is really booting, the console
/// says WHERE the forward listens — `[dwarves] ssh forward: host
/// 127.0.0.1:<port> -> guest 2222` — so the address is read off the pane, never
/// out of the source. RED 2026-08-20 against today's code: no ssh-forward line
/// exists anywhere in the boot path.
#[test]
fn an_iso_boot_announces_the_ssh_forward_address() {
let _guard = SSH_PORT_TEST_LOCK.lock().unwrap();
struct Live;
impl BootLauncher for Live {
fn launch(&self, _c: &str, _r: &Path) -> Result<Box<dyn SerialConsole>, String> {
Ok(Box::new(Replay(Mutex::new(ReplayConsole::new(
KORP_SERIAL,
BootPhase::Passed,
)))))
}
}
let dir = tempfile::tempdir().unwrap();
let iso = dir.path().join("gunnar-appliance-iso.iso");
std::fs::write(&iso, b"the composed medium").unwrap();
let run = BootRun::spawn_iso(
"gunnar-appliance-iso",
dir.path().to_path_buf(),
Arc::new(Live),
HwConfig::default(),
Some(iso),
);
for _ in 0..500 {
if !run.is_running() {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
assert!(run.passed(), "the fake boot replays to BOOT-OK");
let log = run.log().join("\n");
assert!(
log.contains("[dwarves] ssh forward: host 127.0.0.1:"),
"the console names the ssh forward's host address:\n{log}"
);
assert!(
log.contains("-> guest 2222"),
"…and the guest port it reaches (gunnar's sshd convention):\n{log}"
);
}
/// The planned netdev argv, exact: user-mode SLIRP on virtio-net (the tokens the
/// leg always had) plus the LOOPBACK-bound forward — `tcp:127.0.0.1:…`, never the
/// all-interfaces `tcp::…` tunnr's `NetConfig::User` seam would spell. RED
/// 2026-08-20 before `live_ssh_netdev_args` existed: the leg planned tunnr's
/// default plain netdev, no hostfwd at all (the t14s measurement).
#[test]
fn the_live_iso_netdev_plans_a_loopback_ssh_hostfwd() {
assert_eq!(
live_ssh_netdev_args(10022),
vec![
"-netdev".to_string(),
"user,id=n0,hostfwd=tcp:127.0.0.1:10022-:2222".to_string(),
"-device".to_string(),
"virtio-net-pci,netdev=n0".to_string(),
],
"loopback-bound forward to the guest's 2222, on the same user netdev"
);
}
/// The operator line, exact — and a pick-next-free rebind is LOUD, never silent.
#[test]
fn the_ssh_forward_notice_names_the_address_and_any_rebind() {
assert_eq!(
ssh_forward_notice(10022),
"[dwarves] ssh forward: host 127.0.0.1:10022 -> guest 2222"
);
assert_eq!(
ssh_forward_notice(10024),
"[dwarves] ssh forward: host 127.0.0.1:10024 -> guest 2222 (10022 was taken)",
"a rebound port states the port it stepped past"
);
}
/// The pick walk is BOUNDED and an exhausted window fails BY NAME — the two rules
/// that keep the forward honest: the operator is never bound somewhere unstated,
/// and never left guessing which port was meant.
#[test]
fn the_port_pick_is_bounded_and_fails_by_name() {
let _guard = SSH_PORT_TEST_LOCK.lock().unwrap();
// Hold a real port so the walk must step past it.
let held = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap();
let base = held.local_addr().unwrap().port();
let picked = pick_free_loopback_port(base, 4).expect("a later port in the window is free");
assert!(
picked > base && picked < base + 4,
"next-free stays inside the bounded window: picked {picked}, base {base}"
);
// A window of exactly the held port has nothing free: a NAMED failure.
let err = pick_free_loopback_port(base, 1).expect_err("the only candidate is taken");
assert!(
err.contains("no free ssh-forward host port") && err.contains(&base.to_string()),
"the failure names the exhausted range, never a silent rebind: {err}"
);
}
/// …and an exhausted window fails the SPAWNED RUN by name too (coded
/// dwarves-launch-5), before the launcher ever runs — the ▶ never boots a guest
/// while pretending a forward it does not have.
#[test]
fn an_exhausted_port_window_fails_the_iso_boot_by_name() {
let _guard = SSH_PORT_TEST_LOCK.lock().unwrap();
// Occupy the ENTIRE window. A bind that fails means someone else already
// holds that port right now — taken either way, which is all the test needs.
let _held: Vec<std::net::TcpListener> = (LIVE_SSH_HOST_PORT
..LIVE_SSH_HOST_PORT + LIVE_SSH_PORT_TRIES)
.filter_map(|p| std::net::TcpListener::bind(("127.0.0.1", p)).ok())
.collect();
struct MustNotLaunch;
impl BootLauncher for MustNotLaunch {
fn launch(&self, _c: &str, _r: &Path) -> Result<Box<dyn SerialConsole>, String> {
panic!("the launcher must never run without the announced forward");
}
}
let run = BootRun::spawn_iso(
"gunnar-appliance-iso",
PathBuf::from("."),
Arc::new(MustNotLaunch),
HwConfig::default(),
None,
);
for _ in 0..500 {
if !run.is_running() {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
assert!(
!run.passed(),
"no forward, no boot — never a silent forward-less run"
);
assert_eq!(run.error_code(), Some("dwarves-launch-5"));
let log = run.log().join("\n");
assert!(
log.contains("no free ssh-forward host port"),
"the console names the exhausted window:\n{log}"
);
}
/// ── gap #7 (dwarves-ui audit): a COMPOSED iso-kind variant must boot, not be refused ──
///
/// REPRODUCED live 2026-08-20 (t14s): `gunnar-appliance-iso` BYGGed a real 32 MB
/// medium — a self-contained El Torito ESP ISO out of isocompose — and ▶ KÖR still
/// answered `dwarves-launch-5 … no real appliance wired for launch cell
/// 'gunnar-appliance-iso' (bootable: …)`, offering the korp-installer efistub
/// STAND-IN instead. These tests pin the routing: an iso-kind variant WITH a
/// composed medium takes the live-ISO boot leg (the leg that already firmware-boots
/// a self-contained-ESP ISO from RAM, GPU + QMP framebuffer included), so the only
The file goes on; the console shows all of it.