holger-estate-front/src/main.rs
//! `holger-estate-front` — **the PID 1 of holger's front medium** (vm1). No
//! arguments: the medium's cmdline says everything (`korp.env.HOLGER_APPLIANCE_DATA=/data`,
//! `korp.env.HOLGER_NET=eth0:dhcp,eth1:dhcp`), the seed is on the data disk, the
//! installer feeds the rest.
//!
//! In order (gunnar's front's order, on the shared crates): the boot log kept
//! for the installer from the first line; as PID 1 the reaper and both NICs
//! (DHCP, renewed); the medium's seed; a fresh installation clears the previous
//! estate's pairing; the estate's clock (signed time for the boxes); the
//! installer's listener on its WireGuard link (for life:
//! after the close it is the log door); the front's role (relay + the twin's
//! certification); the paid half (monetize, the plugin door); the web face on
//! :80/:443 with the console and the repository route. As PID 1 it never
//! returns: a refusal is said on every console and the machine stays up, so
//! the installer can still collect the boot log that says why.
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use holger_estate_front as front;
// DEV-NARRATE (feature `dev-narrate`) and the STAGE lines: the holger-dev-narrate crate,
// shared with holger-server (A28 L12).
use holger_dev_narrate as dev_narrate;
use nordisk_pairing::bootlog::BootLog;
use nordisk_pairing::seed::{Kind, Seed};
/// The front's boot log, kept for the installer from the first line — with a pinned head:
/// the install, the pairing and the first boot are kept for life (nordisk-pairing seals the
/// head at the installer's close), the runtime after it is the evictable tail (Gunnar
/// f21d8d09; test3's front lost its boot lines to a headless `BootLog::new` on 2026-10-20).
static BOOT_LOG: BootLog = BootLog::for_install();
/// Say `msg` on stderr (the serial line) AND the VGA text screen — the only
/// console a cloud VM shows.
fn shout(msg: &str) {
use std::io::Write;
eprint!("{msg}");
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());
}
}
/// One line where a person sees it, never fd 2 (captured into the boot log): the serial
/// console and the VGA text screen. DEV-NARRATE's own lines.
fn console_line(line: &str) {
use std::io::Write;
for dev in ["/dev/console", "/dev/tty0"] {
if let Ok(mut f) = std::fs::OpenOptions::new().write(true).open(dev) {
let _ = f.write_all(format!("{line}\r\n").as_bytes());
}
}
}
/// Stay up, for ever: PID 1 must not exit (the kernel would panic and the boot
/// log with it).
fn idle(why: &str) -> ! {
shout(&format!("holger-estate-front: {why}\n"));
loop {
std::thread::sleep(Duration::from_secs(3600));
}
}
/// The kernel's story and a network line into the boot log (edda `narration::ship_boot_narration`).
fn narrate_boot(note: &str) {
appliance_pid1::narration::ship_boot_narration(note, &|l| BOOT_LOG.push(l));
}
/// The front's late lease, narrated into its boot log.
fn front_late_lease() -> appliance_pid1::net::LateLease {
front::pid1_fault::late_lease(narrate_boot)
}
fn main() {
BOOT_LOG.capture_esp(
Path::new(nordisk_pairing::bootlog::ESP_BOOTLOG),
"holger-estate-front",
);
// vm1's log on its own disk too (bounded), and what the last boot said before it
// stopped — a crash included — carried into this boot's log door (Gunnar 6dcbbe38).
let carried = front::keep_on_disk(&BOOT_LOG, &front::data_root());
// A panic is edda's fault with Holger's code (`HL-99`, lane G6, Gunnar's GN-99): said on
// the console, the crash record with the boot log's tail, then on the appliance a
// serving front restarts (unless it faulted within 30 min) and a booting one powers off.
front::pid1_fault::install_panic_fault(
front::data_root(),
|| BOOT_LOG.since(0, 4000).lines.join("\n").into_bytes(),
shout,
);
// fd 2 into the boot log (owner 2026-09-28: monetize's log through the log door):
// monetize-server and its guests log with eprintln!, which reached the serial line
// only. The tracing Tap writes the console capture_stderr hands back, never fd 2.
let console: Box<dyn std::io::Write + Send> = match nordisk_pairing::bootlog::capture_stderr(
&BOOT_LOG,
) {
Ok(console) => Box::new(console),
Err(e) => {
BOOT_LOG.push(&format!("holger-estate-front: fd 2 stays the console only ({e}); monetize's eprintln! lines will not reach the log door"));
Box::new(std::io::stderr())
}
};
let tap = std::sync::Mutex::new(nordisk_pairing::bootlog::Tap::new(&BOOT_LOG, console));
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
)
.with_ansi(false)
.with_writer(tap)
.init();
tracing::info!(
build = front::GIT_HASH,
"holger-estate-front: holger's FRONT medium (vm1) starts"
);
if carried > 0 {
tracing::info!(carried, file = %front::data_root().join(front::KEEP_REL).display(), "holger-estate-front: the previous boot's last lines are carried into this log (kept on the front's disk)");
}
tracing::info!(
"{}",
dev_narrate::stage_line(
"exec",
&format!(
"holger-estate-front {} dev-narrate={}",
front::GIT_HASH,
if dev_narrate::ENABLED { "on" } else { "off" }
)
)
);
// DEV builds only: the same boot log, sealed to the installer's key, over UDP from the
// moment the NIC has an address — before the installer's link (Gunnar's dev media).
#[cfg(feature = "dev-narrate")]
dev_narrate::start(
&BOOT_LOG,
front::data_root(),
"holger-estate-front",
console_line,
);
#[cfg(not(feature = "dev-narrate"))]
let _ = console_line;
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(r) => r,
Err(e) => idle(&format!("no async runtime: {e}")),
};
runtime.block_on(run())
}
/// The front's downstream interface, its sync door and the look at main's list (lane HOL-WGDS):
/// this front MAY serve WireGuard downstreams (its estate's tunnel is WireGuard). Nothing is
/// opened here (RA5-28): the interface and its public udp/51830 open only once main lists a
/// WireGuard downstream ([`serve_downstreams`]).
async fn start_downstreams(root: &Path) {
let _ = DOWNSTREAMS_ROOT.set(root.to_path_buf());
tracing::info!(udp = holger_downstream_rules::via::HUB_UDP_PORT, "front: WireGuard downstreams are served from here once main lists one; until then nothing listens");
}
/// The data root of a front that may serve WireGuard downstreams ([`start_downstreams`]).
static DOWNSTREAMS_ROOT: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
/// **Open the downstreams' interface and its sync door** (main lists a WireGuard downstream).
fn open_downstreams(
root: &Path,
vm: &Arc<front::FrontVm>,
) -> Option<Arc<front::downstreams::DownstreamHub>> {
use holger_downstream_rules::via;
let hub = match front::downstreams::identity(root).and_then(|id| {
let tun = front::downstreams::open_tun()?;
front::downstreams::DownstreamHub::start(via::HUB_UDP_PORT, id.private, tun)
.map_err(|e| format!("udp/{}: {e}", via::HUB_UDP_PORT))
}) {
Ok(h) => {
front::front_report::DOWNSTREAMS_OPEN.store(true, std::sync::atomic::Ordering::SeqCst);
Arc::new(h)
}
Err(e) => {
tracing::error!(error = %e, "front: the downstreams' interface could not be opened — no WireGuard downstream can sync (the `via: Front` ones are untouched)");
return None;
}
};
tracing::info!(interface = via::HUB_IFACE, address = %via::HUB_ADDR, udp = via::HUB_UDP_PORT, key = %hub.relay.public_b64, "front: main lists a WireGuard downstream: the downstreams' interface is up (isolated: each downstream reaches the sync door only)");
let v1 = Arc::clone(vm);
let targets: front::downstreams::Targets = Arc::new(move || {
let ip = v1.latest().and_then(|r| front::primary_addr(&r))?;
Some((
std::net::SocketAddrV4::new(ip, front::HTTP_PORT),
std::net::SocketAddrV4::new(ip, front::CONTROL_PORT),
))
});
let names = Arc::clone(&hub);
let router = front::downstreams::door(targets, Arc::new(move |ip| names.name_at(ip)));
// A32-14, stays a task: the sync door on the downstreams' interface, for the front's life.
tokio::spawn(async move {
match front::downstreams::serve_door((via::HUB_ADDR, via::DOOR_PORT).into(), router).await {
Ok(at) => {
tracing::info!(address = %at, "front: the sync door is open on the downstreams' interface")
}
Err(e) => tracing::error!(error = %e, "front: the sync door could not open"),
}
});
Some(hub)
}
/// The look at main's list, once there is a front `vm`; the interface opens at its first
/// WireGuard downstream ([`open_downstreams`]).
async fn serve_downstreams(vm: &Arc<front::FrontVm>) {
let Some(root) = DOWNSTREAMS_ROOT.get().cloned() else {
return;
};
let (v1, v2) = (Arc::clone(vm), Arc::clone(vm));
let start: front::downstreams::HubStart = Arc::new(move || open_downstreams(&root, &v1));
let main: front::downstreams::MainNow = Arc::new(move || {
let reply = v2.latest()?;
let ip = front::primary_addr(&reply)?;
let bearer = front::console_key::bearers(&reply)
.ok()
.flatten()
.and_then(|b| {
front::primary_control(&reply).and_then(|c| b.at(&c).map(str::to_string))
});
Some((std::net::SocketAddrV4::new(ip, front::HTTP_PORT), bearer))
});
front::downstreams::follow_lazily(start, main, front::downstreams::POLL);
}
async fn run() -> ! {
let root = front::data_root();
// ── PID 1's duties: the reaper, the network (nothing else brings one up) ──
if appliance_pid1::net::is_init() {
// The previous stop's record (kept on the ESP), said once and removed.
appliance_pid1::stop_record::say_previous("holger", &|l| tracing::info!("{l}"));
// Whether this /data survived the last install (korp-installer's record on the ESP), once.
appliance_pid1::install::announce_install_mode(None, &|l| tracing::info!("{l}"));
// A32-14, stays a task: PID 1's zombie reaper, for the box's life.
tokio::spawn(appliance_pid1::runtime::reaper());
let spec = std::env::var("HOLGER_NET")
.ok()
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| "eth0:dhcp,eth1:dhcp".to_string());
match appliance_pid1::net::NetConfig::parse_list(&spec) {
Ok(plans) => {
// A7 row 29: the resolver proven on a name, asked again for about 30 s on a miss.
let how = appliance_pid1::net::Bringup {
hostname_prefix: "holger-front",
resolver_probe: Some("crates.io"),
narrate: &narrate_boot,
};
// A late lease (the utility NIC's slow first DHCP) is narrated as the boot's own.
match appliance_pid1::net::bring_up_network_with(plans, &how, front_late_lease()) {
Ok(sessions) => {
let pending: Vec<&str> = ["eth0", front::UTILITY_INTERFACE]
.into_iter()
.filter(|i| appliance_pid1::net::dhcp_pending(i))
.collect();
if pending.is_empty() {
tracing::info!(
leases = sessions.len(),
"front: the declared interfaces are up"
);
} else {
tracing::warn!(leases = sessions.len(), pending = ?pending, "front: NOT every declared interface is up — PID 1 asks DHCP again in the background for the pending ones");
}
tracing::info!(
"{}",
dev_narrate::stage_line(
"network",
&format!("{} DHCP lease(s) held", sessions.len())
)
);
appliance_pid1::fault::reached("network");
for s in sessions {
// A32-14, stays a task: PID 1's lease renewal, one per lease, for the box's life.
tokio::spawn(appliance_pid1::runtime::dhcp_renewal(s, None));
}
}
Err(e) => {
tracing::error!(error = %format!("{e:#}"), "front: the network could not be brought up — the installer cannot reach this VM")
}
}
}
Err(e) => {
tracing::error!(error = %format!("{e:#}"), "REFUSED [holger-net] HOLGER_NET = {spec:?}")
}
}
}
// A utility NIC whose boot DHCP failed (UpCloud's DHCP is slow on a first boot: test3-holger-rs
// r6's front never had eth1, lane D12) is waited for while PID 1 asks again in the
// background — Gunnar's front: the boxes dial the relay on this address.
if appliance_pid1::net::interface_ipv4(front::UTILITY_INTERFACE)
.ok()
.flatten()
.is_none()
&& appliance_pid1::net::dhcp_pending(front::UTILITY_INTERFACE)
{
// Said by name with its start, once a minute (holger-live `waits`, test3-holger-rs 2026-10-22).
let mut wait = holger_live::waits::Wait::new(
"the front",
&format!(
"its utility address on {} (PID 1 asks DHCP again in the background)",
front::UTILITY_INTERFACE
),
);
while appliance_pid1::net::interface_ipv4(front::UTILITY_INTERFACE)
.ok()
.flatten()
.is_none()
&& appliance_pid1::net::dhcp_pending(front::UTILITY_INTERFACE)
{
if let Some(l) = wait.due() {
tracing::warn!("front: {l}");
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
tracing::info!("front: {}", wait.done());
}
let utility = appliance_pid1::net::interface_ipv4(front::UTILITY_INTERFACE)
.ok()
.flatten();
tracing::info!(interface = front::UTILITY_INTERFACE, utility = ?utility, "front: the utility address");
// ── the medium's seed: without it there is no installer to answer ────────
// A release medium stages only the metadata marker: its seed is the shop's, in user_data.
// The ONE seed reader (Skidbladnir nordisk-pairing `seed::metadata`, P4a; A28 L12).
let seed: Seed = match nordisk_pairing::seed::metadata::read(&root.join(front::SEED_REL)) {
Err(e) => idle(&e),
Ok(None) => idle(&format!("no pairing seed at {} — this medium was composed without `--param pairing_seed=@<seed.toml>`, so no installer can reach it", root.join(front::SEED_REL).display())),
Ok(Some(s)) if s.kind == Kind::Front => s,
Ok(Some(s)) => idle(&format!("REFUSED [pairing-seed-kind] the staged seed is for a `{:?}` box; this is holger's FRONT medium", s.kind)),
};
// A keep boot of the SAME estate keeps the feed, the key and the certificate (lane G5).
match front::first_boot(
Path::new("/boot"),
&root,
&seed,
&nordisk_pairing::boxside::mark_esp,
) {
Some(c) if c.is_empty() => {
tracing::info!(estate = %seed.estate, "front: a keep boot of the same estate onto its kept data — the installer's feed, this box's key and certificate are KEPT")
}
Some(cleared) => {
tracing::warn!(cleared = ?cleared, "front: the first boot of this installation — it starts UNPAIRED")
}
None => {}
}
// ── an estate front is WireGuard-only (lane E1): its own overlay interface ──
// A WireGuard seed is an estate: the front opens its overlay interface (the
// relay carries it) and reaches main and the twin at their overlay addresses only.
if seed.tunnel == nordisk_pairing::seed::TunnelKind::Wireguard {
front::wg_only::set_wg_only(true);
match front::wg_only::open_hub() {
Ok(at) => {
tracing::info!(interface = front::wg_only::HUB_IFACE, address = %at, "front: the overlay interface is up — the boxes are reached over the tunnel only");
// The lean estate (owner 2026-10-23): main and the twin hold no public IPv4 and
// leave through this front — its egress door, on its overlay address only.
let door = std::net::SocketAddr::from((
nordisk_pairing::OVERLAY_FRONT,
nordisk_pairing::egress::EGRESS_PORT,
));
match nordisk_pairing::egress::serve(door, nordisk_pairing::egress::Policy::OUT) {
Ok(at) => {
tracing::info!(address = %at, "front: the egress door is open — the boxes behind this front leave through it (TLS ports, outside addresses only)")
}
Err(e) => {
tracing::error!(error = %e, "front: the egress door could not open — a box with no public IPv4 reaches nothing outside the estate")
}
}
}
Err(e) => {
tracing::error!(error = %e, "front: the overlay interface could not be opened — the front cannot reach a box over the tunnel, and dials no other way")
}
}
}
// ── the downstreams' own interface (lane HOL-WGDS): `holger-ds`, its own subnet and UDP
// port, never the estate's inner tunnel; a downstream peer reaches main's sync door only ──
if seed.tunnel == nordisk_pairing::seed::TunnelKind::Wireguard {
start_downstreams(&root).await;
}
// The console's key (A7 row 14): minted once and kept; every box derives its console login
// from its public half, offered below.
match front::console_key::adopt(&root) {
Ok(_) => tracing::info!(
"front: the console's key is ready; its public half is offered to every box"
),
Err(e) => {
tracing::error!(error = %e, "front: the console's key cannot be minted — the console reaches no box")
}
}
#[allow(unused_mut)]
let mut product = front::front::HolgerFront::new(&root, seed.tunnel);
// The front's own live slot (L20): on a korp appliance, its A/B app slots.
#[cfg(target_os = "linux")]
if holger_live::appliance::present() {
product = product.with_live(Arc::new(holger_live::appliance::slot()));
}
let vm: Arc<front::FrontVm> =
match nordisk_pairing::frontside::Front::open(&root, seed.clone(), product) {
Ok(v) => Arc::new(v),
Err(e) => idle(&format!("this box cannot prepare its own key: {e}")),
};
// ── the clock: its key and peer are offered, so start it first ─────────
// The estate's root of time (edda `estate-clock`, gunnar's pattern): this
// front keeps its own clock from the medium's `clock` sources
// (`korp.env.ESTATE_CLOCK`) and serves signed time on `<overlay>:50123` on an
// estate (`<utility>:50123` off one);
// every holger box takes `clock_peer` + `clock_key` from the offer.
let say_clock: Arc<dyn Fn(&str) + Send + Sync> =
Arc::new(|l: &str| tracing::info!("clock: {l}"));
// On an estate the clock is served on the front's overlay address (lane E1): the
// boxes take their time over wg0 too, never over the utility network.
let clock_at = if front::wg_only::wg_only() {
front::wg_only::hub().map(|_| nordisk_pairing::OVERLAY_FRONT)
} else {
utility
};
match estate_clock::offer::start(&root, clock_at, &seed.installer_ca_pem, say_clock) {
Ok(offer) => {
tracing::info!(key = %offer.key_line, peer = ?offer.peer, "front: the clock is started and offered");
vm.product.clock_started(offer);
}
Err(e) => {
tracing::error!(error = %e, "front: the clock could not start — the boxes will be fed no clock")
}
}
// ── the installer's listener (for life), and the front's role ────────────
// Each listener line at its level (nordisk-pairing's Tone, Gunnar 3eb19cd3): a refusal
// is a WARN, the listener doing its job (the close, the log door, a reader going away)
// an INFO — the untoned serve said every line at WARN.
let say: nordisk_pairing::boxside::Say =
Arc::new(|t: nordisk_pairing::bootstrap::Tone, l: &str| match t {
nordisk_pairing::bootstrap::Tone::Info => tracing::info!("installer: {l}"),
nordisk_pairing::bootstrap::Tone::Warn => tracing::warn!("installer: {l}"),
});
// A front the installer closed on an earlier start serves the log door only
// (the install is not served again after a restart).
let serve = if vm.closed() {
nordisk_pairing::boxside::serve_installer_after_close_toned
} else {
nordisk_pairing::boxside::serve_installer_toned
};
// The installer sees the front with its renewal steps (lane E2): the certificate it
// renews at two thirds of its life is kept, and so is the fresh bootstrap handed with it.
// Its pair status names the twin's certify attempts while they fail (lane D10).
let certify_root = root.clone();
let served: Arc<dyn nordisk_pairing::bootstrap::Vm> = Arc::new(
front::renewing::Renewing::new(Arc::clone(&vm), &root, front::SEED_REL, &seed).with_status(
Arc::new(move |s| {
front::certify::with_certify(
s,
front::certify::last_failure(&certify_root).as_ref(),
)
}),
),
);
if let Err(e) = serve(served, seed.clone(), &BOOT_LOG, say) {
tracing::error!(error = %e, "installer: the listener's thread could not start");
}
tracing::info!(
"{}",
dev_narrate::stage_line(
"pairing",
"the seed is read and the installer's listener serves"
)
);
appliance_pid1::fault::reached("pairing");
front::role::start(
Arc::clone(&vm),
tokio::runtime::Handle::current(),
front::role::RoleConfig::default(),
);
// Lane HOL-WGDS: the sync door and main's list of WireGuard downstreams.
serve_downstreams(&vm).await;
// The front tells main its doors and routes (lane HOL-FLOW-FRONT): main's flow unfolds it.
{
let r = Arc::clone(&vm);
let now: front::front_report::Now = Arc::new(move || {
let reply = r.latest()?;
let at = front::primary_door(&reply)?.parse().ok()?;
let key = front::front_report::main_key(&reply)?;
Some((reply.feed.clone(), reply.me.name.clone(), at, key))
});
front::front_report::start(
seed.estate.clone(),
now,
Arc::new(|| {
front::front_report::DOWNSTREAMS_OPEN.load(std::sync::atomic::Ordering::SeqCst)
}),
cfg!(feature = "console"),
);
}
// Every paired link renewed before it ends (lane E2): jera jobs on the front's own ledger.
front::renew::start(Arc::clone(&vm), root.clone());
// WireGuard-only: the front tries a plain-IP connection to each box, and the tunnel, and
// says what it saw (jera `front.mesh_probe` looks, lane W9; Gunnar's mesh probe).
if front::wg_only::wg_only() {
front::mesh_probe::start(Arc::clone(&vm), &root);
}
// A re-laid main that awaits its twin is filled from it by this front when the estate fed
// `repair_auto = true` (jera `front.autofill` looks, lane G11; Gunnar's autofill).
front::autofill::start(Arc::clone(&vm), &root);
// A live update on trial: confirmed once the web face served for a minute, else rolled back
// (jera `live.trial` jobs on the front's own live ledger, L21).
if let Some(slot) = vm.product.live_slot().cloned() {
front::live::watch_trial(
slot,
|| {
std::net::TcpStream::connect_timeout(
&(std::net::Ipv4Addr::LOCALHOST, estate_front::web::HTTPS_PORT).into(),
Duration::from_secs(2),
)
.is_ok()
},
&root,
);
}
// ── the paid half: the ONE monetize's SoldEstate door (no monetize runs here) ──
#[cfg(feature = "metered")]
front::sold::start(Arc::clone(&vm), root.clone());
// ── the web face on the public 80/443: the front holds its certificate ──
let fed = Arc::clone(&vm);
let feed: estate_front::web::Feed =
Arc::new(move || fed.latest().map(|r| r.feed).unwrap_or_default());
let any = std::net::Ipv4Addr::UNSPECIFIED;
// The shop's bearer swapped for the primary's console bearer on EVERY route of :443 (the
// console's pass-through and the repository route), each swap audited (lane OX-WGDS3).
#[cfg(feature = "console")]
let swap = front::console::Swap::default();
#[cfg(feature = "console")]
let hooks = estate_front::web::Hooks {
outer: Some(front::console::swap_layer(swap.clone())),
tls: Some(front::tls()),
..estate_front::web::Hooks::default()
};
#[cfg(not(feature = "console"))]
let hooks = estate_front::web::Hooks {
tls: Some(front::tls()),
..estate_front::web::Hooks::default()
};
match estate_front::web::start_with(
&root,
seed.estate.clone(),
feed,
(any, estate_front::web::HTTP_PORT).into(),
(any, estate_front::web::HTTPS_PORT).into(),
hooks,
)
.await
{
Ok(web) => {
tracing::info!("front: the web face is served on :80 and :443");
tracing::info!(
"{}",
dev_narrate::stage_line("serving", "the web face on :80 and :443")
);
// From here a panic restarts the front (unless it loops): edda appliance_pid1::fault.
appliance_pid1::fault::serving_now();
// The repository: `repo_hosts` carried to the primary's HTTP and gRPC.
let r = Arc::clone(&vm);
// On an estate the repository route reaches the primary over wg0 (lane E1).
let now: estate_front::route::Now = Arc::new(move || {
r.latest()
.map(|reply| (reply.feed.clone(), front::primary_addr(&reply)))
});
estate_front::route::start(
web.app.clone(),
now,
seed.estate.clone(),
front::PRODUCT,
Some((front::HTTP_PORT, front::CONTROL_PORT)),
front::CARRIED_GRPC,
);
#[cfg(feature = "console")]
{
let l = Arc::clone(&vm);
front::console::start_swapping(
web,
Arc::new(move || l.latest()),
root.clone(),
swap,
);
}
#[cfg(not(feature = "console"))]
let _ = web;
}
Err(e) => tracing::error!(error = %e, "front: the web face cannot be served"),
}
// As PID 1: a signal OR the ACPI power button (a cloud's soft stop, which
// is never a signal) stops the front — synced and powered off, never an
// exit (PID 1 exiting is `Attempted to kill init!`, and `panic=10` brings
// back the box it was asked to stop).
if appliance_pid1::net::is_init() {
let why = appliance_pid1::runtime::shutdown_request().await;
// The front's own duties (A28 M18) queue nothing more from here.
tracing::info!("front: {} duty(ies) stopped", holger_live::duty::stop_all());
tracing::warn!(request = why, "front: shutting down; the data disk quiesced, the stop kept on the ESP, then the power (or a re-exec into a staged live update)");
// A7 row 18 (Gunnar f8594dd6 / 812f4ffc / dcf1d82f): quiesce /data bounded at 10 s,
// keep the stop on the ESP, then the power with no sync after the remount.
let said = format!("holger-estate-front: stopped cleanly ({why})");
// The power only on the appliance (lane G6): PID 1 in a container cannot `reboot(2)`,
// and a refused power-off parks the container instead of ending it.
if appliance_pid1::stop::after_clean_stop(appliance_pid1::stop::on_appliance())
== appliance_pid1::stop::AfterStop::PowerOff
{
let data = front::data_root();
#[cfg(target_os = "linux")]
holger_live::appliance::end_of_stop(|| -> () {
appliance_pid1::power::power_off_cleanly("holger", &data, &said, &|l| {
tracing::info!("{l}")
})
});
#[cfg(not(target_os = "linux"))]
appliance_pid1::power::power_off_cleanly("holger", &data, &said, &|l| {
tracing::info!("{l}")
});
}
tracing::warn!("{said}; not the appliance — the process exits");
std::process::exit(0);
}
loop {
tokio::time::sleep(Duration::from_secs(3600)).await;
}
}