1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//! 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();
}
}