diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index c4176bc5d..5c86061d8 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -2977,7 +2977,9 @@ impl Engine { node_synced: st.node.synced && st.clock.severity != "block", boundary_eta_s: if st.node.daa > 0 && st.program.boundary_daa > 0 { Some(st.program.eta_s) } else { None }, // a remote job in progress counts as busy: no update applies under it (src/jobrun.rs) - miner_busy: self.miners.iter().any(|m| m.building) || st.mining.cards.iter().any(|c| c.enabled && c.state == "starting") || self.jobs.active(), + miner_busy: self.miners.iter().any(|m| m.building) || st.mining.cards.iter().any(|c| c.enabled && c.state == "starting"), + // a remote job in progress holds the update, urgent or not (src/manifest.rs safe_to_apply; PC 1, 6 October 2026) + job_active: self.jobs.active(), daa: st.node.daa, } }; diff --git a/app/igneum-app/src/manifest.rs b/app/igneum-app/src/manifest.rs index 8ad020f81..15b883324 100644 --- a/app/igneum-app/src/manifest.rs +++ b/app/igneum-app/src/manifest.rs @@ -300,6 +300,10 @@ pub struct Moment { pub boundary_eta_s: Option, /// A worker is starting, exporting a pack or being built: let it finish. pub miner_busy: bool, + /// A remote job is running on this engine (src/jobrun.rs). It holds even an urgent install: a job is bounded by its + /// cap, and an install under it kills its process tree (PC 1, 6 October 2026, 17:52:54Z: 0.3.14 went in during a + /// measurement job because install_asked from a two-hour-old update-now still counted as urgent). + pub job_active: bool, /// How long the update has been ready and waiting. pub ready_for_s: u64, /// A consensus activation is close, or this version is below min_supported_version: now beats later. @@ -320,6 +324,9 @@ pub fn slot_minute(id8: &str) -> u64 { /// Ok when the update may be applied now; Err carries the reason to wait, in the words the dashboard shows. pub fn safe_to_apply(m: &Moment) -> Result<(), String> { + if m.job_active { + return Err("a remote job is running; installing when it closes".into()); + } if m.urgent { return Ok(()); } @@ -492,7 +499,7 @@ mod tests { #[test] fn safe_moments() { - let base = Moment { node_synced: true, boundary_eta_s: Some(1800), miner_busy: false, ready_for_s: 60, urgent: false, slot_ok: true, network_drop_pct: 0.0 }; + let base = Moment { node_synced: true, boundary_eta_s: Some(1800), miner_busy: false, job_active: false, ready_for_s: 60, urgent: false, slot_ok: true, network_drop_pct: 0.0 }; assert!(safe_to_apply(&base).is_ok()); assert_eq!(safe_to_apply(&Moment { node_synced: false, ..base.clone() }).unwrap_err(), "waiting for the node to sync"); assert!(safe_to_apply(&Moment { boundary_eta_s: Some(120), ..base.clone() }).unwrap_err().contains("boundary in 120 s")); @@ -502,6 +509,11 @@ mod tests { assert!(safe_to_apply(&Moment { miner_busy: true, ..base.clone() }).unwrap_err().contains("worker")); // urgent beats every wait assert!(safe_to_apply(&Moment { node_synced: false, boundary_eta_s: Some(5), miner_busy: true, urgent: true, ..base.clone() }).is_ok()); + // PC 1, 6 October 2026, 17:52:54Z: a scheduled update arriving mid-job is deferred to the job's close, urgent or not + assert!(safe_to_apply(&Moment { job_active: true, ..base.clone() }).unwrap_err().contains("remote job")); + assert!(safe_to_apply(&Moment { job_active: true, urgent: true, ..base.clone() }).unwrap_err().contains("remote job")); + assert!(safe_to_apply(&Moment { job_active: true, urgent: true, slot_ok: false, ..base.clone() }).is_err()); + assert!(safe_to_apply(&Moment { job_active: false, urgent: true, slot_ok: false, ..base.clone() }).is_ok()); // patience: an unsynced node for 6 h applies anyway assert!(safe_to_apply(&Moment { node_synced: false, ready_for_s: SAFE_MOMENT_PATIENCE_S, ..base.clone() }).is_ok()); assert!(safe_to_apply(&Moment { node_synced: false, ready_for_s: SAFE_MOMENT_PATIENCE_S - 1, ..base.clone() }).is_err()); diff --git a/app/igneum-app/src/ota.rs b/app/igneum-app/src/ota.rs index f10563d27..196e2983d 100644 --- a/app/igneum-app/src/ota.rs +++ b/app/igneum-app/src/ota.rs @@ -70,6 +70,8 @@ pub struct Ctx { pub node_synced: bool, pub boundary_eta_s: Option, pub miner_busy: bool, + /// A remote job is running (src/jobrun.rs): the install holds, urgent or not. + pub job_active: bool, pub daa: u64, } @@ -539,7 +541,7 @@ impl Updater { } let slot_ok = minute == self.slot || catch_up || std::env::var("IGNEUM_APP_UPDATE_NO_SLOT").map(|v| v == "1").unwrap_or(false); let ready_for = self.ready_since.map(|t| now.duration_since(t).as_secs()).unwrap_or(0); - let moment = Moment { node_synced: ctx.node_synced, boundary_eta_s: ctx.boundary_eta_s, miner_busy: ctx.miner_busy, ready_for_s: ready_for, urgent: urgent || self.install_asked, slot_ok, network_drop_pct }; + let moment = Moment { node_synced: ctx.node_synced, boundary_eta_s: ctx.boundary_eta_s, miner_busy: ctx.miner_busy, job_active: ctx.job_active, ready_for_s: ready_for, urgent: urgent || self.install_asked, slot_ok, network_drop_pct }; if !self.auto && !urgent && !self.install_asked { shared.state.lock().unwrap().update.wait = "waiting for Install now (automatic updates are off)".into(); return None; @@ -668,6 +670,12 @@ impl Updater { shared.log(&format!("update check: {} is published but has no {} build yet", m.version, manifest::platform_name())); } else { shared.log(&format!("update check: {} is current (manifest {})", self.current, m.version)); + // an asked install (update-now) covers this one check: nothing newer, so the ask is spent. + // Left true it made the NEXT manifest urgent hours later (PC 1, 6 October 2026, 17:52:54Z). + if self.install_asked { + self.install_asked = false; + shared.log("update check: Install now asked and nothing newer is published; the ask is spent (the next manifest takes the usual slot)"); + } } self.entry = None; self.file = None;