diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index 095a6028..c6e57572 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -75,7 +75,8 @@ pub enum Cmd { /// restart the node with the verifier decided again (src/verifier.rs): the trust setting changed, or the /// prover found a host that was not there when the node started RestartNode(String), - Quit, + /// quit, with its source (the log names it: C35, 5 October 2026, two unexplained quits) + Quit(&'static str), } pub struct Shared { @@ -463,6 +464,8 @@ pub struct Engine { last_error_event: Instant, // the efficiency sweep (src/sweep.rs): one card at a time sweep: Option, + /// who asked for the quit (the log's `quit:` line names it) + quit_source: &'static str, /// the request number the vendor tool last carried out (the run's acknowledgement) tune_acked: Option, /// cards whose confirm check found a better neighbour: the full plan runs next @@ -559,6 +562,7 @@ impl Engine { last_settings_save: now, last_error_event: now - Duration::from_secs(600), sweep: None, + quit_source: "unknown", tune_acked: None, tune_full_due: std::collections::HashSet::new(), sweep_pending: None, @@ -702,7 +706,7 @@ impl Engine { if self.shared.runtime.sweep_only { if supported.is_empty() { self.sweep_say("SWEEP none reason=no_supported_card"); - self.shared.send(Cmd::Quit); + self.shared.send(Cmd::Quit("the --sweep run (every card done)")); } else { self.sweep_queue = supported; } @@ -1021,7 +1025,9 @@ impl Engine { } } }, - Cmd::Quit => { + Cmd::Quit(source) => { + self.shared.log(&format!("quit requested by {source}")); + self.quit_source = source; self.quitting = true; self.st().quitting = true; } @@ -1472,6 +1478,10 @@ impl Engine { if self.power_busy { return; } + if self.shared.runtime.sweep_only { + self.shared.log(&format!("power cap ({why}): not touched under --sweep; the tune sets every limit itself")); + return; + } if self.sweep.is_some() || self.sweep_pending.is_some() { // the sweep owns the caps until it ends; it applies the chosen one itself self.shared.log(&format!("power cap ({why}): deferred, a sweep is running")); @@ -1856,7 +1866,7 @@ impl Engine { if let Some((idx, forced)) = pick { self.sweep_begin(idx, forced); } else if self.shared.runtime.sweep_only && self.sweep_queue.is_empty() && self.sweep_pending.is_none() { - self.shared.send(Cmd::Quit); + self.shared.send(Cmd::Quit("the --sweep run (every card done)")); } } @@ -2304,7 +2314,7 @@ impl Engine { } self.upload_logs(false); if self.shared.runtime.sweep_only && self.sweep_queue.is_empty() { - self.shared.send(Cmd::Quit); + self.shared.send(Cmd::Quit("the --sweep run (every card done)")); } } @@ -2356,7 +2366,7 @@ impl Engine { } else { self.sweep_retry.insert(idx, Instant::now() + Duration::from_secs(3600)); if self.shared.runtime.sweep_only && self.sweep_queue.is_empty() { - self.shared.send(Cmd::Quit); + self.shared.send(Cmd::Quit("the --sweep run (every card done)")); } } } @@ -3371,7 +3381,7 @@ impl Engine { } fn shutdown(&mut self) { - self.shared.log("quit: stopping the miners, then the node"); + self.shared.log(&format!("quit: stopping the miners, then the node (source: {})", self.quit_source)); self.jobs.abort(&self.shared, "the app is quitting"); if self.sweep.is_some() || self.sweep_pending.is_some() { self.sweep_abort("the app is quitting"); @@ -3426,7 +3436,11 @@ impl Engine { /// administrator rights (one UAC prompt on Windows, pkexec on Linux); the engine builds an elevated command only when /// Power control is on in Settings, or when it is itself the elevated PC sweep job (--sweep). fn elevation_allowed(power_control: bool, sweep_only: bool) -> bool { - power_control || sweep_only + // C35 (5 October 2026, 22:30 UTC): the unattended --sweep job on PC 1 counted as allowed and raised the one + // administrator prompt nobody was there to answer; an elevated job sets limits directly without asking (the tune + // probe's `direct`), so the flag adds nothing and Power control alone decides + let _ = sweep_only; + power_control } /// The notice when the one prompt was refused, cancelled or not answered: Power control goes back off, no retries. @@ -3634,7 +3648,7 @@ mod tests { // the decision (the project lead, 5 October 2026): off = the app never asks; the elevated PC sweep job is the exception assert!(!super::elevation_allowed(false, false)); assert!(super::elevation_allowed(true, false)); - assert!(super::elevation_allowed(false, true)); + assert!(!super::elevation_allowed(false, true), "the --sweep job alone never asks (C35)"); let mut cards = vec![ super::CardState { vendor: "nvidia".into(), enabled: true, device: "0".into(), name: "RTX 5090".into(), power_default_w: 575.0, power_limit_w: 575.0, power_pct: 80, ..Default::default() }, super::CardState { vendor: "amd".into(), enabled: true, device: "1".into(), name: "RX 9070 XT".into(), power_default_w: 300.0, power_limit_w: 300.0, power_pct: 80, ..Default::default() }, diff --git a/app/igneum-app/src/main.rs b/app/igneum-app/src/main.rs index e94427d9..19b96ed9 100644 --- a/app/igneum-app/src/main.rs +++ b/app/igneum-app/src/main.rs @@ -133,7 +133,7 @@ fn main() { let Ok(l) = line else { break }; let t = l.trim(); match t { - "quit" => shared.send(engine::Cmd::Quit), + "quit" => shared.send(engine::Cmd::Quit("the window host (quit on stdin: the tray menu or the installer)")), "pause" => shared.send(engine::Cmd::Pause), "resume" => shared.send(engine::Cmd::Resume), "elevated ok" => shared.send(engine::Cmd::ElevatedDone(Ok(()))), @@ -142,7 +142,7 @@ fn main() { } } if wrapper { - shared.send(engine::Cmd::Quit); + shared.send(engine::Cmd::Quit("the window host went away (stdin closed)")); } }); } diff --git a/app/igneum-app/src/server.rs b/app/igneum-app/src/server.rs index e9884e36..b2ddb9f3 100644 --- a/app/igneum-app/src/server.rs +++ b/app/igneum-app/src/server.rs @@ -339,7 +339,8 @@ fn api_post(shared: &Arc, path: &str, body: Value) -> Result { - shared.send(Cmd::Quit); + // the caller is on 127.0.0.1 and holds the token: the installer, the OTA apply, a script that read app.url + shared.send(Cmd::Quit("POST /api/quit (a local caller with the token: the installer, the OTA apply, or a script that read app.url)")); Ok(json!({ "ok": true })) } _ => Err("unknown api".into()), diff --git a/relay/playbooks/ember-tune-pc1.ps1 b/relay/playbooks/ember-tune-pc1.ps1 index 2e135be2..26852830 100644 --- a/relay/playbooks/ember-tune-pc1.ps1 +++ b/relay/playbooks/ember-tune-pc1.ps1 @@ -15,6 +15,7 @@ # therefore measure only tonight unless the engine finds itself elevated. $ErrorActionPreference = 'Continue' $budgetMinutes = 35 +if (-not ($budgetMinutes -is [int]) -or $budgetMinutes -lt 5) { $budgetMinutes = 35 } # a budget under 5 minutes is a bug, not a budget (C35) $started = Get-Date $deadline = $started.AddMinutes($budgetMinutes) function Say([string] $m) { Write-Host ("[" + (Get-Date -Format 'HH:mm:ss') + "] " + $m) } @@ -123,8 +124,16 @@ while (-not $p.HasExited) { } if ((Get-Date) -gt $deadline) { Say ("budget of " + $budgetMinutes + " min spent; asking the tune engine to quit") + # C35 (5 October 2026): the only quit this script may send goes to the TUNE engine's own URL file in the scratch + # root, never to a file under the installed app's folder; the RESULT line names the file it used $u = Join-Path $sApp 'app.url' - if (Test-Path $u) { try { Invoke-WebRequest -Uri ((Get-Content -LiteralPath $u -Raw).Trim() + 'api/quit') -Method POST -Body '{}' -ContentType 'application/json' -UseBasicParsing -TimeoutSec 5 | Out-Null } catch { } } + $installedUrl = Join-Path $appDir 'app.url' + if ((Resolve-Path -LiteralPath $u -ErrorAction SilentlyContinue).Path -eq (Resolve-Path -LiteralPath $installedUrl -ErrorAction SilentlyContinue).Path -or $u -like '*\igneum\app\*') { + Write-Output ('RESULT TUNE quit refused: ' + $u + ' is the installed app''s URL file') + } elseif (Test-Path -LiteralPath $u) { + Write-Output ('RESULT TUNE quit asked of the tune engine through ' + $u + ' (pid ' + $p.Id + ')') + try { Invoke-WebRequest -Uri ((Get-Content -LiteralPath $u -Raw).Trim() + 'api/quit') -Method POST -Body '{}' -ContentType 'application/json' -UseBasicParsing -TimeoutSec 5 | Out-Null } catch { } + } else { Write-Output ('RESULT TUNE quit not sent: no URL file at ' + $u + '; killing pid ' + $p.Id) } Start-Sleep -Seconds 20 if (-not $p.HasExited) { $p.Kill() } Write-Output 'RESULT TUNE error=budget_exceeded'