Skip to main content

gunnar_server/
live_update.rs

1//! **Live update, gunnar's side** (owner 2026-10-12, TURNAROUND §8) — generic primitives only;
2//! the decisions are korp-installer's (`korp_installer::live`), the wire is nordisk-pairing's
3//! (`nordisk_pairing::live`, the installer's log door, its own certificate only):
4//!
5//! * [`running`] — what this box runs: BASE, app pin, slot, the off switch.
6//! * [`stage`] — a new signed app verified and written to the slot NOT running; then this
7//!   process stops GRACEFULLY (the ordinary stop) and, instead of powering off, re-execs
8//!   korp-start ([`after_stop`]): PID 1 stays PID 1, `/data` stays mounted, korp-start boots the
9//!   staged slot through its own checks.
10//! * [`spawn_confirm`] — the new app confirms once it serves ([`confirm_plan`]); a slot still
11//!   unconfirmed at the deadline re-execs korp-start, which rolls it back.
12
13use std::time::Duration;
14
15/// How long the new app serves before it confirms its slot.
16pub const CONFIRM_AFTER: Duration = Duration::from_secs(60);
17/// The health deadline: a trial slot not serving by then is rolled back.
18pub const CONFIRM_DEADLINE: Duration = Duration::from_secs(600);
19/// How long the graceful stop of a live update has before the staging thread re-execs itself.
20pub const STOP_GRACE: Duration = Duration::from_secs(30);
21
22/// What the trial watcher does next ([`confirm_plan`]).
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ConfirmStep {
25    /// Nothing on trial: nothing to do.
26    Nothing,
27    /// Not serving long enough yet: look again.
28    Wait,
29    /// Serving for [`CONFIRM_AFTER`]: confirm the slot.
30    Confirm,
31    /// Not serving by [`CONFIRM_DEADLINE`]: re-exec korp-start (it rolls back).
32    RollBack,
33}
34
35/// Pure: **the trial watcher's step** — `on_trial` (the setting says `booted`), how long this
36/// process has run, and since when it serves (`None`: not yet).
37pub fn confirm_plan(on_trial: bool, up: Duration, serving_since: Option<Duration>) -> ConfirmStep {
38    if !on_trial {
39        return ConfirmStep::Nothing;
40    }
41    match serving_since {
42        Some(since) if up.saturating_sub(since) >= CONFIRM_AFTER => ConfirmStep::Confirm,
43        Some(_) => ConfirmStep::Wait,
44        None if up > CONFIRM_DEADLINE => ConfirmStep::RollBack,
45        None => ConfirmStep::Wait,
46    }
47}
48
49/// What a clean stop ends in.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum AfterStop {
52    /// The ordinary end on the appliance.
53    PowerOff,
54    /// A live update is staged: re-exec korp-start.
55    ReExec,
56}
57
58/// Pure: **after the graceful stop** — a staged live update re-execs, everything else powers
59/// off as before.
60pub fn after_stop(staged: bool) -> AfterStop {
61    if staged {
62        AfterStop::ReExec
63    } else {
64        AfterStop::PowerOff
65    }
66}
67
68/// A live update staged in this process: the stop ends in a re-exec ([`after_stop`]).
69static STAGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
70
71/// Whether a live update waits for the stop.
72pub fn staged() -> bool {
73    STAGED.load(std::sync::atomic::Ordering::SeqCst)
74}
75
76#[cfg(all(feature = "live-app", target_os = "linux"))]
77mod appliance {
78    use super::*;
79    use korp_installer::live::{self, Esp};
80
81    fn policy() -> live::Policy {
82        let cl = korp_installer::cmdline::effective();
83        let signed = korp_installer::cmdline::decide(&cl, std::env::var(korp_installer::cmdline::REQUIRE_SIGNED_ENV).ok().as_deref()).require_signed;
84        live::Policy { live_update: cl.live_update_on(), require_signed: signed }
85    }
86
87    /// **What this box runs** (`GET /live/running`).
88    pub fn running() -> Result<nordisk_pairing::live::Running, String> {
89        let esp = Esp::boot();
90        let s = esp.read_setting()?;
91        let pin = std::fs::read_to_string(esp.records(s.active).pin).map_err(|e| format!("[live-running] the running slot's pin: {e}"))?;
92        Ok(nordisk_pairing::live::Running { base: live::running_base(&esp)?, app: pin.trim().to_string(), slot: live::render_setting(&s).trim().to_string(), live_update: policy().live_update })
93    }
94
95    /// **A new app** (`POST /live/stage`): verified against this box's enrolled root, floor and
96    /// BASE, written to the slot not running, the setting last; then the GRACEFUL stop is
97    /// asked (SIGTERM to PID 1 itself, a second from now, after this answer left) and the
98    /// stop ends in the re-exec.
99    pub fn stage(st: &nordisk_pairing::live::Stage) -> Result<String, String> {
100        let esp = Esp::boot();
101        let pubkey = std::fs::read(esp.root.join("korp/root.pub")).ok().and_then(|b| <[u8; 32]>::try_from(b.as_slice()).ok());
102        let floor = korp_installer::rollback::FloorStore::floor(&mut korp_installer::rollback::FloorFile::at(esp.root.join("korp/rollback.floor"))).map_err(|e| format!("[live-floor] {e:#}"))?;
103        let base = live::running_base(&esp)?;
104        let p = live::LivePayload { archive: &st.archive, pin_hex: &st.pin_hex, signature: Some(&st.signature), declared_base: Some(&st.base) };
105        let slot = live::stage_on(&esp, &p, pubkey.as_ref(), floor, &base, policy(), &mut live::write_slot_partition)?;
106        STAGED.store(true, std::sync::atomic::Ordering::SeqCst);
107        tracing::warn!(target: "gunnar::live", event = "live.staged", slot = ?slot, app = %st.pin_hex, "live update: a new app is staged in slot {slot:?} — this box stops gracefully and re-execs korp-start into it");
108        std::thread::spawn(|| {
109            std::thread::sleep(Duration::from_secs(1));
110            let _ = nix::sys::signal::kill(nix::unistd::Pid::this(), nix::sys::signal::Signal::SIGTERM);
111            // The graceful stop has STOP_GRACE to reach `main`'s re-exec. A process that is
112            // still here after it (MEASURED on the dwarf mock: the listeners shut down and the
113            // runtime never let go) is re-exec'd from this thread: the listeners are down, the
114            // writers were given their grace, the disks are synced — execve replaces every thread.
115            std::thread::sleep(STOP_GRACE);
116            tracing::warn!(target: "gunnar::live", event = "live.reexec_after_grace", "live update: the graceful stop did not reach its end within {} s — synced, re-exec from here", STOP_GRACE.as_secs());
117            gunnar_common::logqueue::flush(Duration::from_secs(1));
118            let e = reexec_now();
119            tracing::error!(target: "gunnar::live", "live update: the re-exec FAILED: {e}");
120        });
121        Ok(format!("staged into slot {slot:?} ({} bytes, app {}); stopping gracefully, then re-exec", st.archive.len(), st.pin_hex))
122    }
123
124    /// **The re-exec** after the graceful stop: synced, then korp-start (`/init`) again. Only
125    /// returns when the execve failed.
126    pub fn reexec_now() -> String {
127        nix::unistd::sync();
128        let argv = [std::ffi::CString::new("/init").expect("no NUL")];
129        let envp: Vec<std::ffi::CString> = std::env::vars().filter_map(|(k, v)| std::ffi::CString::new(format!("{k}={v}")).ok()).collect();
130        match live::reexec(std::path::Path::new("/init"), &argv, &envp) {
131            Ok(never) => match never {},
132            Err(e) => e,
133        }
134    }
135
136    /// **The trial watcher**: on a slot started on trial, confirm once the app has served for
137    /// [`CONFIRM_AFTER`]; not serving by [`CONFIRM_DEADLINE`], re-exec (the rollback).
138    pub fn spawn_confirm(serving: fn() -> bool) {
139        let esp = Esp::boot();
140        let on_trial = esp.read_setting().map(|s| s.trial == live::Trial::Booted).unwrap_or(false);
141        if !on_trial {
142            return;
143        }
144        tracing::warn!(target: "gunnar::live", event = "live.trial", "live update: this app runs on TRIAL — it confirms after serving {} s, or is rolled back at {} s", CONFIRM_AFTER.as_secs(), CONFIRM_DEADLINE.as_secs());
145        std::thread::spawn(move || {
146            let start = std::time::Instant::now();
147            let mut since: Option<Duration> = None;
148            loop {
149                std::thread::sleep(Duration::from_secs(5));
150                if since.is_none() && serving() {
151                    since = Some(start.elapsed());
152                }
153                match confirm_plan(true, start.elapsed(), since) {
154                    ConfirmStep::Wait | ConfirmStep::Nothing => continue,
155                    ConfirmStep::Confirm => {
156                        match live::confirm_on(&esp) {
157                            Ok(s) => tracing::info!(target: "gunnar::live", event = "live.confirmed", "live update: confirmed — slot {:?} is this box's app now", s.active),
158                            Err(e) => tracing::error!(target: "gunnar::live", "live update: the confirm was NOT written ({e}) — the next start rolls this slot back"),
159                        }
160                        return;
161                    }
162                    ConfirmStep::RollBack => {
163                        tracing::error!(target: "gunnar::live", event = "live.rollback", "live update: the new app did not serve within {} s — re-exec, korp-start rolls back", CONFIRM_DEADLINE.as_secs());
164                        let e = reexec_now();
165                        tracing::error!(target: "gunnar::live", "live update: the rollback re-exec failed: {e}");
166                        return;
167                    }
168                }
169            }
170        });
171    }
172}
173
174#[cfg(all(feature = "live-app", target_os = "linux"))]
175pub use appliance::{reexec_now, running, spawn_confirm, stage};
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    /// ★ The new app confirms only after it SERVED for a while; one that never serves is
182    /// rolled back at the deadline; nothing on trial does nothing.
183    #[test]
184    fn the_new_app_confirms_after_serving_and_is_rolled_back_at_the_deadline() {
185        let s = Duration::from_secs;
186        assert_eq!(confirm_plan(false, s(9999), None), ConfirmStep::Nothing);
187        assert_eq!(confirm_plan(true, s(10), None), ConfirmStep::Wait);
188        assert_eq!(confirm_plan(true, s(30), Some(s(20))), ConfirmStep::Wait, "serving, not long enough");
189        assert_eq!(confirm_plan(true, s(90), Some(s(20))), ConfirmStep::Confirm);
190        assert_eq!(confirm_plan(true, CONFIRM_DEADLINE + s(1), None), ConfirmStep::RollBack, "never served: rolled back");
191        assert_eq!(confirm_plan(true, CONFIRM_DEADLINE + s(1), Some(CONFIRM_DEADLINE)), ConfirmStep::Wait, "serving at the deadline: it gets its minute");
192    }
193
194    /// ★ A staged live update ends the graceful stop in a re-exec, never a power-off.
195    #[test]
196    fn a_staged_update_reexecs_after_the_graceful_stop() {
197        assert_eq!(after_stop(true), AfterStop::ReExec);
198        assert_eq!(after_stop(false), AfterStop::PowerOff);
199    }
200}