1use std::time::Duration;
14
15pub const CONFIRM_AFTER: Duration = Duration::from_secs(60);
17pub const CONFIRM_DEADLINE: Duration = Duration::from_secs(600);
19pub const STOP_GRACE: Duration = Duration::from_secs(30);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ConfirmStep {
25 Nothing,
27 Wait,
29 Confirm,
31 RollBack,
33}
34
35pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum AfterStop {
52 PowerOff,
54 ReExec,
56}
57
58pub fn after_stop(staged: bool) -> AfterStop {
61 if staged {
62 AfterStop::ReExec
63 } else {
64 AfterStop::PowerOff
65 }
66}
67
68static STAGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
70
71pub 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 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 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 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 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 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 #[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 #[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}