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(|| {
110 std::thread::sleep(Duration::from_secs(1));
111 let _ = nix::sys::signal::kill(nix::unistd::Pid::this(), nix::sys::signal::Signal::SIGTERM);
112 std::thread::sleep(STOP_GRACE);
117 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());
118 gunnar_common::logqueue::flush(Duration::from_secs(1));
119 let e = reexec_now();
120 tracing::error!(target: "gunnar::live", "live update: the re-exec FAILED: {e}");
121 });
122 Ok(format!("staged into slot {slot:?} ({} bytes, app {}); stopping gracefully, then re-exec", st.archive.len(), st.pin_hex))
123 }
124
125 pub fn reexec_now() -> String {
128 nix::unistd::sync();
129 let argv = [std::ffi::CString::new("/init").expect("no NUL")];
130 let envp: Vec<std::ffi::CString> = std::env::vars().filter_map(|(k, v)| std::ffi::CString::new(format!("{k}={v}")).ok()).collect();
131 match live::reexec(std::path::Path::new("/init"), &argv, &envp) {
132 Ok(never) => match never {},
133 Err(e) => e,
134 }
135 }
136
137 pub fn spawn_confirm(serving: fn() -> bool) {
140 let esp = Esp::boot();
141 let on_trial = esp.read_setting().map(|s| s.trial == live::Trial::Booted).unwrap_or(false);
142 if !on_trial {
143 return;
144 }
145 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());
146 let start = std::time::Instant::now();
150 let mut since: Option<Duration> = None;
151 let mut first = true;
152 let say: gunnar_common::jobs::Say = std::sync::Arc::new(|l: &str| tracing::error!(target: "gunnar::live", "{l}"));
153 let started = gunnar_common::jobs::start(super::TRIAL_DUTY, Duration::from_secs(5), say, move || {
154 if std::mem::take(&mut first) {
155 return gunnar_common::jobs::Round::Again;
156 }
157 if since.is_none() && serving() {
158 since = Some(start.elapsed());
159 }
160 match confirm_plan(true, start.elapsed(), since) {
161 ConfirmStep::Wait | ConfirmStep::Nothing => gunnar_common::jobs::Round::Again,
162 ConfirmStep::Confirm => {
163 match live::confirm_on(&esp) {
164 Ok(s) => tracing::info!(target: "gunnar::live", event = "live.confirmed", "live update: confirmed — slot {:?} is this box's app now", s.active),
165 Err(e) => tracing::error!(target: "gunnar::live", "live update: the confirm was NOT written ({e}) — the next start rolls this slot back"),
166 }
167 gunnar_common::jobs::Round::Done
168 }
169 ConfirmStep::RollBack => {
170 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());
171 let e = reexec_now();
172 tracing::error!(target: "gunnar::live", "live update: the rollback re-exec failed: {e}");
173 gunnar_common::jobs::Round::Done
174 }
175 }
176 });
177 if let Err(e) = started {
178 tracing::error!(target: "gunnar::live", "ALERT {} no trial watch ({e}): this trial is never confirmed, and the next start rolls it back", super::TRIAL_DUTY);
179 }
180 }
181}
182
183#[cfg(all(feature = "live-app", target_os = "linux"))]
184pub use appliance::{reexec_now, running, spawn_confirm, stage};
185
186pub const TRIAL_DUTY: &str = "live-trial";
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
196 fn the_trial_watch_is_a_duty() {
197 let src = include_str!("live_update.rs");
198 let code = &src[..src.find("#[cfg(test)]").unwrap()];
199 assert!(code.contains("gunnar_common::jobs::start(super::TRIAL_DUTY"), "the watch starts as a jera job (A32-14)");
200 }
201
202 #[test]
205 fn the_new_app_confirms_after_serving_and_is_rolled_back_at_the_deadline() {
206 let s = Duration::from_secs;
207 assert_eq!(confirm_plan(false, s(9999), None), ConfirmStep::Nothing);
208 assert_eq!(confirm_plan(true, s(10), None), ConfirmStep::Wait);
209 assert_eq!(confirm_plan(true, s(30), Some(s(20))), ConfirmStep::Wait, "serving, not long enough");
210 assert_eq!(confirm_plan(true, s(90), Some(s(20))), ConfirmStep::Confirm);
211 assert_eq!(confirm_plan(true, CONFIRM_DEADLINE + s(1), None), ConfirmStep::RollBack, "never served: rolled back");
212 assert_eq!(confirm_plan(true, CONFIRM_DEADLINE + s(1), Some(CONFIRM_DEADLINE)), ConfirmStep::Wait, "serving at the deadline: it gets its minute");
213 }
214
215 #[test]
217 fn a_staged_update_reexecs_after_the_graceful_stop() {
218 assert_eq!(after_stop(true), AfterStop::ReExec);
219 assert_eq!(after_stop(false), AfterStop::PowerOff);
220 }
221}