From 0ef80a99bdd6617cb01baca9102eeacd234849c1 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:48:14 +0000 Subject: [PATCH] The one approved Power control step on PC 1 failed with exit 1 (15:45:21Z, no cap applied, no helper registered): cmd.exe strips the first and last quote of a /c line that starts with one and holds more than two (two NVIDIA cards, or a cap plus the registration script); both launchers now wrap the line in one outer pair (platform::cmd_c_args, host.cpp runElevated), unit-tested; Ember 2: the memory clock set through nvidia-smi -lmc/-rmc (direct and through the helper's lmc/rmc verbs), the goal, the electricity price and the hill-climb switch in Settings and /api/tune/goal, the chosen point's memory clock and the measured curve on the card Co-Authored-By: Claude Fable 5.1 --- app/igneum-app/src/config.rs | 17 ++++++++++- app/igneum-app/src/engine.rs | 54 ++++++++++++++++++++++++++++++--- app/igneum-app/src/hotplug.rs | 2 ++ app/igneum-app/src/platform.rs | 26 +++++++++++++++- app/igneum-app/src/powertask.rs | 11 +++++++ app/igneum-app/src/server.rs | 8 +++++ app/igneum-app/src/state.rs | 7 +++++ app/igneum-app/src/sweep.rs | 9 ++++++ app/windows/host.cpp | 4 ++- 9 files changed, 131 insertions(+), 7 deletions(-) diff --git a/app/igneum-app/src/config.rs b/app/igneum-app/src/config.rs index 1f481e1a3..9746985ac 100644 --- a/app/igneum-app/src/config.rs +++ b/app/igneum-app/src/config.rs @@ -39,6 +39,9 @@ pub struct CardPref { pub sweep_class: String, #[serde(default)] pub sweep_source: String, + /// Ember 2: the memory clock the last tune chose (0 = the driver's default) + #[serde(default)] + pub sweep_mem_mhz: u32, } #[derive(Clone, Serialize, Deserialize)] @@ -90,6 +93,15 @@ pub struct Settings { /// unanswered prompt switches it back off with a notice, no retries. #[serde(default)] pub power_control: bool, + /// Ember 2 (6 October 2026): the tune's goal ("efficiency" = most MH per watt within 10% of the top rate, + /// "balanced" = within 1%, "rate" = the fastest point), the electricity price in pence per kWh for the £/day + /// reading on each card, and the hill-climb switch (memory up, core down from the fleet prior; off = the ladders). + #[serde(default = "balanced")] + pub tune_goal: String, + #[serde(default)] + pub power_price_pence: f64, + #[serde(default)] + pub tune_climb: bool, /// When this install first ran (unix s), for the "first hour after install" sweep. #[serde(default)] pub installed_at: u64, @@ -113,13 +125,16 @@ pub struct Settings { fn one() -> u32 { 1 } +fn balanced() -> String { + "balanced".into() +} fn yes() -> bool { true } impl Default for Settings { fn default() -> Settings { - Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, power_control: false, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false, prove_default_applied: false } + Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, power_control: false, tune_goal: "balanced".into(), power_price_pence: 0.0, tune_climb: false, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false, prove_default_applied: false } } } diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index cc3ab85ac..096eec0c2 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -75,6 +75,8 @@ pub enum Cmd { TuneSet(u64, Result), /// POST /api/tune-progress from a measurement engine beside this app: the card's live tune state TuneProgress(Value), + /// Ember 2: Settings > goal, electricity price, the hill-climb switch + TuneGoal(Option, Option, Option), /// 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), @@ -121,7 +123,7 @@ impl Shared { st.mining.accepted_total = settings.accepted_total; st.mining.fee_total = settings.fee_total; st.address = address_state(&settings, &wallet_path); - st.settings = crate::state::SettingsState { identities: settings.identities, vote: settings.vote, start_at_login: crate::platform::start_at_login_is_on(), auto_update: settings.auto_update, remote_jobs: settings.remote_jobs, prove: settings.prove, sweep: settings.sweep, power_control: settings.power_control, power_note: String::new(), tuning_off: false, tuning_note: String::new(), dev_fee: settings.dev_fee, proof_verify_trust: settings.proof_verify_trust }; + st.settings = crate::state::SettingsState { identities: settings.identities, vote: settings.vote, start_at_login: crate::platform::start_at_login_is_on(), auto_update: settings.auto_update, remote_jobs: settings.remote_jobs, prove: settings.prove, sweep: settings.sweep, power_control: settings.power_control, power_note: String::new(), tuning_off: false, tuning_note: String::new(), tune_goal: settings.tune_goal.clone(), power_price_pence: settings.power_price_pence, tune_climb: settings.tune_climb, dev_fee: settings.dev_fee, proof_verify_trust: settings.proof_verify_trust }; st.dev_fee = crate::state::DevFeeState { on: settings.dev_fee, percent: if settings.dev_fee { 1 } else { 0 }, address: String::new(), line: String::new() }; st.live_page = packaged.live_page.clone(); st.finality.message = "waiting for the miner".into(); @@ -1145,6 +1147,28 @@ impl Engine { } } Cmd::TuneProgress(v) => self.tune_progress(&v), + Cmd::TuneGoal(goal, price, climb) => { + { + let mut s = self.shared.settings.lock().unwrap(); + if let Some(g) = &goal { + s.tune_goal = g.clone(); + } + if let Some(p) = price { + s.power_price_pence = p; + } + if let Some(c) = climb { + s.tune_climb = c; + } + } + self.shared.save_settings(); + let s = self.shared.settings.lock().unwrap().clone(); + let mut st = self.st(); + st.settings.tune_goal = s.tune_goal.clone(); + st.settings.power_price_pence = s.power_price_pence; + st.settings.tune_climb = s.tune_climb; + drop(st); + self.shared.event("info", &format!("tune goal: {}{}{}", s.tune_goal, if s.power_price_pence > 0.0 { format!(", electricity {:.1} p/kWh", s.power_price_pence) } else { String::new() }, if s.tune_climb { ", hill-climb on" } else { "" })); + } Cmd::TuneSet(seq, r) => match r { Ok(text) => { self.tune_acked = Some(seq); @@ -2361,11 +2385,19 @@ impl Engine { let key = crate::ember::prior_key(&c.name, &probe.driver, &c.program_class); let prior = crate::ember::prior_of(tuning.as_ref(), &key, ember.min_samples); let full_due = self.tune_full_due.remove(&idx); + let (goal, climb_on) = { + let s = self.shared.settings.lock().unwrap(); + (crate::ember::Goal::parse(&s.tune_goal), s.tune_climb) + }; let plan = if let Some(why) = control.as_ref() { if let Some(cc) = self.st().mining.cards.get_mut(idx) { cc.sweep_note = why.clone(); } crate::ember::Plan::baseline(&limits, before, ember.tolerance_pct) + } else if climb_on || prior.as_ref().map(|p| p.point.mem_mhz > 0).unwrap_or(false) { + // Ember 2: the hill-climb from the fleet prior (or the card's point) toward the goal + let start = prior.as_ref().map(|p| p.point).unwrap_or(before); + crate::ember::Plan::climb(&limits, start, goal, ember.tolerance_pct) } else if let (Some(p), false, 0) = (prior.as_ref(), full_due, c.sweep_pct) { crate::ember::Plan::confirm(&limits, p.point, before, ember.tolerance_pct) } else { @@ -2477,6 +2509,7 @@ impl Engine { let Some(c) = card else { return }; let w = step.watts.round() as u64; let clock = step.point.clock_mhz; + let mem = step.point.mem_mhz; let shared = self.shared.clone(); self.tune_acked = None; match c.vendor.as_str() { @@ -2500,6 +2533,15 @@ impl Engine { ok &= out.contains("All done") || out.to_ascii_lowercase().contains("clocks set") || out.to_ascii_lowercase().contains("reset"); text.push(' '); text.push_str(out.trim()); + // Ember 2: the memory clock, locked to one value (`-lmc m,m`) or reset (`-rmc`) + let out = if mem > 0 { + crate::detect::run_timeout(std::process::Command::new(&smi).args(["-i", &device, "-lmc", &format!("{mem},{mem}")]), None, Duration::from_secs(20)).unwrap_or_else(|| "nvidia-smi did not answer".into()) + } else { + crate::detect::run_timeout(std::process::Command::new(&smi).args(["-i", &device, "-rmc"]), None, Duration::from_secs(20)).unwrap_or_else(|| "nvidia-smi did not answer".into()) + }; + ok &= out.contains("All done") || out.to_ascii_lowercase().contains("clocks set") || out.to_ascii_lowercase().contains("reset") || out.to_ascii_lowercase().contains("not supported"); + text.push(' '); + text.push_str(out.trim()); shared.send(Cmd::TuneSet(seq, if ok { Ok(text) } else { Err(text) })); }); } @@ -2509,7 +2551,7 @@ impl Engine { return; } let dev = device.to_string(); - let cmd = format!("{seq}0 dev {dev}\n{seq}1 pl {w}\n{seq}2 {}\n", if clock > 0 { format!("lgc {clock}") } else { "rgc".to_string() }); + let cmd = format!("{seq}0 dev {dev}\n{seq}1 pl {w}\n{seq}2 {}\n{seq}3 {}\n", if clock > 0 { format!("lgc {clock}") } else { "rgc".to_string() }, if mem > 0 { format!("lmc {mem}") } else { "rmc".to_string() }); let _ = std::fs::write(self.sweep_dir().join("cmd.txt"), cmd); // the helper polls twice a second and nvidia-smi answers within a second or two std::thread::spawn(move || { @@ -2551,7 +2593,7 @@ impl Engine { } _ => {} } - self.shared.log(&format!("tune: {} MHz, {}% ({w} W) requested on device {device} (request {seq})", if clock > 0 { clock.to_string() } else { "unlocked".into() }, step.point.power_pct)); + self.shared.log(&format!("tune: {} MHz, {}% ({w} W), memory {} requested on device {device} (request {seq})", if clock > 0 { clock.to_string() } else { "unlocked".into() }, step.point.power_pct, if mem > 0 { format!("{mem} MHz") } else { "default".into() })); } /// A measurement engine's progress for one card (POST /api/tune-progress, forwarded by the job playbook from the @@ -2704,8 +2746,10 @@ impl Engine { c.tune_steps = 0; c.tune_eta_s = 0; c.tune_clock_mhz = row.point.clock_mhz; + c.tune_mem_mhz = row.point.mem_mhz; c.tune_source = kind.name().into(); c.tune_line = crate::ember::tuned_line(row.mhs, row.watts, row.eff); + c.tune_curve = run.rows.iter().map(|r| r.json()).collect(); let control = c.tune_control; if kind == crate::ember::PlanKind::Baseline { c.sweep_note = if control { String::new() } else { c.sweep_note.clone() }; @@ -2714,6 +2758,7 @@ impl Engine { } else { c.power_pct = row.point.power_pct; c.clock_cap_mhz = row.point.clock_mhz; + c.mem_cap_mhz = row.point.mem_mhz; if c.vendor == "nvidia" { c.power_limit_w = row.limit; c.power_applied = true; @@ -2728,6 +2773,7 @@ impl Engine { e.sweep_watts = row.watts; e.sweep_mhs = row.mhs; e.sweep_clock_mhz = row.point.clock_mhz; + e.sweep_mem_mhz = row.point.mem_mhz; e.sweep_driver = c.driver.clone(); e.sweep_class = c.program_class.clone(); e.sweep_source = kind.name().into(); @@ -2738,7 +2784,7 @@ impl Engine { }; let _ = key; self.shared.save_settings(); - self.sweep_say(&format!("TUNE chosen card={} clock={} cap={} limit={:.0} watts={:.1} mhs={:.2} eff={:.4} plan={}{}", run.label, row.point.clock_mhz, row.point.power_pct, row.limit, row.watts, row.mhs, row.eff, kind.name(), if pinned { " pinned=1" } else { "" })); + self.sweep_say(&format!("TUNE chosen card={} clock={} cap={} mem={} limit={:.0} watts={:.1} mhs={:.2} eff={:.4} plan={}{}", run.label, row.point.clock_mhz, row.point.power_pct, row.point.mem_mhz, row.limit, row.watts, row.mhs, row.eff, kind.name(), if pinned { " pinned=1" } else { "" })); let before = run.rows.first().filter(|_| kind == crate::ember::PlanKind::Full).cloned(); let record = crate::ember::record_json(crate::platform::unix_now_f(), &crate::config::fingerprint8(&self.shared.runtime.machine_id), VERSION, crate::manifest::platform_name(), &name, &vendor, &driver, &class, kind, &run.rows, Some(&row), before.as_ref()); self.sweep_say(&format!("TUNE {record}")); diff --git a/app/igneum-app/src/hotplug.rs b/app/igneum-app/src/hotplug.rs index f26779ba1..588e93e0a 100644 --- a/app/igneum-app/src/hotplug.rs +++ b/app/igneum-app/src/hotplug.rs @@ -163,6 +163,8 @@ pub fn apply_pref(c: &mut CardState, p: &CardPref) { c.sweep_at = p.sweep_at as f64; // Ember Tune (src/ember.rs): the clock cap the last tune chose, its plan, and the row's Tuned line c.tune_clock_mhz = p.sweep_clock_mhz; + c.tune_mem_mhz = p.sweep_mem_mhz; + c.mem_cap_mhz = if p.pinned || p.sweep_source == "baseline" { 0 } else { p.sweep_mem_mhz }; c.clock_cap_mhz = if p.pinned || p.sweep_source == "baseline" { 0 } else { p.sweep_clock_mhz }; c.tune_source = p.sweep_source.clone(); if p.sweep_mhs > 0.0 && p.sweep_watts > 0.0 { diff --git a/app/igneum-app/src/platform.rs b/app/igneum-app/src/platform.rs index bb41df4ff..01f288c6d 100644 --- a/app/igneum-app/src/platform.rs +++ b/app/igneum-app/src/platform.rs @@ -430,11 +430,21 @@ pub fn sync_clock() -> Result { /// Runs a command line with administrator rights (one prompt): the NVIDIA power cap needs it on Windows. /// Blocking; call from a thread. +/// The cmd.exe argument for one elevated line. cmd's documented rule: when the text after /c starts with a quote +/// and holds more than two quotes (two cards, or a cap plus a script), it strips the FIRST and LAST quote and runs +/// the broken remainder (PC 1, 6 October 2026, 15:45Z: the one approved Power control step ran +/// `"...\nvidia-smi.exe" -i 0 -pl 460 & "...\nvidia-smi.exe" -i 1 -pl 160 & "...\powershell.exe" ... -File "...ps1"`, +/// exit 1, no cap applied, no task registered; a single-card PC, two quotes, was fine, which is why PC 2 never +/// showed it). Wrapping the whole line in one outer pair makes cmd strip exactly those. +pub fn cmd_c_args(cmdline: &str) -> String { + format!("/c \"{cmdline}\"") +} + pub fn run_elevated(cmdline: &str) -> Result<(), String> { #[cfg(windows)] { let cmd = tool("cmd").display().to_string(); - let mut c = elevated_command(&cmd, &format!("/c {cmdline}")); + let mut c = elevated_command(&cmd, &cmd_c_args(cmdline)); let out = c.output().map_err(|e| e.to_string())?; if out.status.success() { Ok(()) @@ -511,6 +521,20 @@ pub fn quiet(cmd: &mut Command) -> &mut Command { cmd } +#[cfg(test)] +mod cmd_tests { + #[test] + fn an_elevated_line_is_wrapped_so_cmd_keeps_every_inner_quote() { + let line = r#""C:\WINDOWS\System32\nvidia-smi.exe" -i 0 -pl 460 & "C:\WINDOWS\System32\nvidia-smi.exe" -i 1 -pl 160 & "C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -File "C:\x\register-power-task.ps1""#; + let a = super::cmd_c_args(line); + assert!(a.starts_with("/c \"\"C:\\WINDOWS"), "{a}"); + assert!(a.ends_with("register-power-task.ps1\"\""), "{a}"); + // the inner line is intact between the outer pair + assert_eq!(&a[4..a.len() - 1], line); + assert_eq!(super::cmd_c_args("echo hi"), "/c \"echo hi\""); + } +} + #[cfg(test)] mod lock_tests { #[test] diff --git a/app/igneum-app/src/powertask.rs b/app/igneum-app/src/powertask.rs index 787142f38..0cb86b083 100644 --- a/app/igneum-app/src/powertask.rs +++ b/app/igneum-app/src/powertask.rs @@ -43,6 +43,9 @@ pub enum HelperCmd { PowerLimit(u64), ClockCap(u64), ClockReset, + /// Ember 2: the memory clock locked to one value (`-lmc m,m`), or reset (`-rmc`) + MemClock(u64), + MemReset, Quit, Remove, } @@ -65,6 +68,8 @@ pub fn parse_line(line: &str) -> Option<(u64, HelperCmd)> { [_, "pl", w] if digits(w) => Some((seq, HelperCmd::PowerLimit(w.parse().ok()?))), [_, "lgc", m] if digits(m) => Some((seq, HelperCmd::ClockCap(m.parse().ok()?))), [_, "rgc"] => Some((seq, HelperCmd::ClockReset)), + [_, "lmc", m] if digits(m) => Some((seq, HelperCmd::MemClock(m.parse().ok()?))), + [_, "rmc"] => Some((seq, HelperCmd::MemReset)), [_, "dev", d] if digits(d) => Some((seq, HelperCmd::Dev(d.to_string()))), _ => None, } @@ -76,6 +81,8 @@ pub fn smi_args(dev: &str, c: &HelperCmd) -> Option> { HelperCmd::PowerLimit(w) => Some(vec!["-i".into(), dev.into(), "-pl".into(), w.to_string()]), HelperCmd::ClockCap(m) => Some(vec!["-i".into(), dev.into(), "-lgc".into(), format!("0,{m}")]), HelperCmd::ClockReset => Some(vec!["-i".into(), dev.into(), "-rgc".into()]), + HelperCmd::MemClock(m) => Some(vec!["-i".into(), dev.into(), "-lmc".into(), format!("{m},{m}")]), + HelperCmd::MemReset => Some(vec!["-i".into(), dev.into(), "-rmc".into()]), _ => None, } } @@ -229,6 +236,10 @@ mod tests { assert_eq!(parse_line("7 pl 460"), Some((7, HelperCmd::PowerLimit(460)))); assert_eq!(parse_line("8 lgc 2472"), Some((8, HelperCmd::ClockCap(2472)))); assert_eq!(parse_line("9 rgc"), Some((9, HelperCmd::ClockReset))); + assert_eq!(parse_line("10 lmc 14001"), Some((10, HelperCmd::MemClock(14001)))); + assert_eq!(parse_line("11 rmc"), Some((11, HelperCmd::MemReset))); + assert_eq!(smi_args("0", &HelperCmd::MemClock(14001)).unwrap(), vec!["-i", "0", "-lmc", "14001,14001"]); + assert_eq!(smi_args("0", &HelperCmd::MemReset).unwrap(), vec!["-i", "0", "-rmc"]); assert_eq!(parse_line("3 dev 1"), Some((3, HelperCmd::Dev("1".into())))); assert_eq!(parse_line("5 403"), Some((5, HelperCmd::PowerLimit(403))), "the 0.3.9 form"); assert_eq!(parse_line("quit"), Some((0, HelperCmd::Quit))); diff --git a/app/igneum-app/src/server.rs b/app/igneum-app/src/server.rs index e85d949b9..95984e600 100644 --- a/app/igneum-app/src/server.rs +++ b/app/igneum-app/src/server.rs @@ -251,6 +251,14 @@ fn api_post(shared: &Arc, path: &str, body: Value) -> Result { + // Ember 2: the goal, the electricity price (pence per kWh) and the hill-climb switch + let goal = body.get("goal").and_then(|v| v.as_str()).map(|g| crate::ember::Goal::parse(g).name().to_string()); + let price = body.get("price_pence").and_then(|v| v.as_f64()).filter(|p| (0.0..=500.0).contains(p)); + let climb = body.get("climb").and_then(|v| v.as_bool()); + shared.send(Cmd::TuneGoal(goal, price, climb)); + Ok(json!({ "ok": true })) + } "/api/settings" => { let identities = body.get("identities").and_then(|v| v.as_u64()).map(|v| v.clamp(1, 64) as u32); let vote = body.get("vote").and_then(|v| v.as_bool()); diff --git a/app/igneum-app/src/state.rs b/app/igneum-app/src/state.rs index 81f90e263..cf0ccd395 100644 --- a/app/igneum-app/src/state.rs +++ b/app/igneum-app/src/state.rs @@ -132,6 +132,9 @@ pub struct CardState { pub tune_steps: u32, pub tune_eta_s: i64, pub tune_plan: String, + // Ember 2: the memory clock the last tune chose and the measured curve (every row of the last plan) + pub tune_mem_mhz: u32, + pub tune_curve: Vec, // the kernel variant race (docs/design/miner-tuning.md): what the worker's last race chose pub variant: String, pub race_mhs: f64, @@ -290,6 +293,10 @@ pub struct SettingsState { /// Ember Tune is paused fleet-wide by the signed manifest's kill switch (tuning.ember.enabled = false) pub tuning_off: bool, pub tuning_note: String, + /// Ember 2: the goal (efficiency | balanced | rate), the electricity price in pence per kWh, the hill-climb switch + pub tune_goal: String, + pub power_price_pence: f64, + pub tune_climb: bool, /// the miner software's dev fee switch (settings; `--dev-fee 0` when off) pub dev_fee: bool, /// devnet only: the node trusts proof records without a verifier (`IGNEUM_PROOF_VERIFY=trust`) diff --git a/app/igneum-app/src/sweep.rs b/app/igneum-app/src/sweep.rs index 5ae546f3d..92e402eee 100644 --- a/app/igneum-app/src/sweep.rs +++ b/app/igneum-app/src/sweep.rs @@ -375,6 +375,12 @@ while ($true) { } elseif ($op -eq 'rgc') { $out = (& $Smi -i $Device -rgc 2>&1 | Out-String).Trim() "$(Get-Date -Format o) $($p[0]) -rgc : $out" | Out-File -FilePath $log -Append -Encoding utf8 + } elseif ($op -eq 'lmc' -and $v -match '^\d+$') { + $out = (& $Smi -i $Device -lmc "$v,$v" 2>&1 | Out-String).Trim() + "$(Get-Date -Format o) $($p[0]) -lmc $v,$v : $out" | Out-File -FilePath $log -Append -Encoding utf8 + } elseif ($op -eq 'rmc') { + $out = (& $Smi -i $Device -rmc 2>&1 | Out-String).Trim() + "$(Get-Date -Format o) $($p[0]) -rmc : $out" | Out-File -FilePath $log -Append -Encoding utf8 } } } @@ -411,6 +417,8 @@ while true; do pl) case "$v" in ''|*[!0-9]*) ;; *) echo "$(date -u +%FT%TZ) $1 -pl $v : $("$smi" -i "$dev" -pl "$v" 2>&1)" >> "$dir/helper.log";; esac ;; lgc) case "$v" in ''|*[!0-9]*) ;; *) echo "$(date -u +%FT%TZ) $1 -lgc 0,$v : $("$smi" -i "$dev" -lgc "0,$v" 2>&1)" >> "$dir/helper.log";; esac ;; rgc) echo "$(date -u +%FT%TZ) $1 -rgc : $("$smi" -i "$dev" -rgc 2>&1)" >> "$dir/helper.log" ;; + lmc) case "$v" in ''|*[!0-9]*) ;; *) echo "$(date -u +%FT%TZ) $1 -lmc $v,$v : $("$smi" -i "$dev" -lmc "$v,$v" 2>&1)" >> "$dir/helper.log";; esac ;; + rmc) echo "$(date -u +%FT%TZ) $1 -rmc : $("$smi" -i "$dev" -rmc 2>&1)" >> "$dir/helper.log" ;; esac done fi @@ -614,6 +622,7 @@ mod tests { for s in [helper_script_windows(), helper_script_unix()] { assert!(s.contains("cmd.txt") && s.contains("quit") && s.contains("-pl") && s.contains("20 min")); assert!(s.contains("-lgc") && s.contains("-rgc"), "the clock cap and its reset"); + assert!(s.contains("-lmc") && s.contains("-rmc"), "Ember 2: the memory clock and its reset"); } } } diff --git a/app/windows/host.cpp b/app/windows/host.cpp index 84bf99b5e..88328f388 100644 --- a/app/windows/host.cpp +++ b/app/windows/host.cpp @@ -112,7 +112,9 @@ static void applyState(const std::string& j) { // Runs cmd /c as administrator, waits, and answers on the engine's stdin. static void runElevated(std::wstring line) { std::thread([line] { - std::wstring params = L"/c " + line; + // cmd strips the first and last quote of a line that starts with one and holds more than two (two cards, or a + // cap plus a script): one outer pair around the whole line is what it may strip (PC 1, 6 October 2026) + std::wstring params = L"/c \"" + line + L"\""; wchar_t sysdir[MAX_PATH]; GetSystemDirectoryW(sysdir, MAX_PATH); std::wstring cmdExe = std::wstring(sysdir) + L"\\cmd.exe"; // the absolute path, never a bare name (R4.3.3)