From b86fcbe6b51ef9a8db7484f4560e78982eca3ceb Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:11:14 +0000 Subject: [PATCH] v2.0.0: a remote app restart under the window host is the host's ladder, never a helper racing it (PC 1, 6 October 2026 19:10Z and 8 October 2026 14:50Z: the restart kind quit the engine and left a hidden PowerShell to Start-Process igneum-app.exe --launch 8 s later, racing the host's own restart ladder for the single instance; it lost both times, "the relaunch helper did not bring it back", the app down for hours with the whole job queue behind it). Now the engine decides (engine::restart_plan): under the host it sets the quit source "remote job: app restart" (a plain EXIT, so the host restarts it in about 10 s; an update exit would end the host) and starts nothing; with no host (--boot, a headless start) it starts the relaunch helper itself and keeps running when the helper cannot start. The job runner's restart arm no longer spawns the helper. Test known-failed first: engine::tests::a_remote_app_restart_under_the_host_uses_the_hosts_ladder_and_no_helper Co-Authored-By: Claude Fable 5.1 --- app/igneum-app/src/engine.rs | 45 +++++++++++++++++++++++++++++++++++- app/igneum-app/src/jobrun.rs | 13 ++++------- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index 3271796d1..1e9b1040a 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -4433,7 +4433,20 @@ impl Engine { } } Action::RestartApp => { - self.shared.event("info", "remote job: the app restarts (miners stop, then the node, then it opens again)"); + // v2.0.0 (PC 1, 6 and 8 October 2026): under the window host the engine exits with a plain EXIT and the + // host's ladder starts it again in 10 s; a detached helper raced that ladder for the single instance and + // lost twice, the app down for hours. The helper only when no host runs (--boot, a headless start). + match restart_plan(self.wrapper) { + RestartPlan::HostLadder => self.shared.event("info", "remote job: the app restarts (miners stop, then the node; the window opens it again in about 10 s)"), + RestartPlan::Helper => { + self.shared.event("info", "remote job: the app restarts (miners stop, then the node, then it opens again)"); + if let Err(e) = crate::jobrun::spawn_relaunch_helper(&self.shared) { + self.shared.event("error", &format!("remote job: the app cannot restart by itself ({e}); it keeps running")); + return; + } + } + } + self.quit_source = "remote job: app restart"; self.quitting = true; self.st().quitting = true; } @@ -5726,6 +5739,18 @@ impl Engine { /// the installer (PC 2, 7 October 2026, 20:54 BST: a job's silent 0.3.22 install quit the engine through api/quit, the /// 0.3.21 host's restart ladder started the old engine 10 s later, Inno could not replace the locked host, and every /// file stayed 0.3.21). A plain "EXIT" for every other quit (the tray, a crash of the node the engine gave up on). +/// How a remote `restart --what app` brings the app back (v2.0.0). Under the window host: the engine's plain EXIT and the +/// host's restart ladder (10 s), nothing else started. With no host: the detached relaunch helper (src/jobrun.rs). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RestartPlan { + HostLadder, + Helper, +} + +pub fn restart_plan(under_host: bool) -> RestartPlan { + if under_host { RestartPlan::HostLadder } else { RestartPlan::Helper } +} + pub fn exit_line(quit_source: &str) -> &'static str { if quit_source.contains("api/quit") || quit_source.contains("installer") || quit_source.contains("update") { "EXIT update" @@ -6164,6 +6189,24 @@ mod tests { assert_eq!(crate::ember::amd_gmax_arg(2992, 3292, true), -300); } + /// Known-failed first (PC 1, 6 and 8 October 2026: a remote app restart left the app down for hours, "the relaunch + /// helper did not bring it back"): under the window host the restart is the host's ladder after a plain EXIT, never a + /// helper racing it; the helper only with no host; the quit source reads as a plain EXIT, not an update exit. + #[test] + fn a_remote_app_restart_under_the_host_uses_the_hosts_ladder_and_no_helper() { + assert_eq!(super::restart_plan(true), super::RestartPlan::HostLadder); + assert_eq!(super::restart_plan(false), super::RestartPlan::Helper); + assert_eq!(super::exit_line("remote job: app restart"), "EXIT", "a plain EXIT: the host restarts the engine (an update exit would end the host)"); + let j = include_str!("jobrun.rs"); + let arm = j.find("\"restart\" | \"update-now\" =>").unwrap(); + assert!(!j[arm..arm + 1800].contains("spawn_relaunch_helper(shared)"), "the runner no longer starts the helper itself"); + let e = include_str!("engine.rs"); + let ra = e.find("Action::RestartApp => {").unwrap(); + let body = &e[ra..ra + 1500]; + assert!(body.contains("restart_plan(self.wrapper)") && body.contains("RestartPlan::HostLadder =>") && body.contains("crate::jobrun::spawn_relaunch_helper(&self.shared)"), "the engine decides by the host"); + assert!(body.contains("self.quit_source = \"remote job: app restart\";")); + } + /// PC 2, 7 October 2026, 14:39Z: "the helper did not run sequence 1 within 15 s (no line in ...helper.log)" is a FAULT line #[test] fn a_helper_that_does_not_answer_is_a_fault_line() { diff --git a/app/igneum-app/src/jobrun.rs b/app/igneum-app/src/jobrun.rs index 80f1d5b6f..b4a0eedba 100644 --- a/app/igneum-app/src/jobrun.rs +++ b/app/igneum-app/src/jobrun.rs @@ -604,13 +604,10 @@ impl Jobs { (_, "node") => ("node restarted (the miners follow)".to_string(), Action::RestartNode), _ => ("app restarting".to_string(), Action::RestartApp), }; - if let Action::RestartApp = action { - if let Err(e) = spawn_relaunch_helper(shared) { - let o = Outcome { status: "failed".into(), exit: 1, summary: format!("could not start the relaunch helper: {e}"), ..Default::default() }; - self.event(shared, Event::Finished { id: job.id.clone(), outcome: o }); - return None; - } - } + // the app restart's relaunch is the ENGINE's decision (Action::RestartApp, src/engine.rs restart_plan): under the + // window host the host's own ladder restarts the engine, and a helper here would race it for the single + // instance (PC 1, 6 and 8 October 2026: "the relaunch helper did not bring it back", twice); the helper is + // for an engine with no host let sink = Sink::new(shared, &job, &self.dir); sink.line(&format!("{}: {summary}", job.kind)); let uploaded = report(shared, &job, &sink, "done", 0, now, crate::platform::unix_now(), &summary, json!({})); @@ -2151,7 +2148,7 @@ fn run_build(shared: &Arc, job: &Job, sink: &Sink, jobs_dir: &Path, ctl: // ---- kind: restart app ----------------------------------------------------------------------------------------------- /// A detached helper that starts the app again a few seconds after this engine has gone. -fn spawn_relaunch_helper(shared: &Arc) -> Result<(), String> { +pub fn spawn_relaunch_helper(shared: &Arc) -> Result<(), String> { let mut c; #[cfg(target_os = "macos")] {