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 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 16:11:14 +00:00
parent 1602298d08
commit b86fcbe6b5
2 changed files with 49 additions and 9 deletions

View file

@ -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() {

View file

@ -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<Shared>, 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<Shared>) -> Result<(), String> {
pub fn spawn_relaunch_helper(shared: &Arc<Shared>) -> Result<(), String> {
let mut c;
#[cfg(target_os = "macos")]
{