diff --git a/app/igneum-app/src/jobrun.rs b/app/igneum-app/src/jobrun.rs index bc833a21..dc7a6773 100644 --- a/app/igneum-app/src/jobrun.rs +++ b/app/igneum-app/src/jobrun.rs @@ -1119,7 +1119,10 @@ fn run_script(shared: &Arc, job: &Job, sink: &Sink, jobs_dir: &Path, dat std::fs::write(&wrapper, [b"\xEF\xBB\xBF".as_slice(), w.as_bytes()].concat()).map_err(|e| e.to_string())?; let _ = std::fs::remove_file(&out_file); let inner = format!("-NoProfile -ExecutionPolicy Bypass -File \"{}\"", wrapper.display()); - let ps = format!("$p = Start-Process -FilePath powershell.exe -ArgumentList '{}' -Verb RunAs -Wait -WindowStyle Hidden -PassThru; exit $p.ExitCode", inner.replace('\'', "''")); + // A refused or unanswered UAC prompt makes Start-Process throw (`$p` stays null) and `exit $p.ExitCode` + // would exit 0: the 5 October 2026 driver job on PC 1 was reported "done" after Windows cancelled its + // prompt at 122 s. The launch failure is exit 251 and says so on stderr. + let ps = format!("try {{ $p = Start-Process -FilePath powershell.exe -ArgumentList '{}' -Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop }} catch {{ Write-Error ('elevated launch failed (UAC refused, cancelled or timed out): ' + $_.Exception.Message); exit 251 }}; if ($null -eq $p) {{ Write-Error 'elevated launch failed: no process'; exit 251 }}; exit $p.ExitCode", inner.replace('\'', "''")); cmd = Command::new(crate::platform::tool("powershell")); cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps]); } else if shell == "powershell" { @@ -1145,6 +1148,7 @@ fn run_script(shared: &Arc, job: &Job, sink: &Sink, jobs_dir: &Path, dat fn finish_ran(ran: Ran, what: &str) -> Result { match ran.code { Some(0) => Ok(Done { status: "done".into(), exit: 0, summary: format!("{what} finished, exit 0"), extra: json!({}) }), + Some(251) => Ok(Done { status: "failed".into(), exit: 251, summary: format!("{what} did not start: the administrator prompt was refused, cancelled or timed out (click Yes within 2 minutes)"), extra: json!({}) }), Some(c) => Ok(Done { status: "failed".into(), exit: c as i64, summary: format!("{what} exited with code {c}"), extra: json!({}) }), None if ran.timed_out => Ok(Done { status: "timeout".into(), exit: -1, summary: format!("{what} hit the time cap and was ended"), extra: json!({}) }), None => Err(format!("{what} was ended")), @@ -1259,6 +1263,20 @@ fn collect_done(uploaded: u32, failed: u32, names: Vec, ran: Option mod tests { use super::*; + #[test] + fn a_refused_administrator_prompt_is_a_failure_not_done() { + // 5 October 2026: the elevated launcher exited 0 after Windows cancelled an unanswered UAC prompt + let d = finish_ran(Ran { code: Some(251), timed_out: false }, "script").unwrap(); + assert_eq!((d.status.as_str(), d.exit), ("failed", 251)); + assert!(d.summary.contains("administrator prompt"), "{}", d.summary); + let d = finish_ran(Ran { code: Some(0), timed_out: false }, "script").unwrap(); + assert_eq!(d.status, "done"); + // the launcher string itself: a thrown Start-Process must not fall through to `exit $p.ExitCode` + let src = include_str!("jobrun.rs"); + assert!(src.contains("-Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop }} catch {{")); + assert!(src.contains("if ($null -eq $p) {{ Write-Error 'elevated launch failed: no process'; exit 251 }}")); + } + #[test] fn collect_outcome_follows_the_command_exit() { let d = collect_done(0, 0, vec![], None);