gunnar-server 2.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
//! The `gunnar-server` binary — the appliance.
//!
//! Parsing lives in [`gunnar_common::cli`], the verbs in [`gunnar_server`]; this
//! file is the process entry point and nothing else. It dispatches only server
//! commands — a build of this crate links no client and no control plane.
//!
//! It reuses [`gunnar_server::run`], the one server dispatch, so `gunnar-cli`'s
//! `gunnar-solo` binary and this one route every server verb through the same
//! match (one implementation).

use clap::Parser;

/// The heap. One line, and the choice it names lives in
/// [`gunnar_server::ServerAllocator`] so this binary, `gunnar-solo`,
/// `gunnar-service` and the bench's `git-store-serve` cannot drift apart.
#[global_allocator]
static ALLOC: gunnar_server::ServerAllocator = gunnar_server::SERVER_ALLOCATOR;

// The build-thing RECEIPT, as a `#[used] #[link_section = ".nordisk.buildthing"]`
// static when this compilation is a declared cell, and a comment otherwise. The
// path comes from the env var `build.rs` published rather than from a name
// spelled here, so the file is named in exactly one place — the writer.
include!(env!("GUNNAR_BUILD_RECEIPT"));

/// Say `msg` on every console a human might be looking at: stderr (the
/// `/dev/console` PID 1 inherits — the serial line the proofs read) AND the VGA
/// text screen `/dev/tty0` when it exists.
///
/// MEASURED 2026-08-28 on UpCloud (SeaBIOS, VNC as the only console): the
/// appliance died as PID 1 with `Attempted to kill init! exitcode=0x00006500` —
/// exit 101, a Rust panic — and the panic text went to stderr = ttyS0, which no
/// cloud console shows. Locally the same disk served. A last word nobody can
/// read is no last word; so the panic hook and the top-level refusal both go
/// through here.
fn shout(msg: &str) {
    use std::io::Write;
    eprint!("{msg}");
    // Outside tracing: handed to the boot log the installer reads, too (the
    // panic and the top-level refusal are the lines it most needs).
    gunnar_common::logqueue::tap_line(msg.trim_matches('\n'));
    if let Ok(mut tty0) = std::fs::OpenOptions::new().write(true).open("/dev/tty0") {
        let _ = tty0.write_all(msg.replace('\n', "\r\n").as_bytes());
    }
}

fn main() {
    // The boot log the installer collects keeps every line from here on
    // (the ESP's BOOTLOG.TXT first — the install and this boot so far).
    #[cfg(feature = "wg-client")]
    gunnar_server::installer_listener::capture();
    // DEV builds only: the same log, sealed to the installer's key, over UDP
    // from the moment the NIC has an address — before the installer's link.
    #[cfg(feature = "dev-narrate")]
    gunnar_server::dev_narrate::start("gunnar-server");
    let cli = gunnar_common::cli::Cli::parse();
    // As PID 1 there is no parent to read the exit code and no serial line on a
    // cloud VM: the panic message itself must reach the screen (`shout`), and is
    // written down off the stripe (`slow::record_crash`). MEASURED 2026-09-23: the
    // hook ran on the estate's twin, wrote its record, and the process died —
    // `Attempted to kill init!`, an exit code and nothing else. So a panic on an
    // appliance ends as a FAULT (`GN-99`): edda's panic hook (`appliance_pid1::fault`)
    // writes `<data>/crash/fault.txt`, says the fault line through the narrator and
    // restarts a serving box (unless it loops) or powers a booting one off.
    //
    // Armed on BOTH `is_init` and an installed narrator: the hook also runs in
    // `gunnar-solo` on a laptop and inside every test binary, and powering off a
    // developer's machine because a test panicked would be a considerably worse
    // defect than the one being fixed. See `boot::install_panic_fault`.
    gunnar_server::boot::install_panic_fault(Some(cli.data_root.clone()), shout);
    gunnar_common::init_tracing(cli.verbose);
    // The previous stop's record (the ESP survives a read-only /data), said once and removed.
    gunnar_server::stop_record::say_previous();
    let data_root = cli.data_root.clone();
    let outcome = gunnar_server::run(cli.data_root, cli.command);
    // The box's own duties (A28 M18: the pairing loop and the twin's follower, on threads of their
    // own) and jera jobs (A32-14: the read sweeper, the live trial's watch) queue nothing more once
    // the server has returned.
    let duties = nordisk_pairing::duty::stop_all(std::time::Duration::from_secs(2)) + gunnar_common::jobs::stop_all(std::time::Duration::from_secs(2));
    if duties > 0 {
        tracing::info!("gunnar: {duties} duty(ies) stopped");
    }
    // The console queue (`gunnar_common::logqueue`) is drained by its own
    // thread; a return from `main` would take whatever it still holds.
    gunnar_common::logqueue::flush(std::time::Duration::from_secs(5));
    if let Err(err) = outcome {
        // `{err:#}` prints the whole anyhow chain — the sentence that says what
        // to do about it, not just "refusing to serve".
        shout(&format!("gunnar: {err:#}\n"));
        // **AN ORDINARY ERROR ENDS THE SAME WAY A PANIC DOES.** Until
        // 2026-09-23 only the panic hook above sent a code; an `Err` out of
        // `serve` reached this `exit(1)`, and as PID 1 that is the kernel's
        // `Attempted to kill init!` — no code, no power-off, the tick simply
        // stopped. `GN-01..04` were declared in `narrate::CODES` and never
        // called. The code is read off the last boundary the narrator crossed
        // (`narrate::code_after`), sent, waited on for the ear's acknowledgement
        // and the box powers off cleanly. Same gate as the hook: PID 1 with a
        // narrator installed, so a laptop's `gunnar-solo` still just exits.
        if appliance_pid1::net::is_init() {
            if let Some(n) = gunnar_server::narrate::narrator() {
                let stage = n.stage();
                gunnar_server::narrate::fault(
                    gunnar_server::narrate::code_after(stage),
                    stage,
                    &format!("{err:#}"),
                );
            }
        }
        std::process::exit(1);
    }
    // **A CLEAN STOP ON THE APPLIANCE POWERS OFF.** Until 2026-09-28 `main`
    // returned here, and as PID 1 that is the kernel's `Attempted to kill
    // init!` — then `panic=10` REBOOTS the box: a soft stop (the power
    // button, `appliance_pid1::runtime::stop_request`) or a SIGTERM came back up as a start.
    // Off the appliance (a laptop, a test, a container) `main` returns.
    // **A LIVE UPDATE ends the graceful stop in a RE-EXEC** (owner 2026-10-12): synced, and
    // korp-start (`/init`) again — PID 1 stays PID 1, /data stays mounted; korp-start boots the
    // staged slot. Only an execve that failed falls through to the ordinary power-off.
    #[cfg(all(feature = "live-app", target_os = "linux"))]
    if appliance_pid1::stop::on_appliance() && gunnar_server::live_update::after_stop(gunnar_server::live_update::staged()) == gunnar_server::live_update::AfterStop::ReExec {
        shout("gunnar: stopped cleanly for a LIVE UPDATE — re-exec korp-start into the staged slot\n");
        gunnar_common::logqueue::flush(std::time::Duration::from_secs(2));
        let e = gunnar_server::live_update::reexec_now();
        shout(&format!("gunnar: the live update's re-exec FAILED ({e}) — powering off as on any clean stop\n"));
    }
    if appliance_pid1::stop::after_clean_stop(appliance_pid1::stop::on_appliance()) == appliance_pid1::stop::AfterStop::PowerOff {
        shout("gunnar: stopped cleanly — the data disk is synced and remounted read-only, then the power goes\n");
        gunnar_common::logqueue::flush(std::time::Duration::from_secs(2));
        // sync(2), this process's writers released, then the data disk REMOUNTED READ-ONLY
        // — XFS's unmount record written, no recovery at the next mount (a freeze only if the
        // remount is refused) — each bounded and said (edda appliance_pid1::power); then the
        // power with no sync after it.
        let ops: std::sync::Arc<dyn appliance_pid1::power::QuiesceOps> = std::sync::Arc::new(appliance_pid1::power::Kernel);
        let steps = appliance_pid1::power::quiesce_for_power_off(ops, &data_root, std::time::Duration::from_secs(10), &|l: &str| shout(&format!("gunnar: {l}\n")));
        gunnar_common::logqueue::flush(std::time::Duration::from_secs(1));
        // The last write before the power: the steps and the power-off time, on the ESP.
        gunnar_server::stop_record::keep(&format!("gunnar: stopped cleanly — {}", steps.join("; ")));
        appliance_pid1::power::power_off_quiesced();
    }
}