app: Power control setting (default off): the app never asks for administrator rights on its own
the project lead, 5 October 2026: "if we don't have to ask then don't ask". The NVIDIA power cap and the efficiency sweep need
administrator rights (one UAC prompt); PC 1 raised that prompt for cmd.exe at every app start and every sweep attempt
(17:00, 17:30, 18:12, 19:04 UTC today, each cancelled unanswered after 2 minutes; the "Windows Command Processor"
the project lead saw).
- config.rs: `power_control` (default OFF on every machine); `sweep` default becomes off and is implied by it (an
install carrying sweep = true without power_control is migrated to off on load).
- engine.rs: `elevation_allowed(power_control, sweep_only)` gates the power cap (`power_cap_plan` builds nothing when
off, the card note says so), the sweep scheduler, Sweep now, the sweep helper; no prompt on quit (the limits reset at
the next reboot); no second prompt through PowerShell when the window host's prompt goes unanswered.
Cmd::PowerControl(on): on = ONE prompt at that moment (every NVIDIA cap in one step), off = nothing asks;
`power_control_after_prompt` turns a refused, cancelled or unanswered prompt into "power control off:
administrator rights were not given" (switch back off, sweep off, no retries). Unit tests: off builds no elevated
command; on + refusal gives the notice; rights given keeps it on.
- platform.rs: `elevated_failure` maps the launcher's exit 251 and the "canceled" wording to the prompt, any other
code to the step itself.
- server.rs: POST /api/power/control {on}. ui: the Power control switch with the line "Windows asks for administrator
rights once; the cap and the sweep need them", the note beside it, the sweep switch disabled while it is off.
- The clock-sync prompt stays behind the Sync clock button only (unchanged).
- tools/windows/power-prompts-off.ps1: the 0.3.9 job that switched PC 1's sweep off through the API it has
(run-20261005-192313: sweep True -> False; the 0.3.9 cap has no off switch, it asks at an app start only).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
60248a3dfb
commit
057f0ec8bf
8 changed files with 249 additions and 38 deletions
|
|
@ -70,9 +70,16 @@ pub struct Settings {
|
|||
#[serde(default)]
|
||||
pub prove: bool,
|
||||
/// The efficiency sweep (src/sweep.rs): once after install, then weekly, each NVIDIA card's cap is stepped from
|
||||
/// 100% to 50% on the live program and held at the best MH per watt. Default on. A pinned card is skipped.
|
||||
#[serde(default = "yes")]
|
||||
/// 100% to 50% on the live program and held at the best MH per watt. Default off; implied by `power_control`
|
||||
/// (on when that is switched on, never effective while it is off). A pinned card is skipped.
|
||||
#[serde(default)]
|
||||
pub sweep: bool,
|
||||
/// Power control (the project lead, 5 October 2026: "if we don't have to ask then don't ask"): the NVIDIA power cap and the
|
||||
/// efficiency sweep need administrator rights (one UAC prompt on Windows). Default OFF on every machine; the app
|
||||
/// never raises the prompt on its own. Switching it on asks once, at that moment; a refused, cancelled or
|
||||
/// unanswered prompt switches it back off with a notice, no retries.
|
||||
#[serde(default)]
|
||||
pub power_control: bool,
|
||||
/// When this install first ran (unix s), for the "first hour after install" sweep.
|
||||
#[serde(default)]
|
||||
pub installed_at: u64,
|
||||
|
|
@ -98,16 +105,26 @@ fn yes() -> bool {
|
|||
|
||||
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, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: 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: false, power_control: false, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn load(path: &Path) -> Settings {
|
||||
let mut s: Settings = std::fs::read_to_string(path).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or_default();
|
||||
let mut dirty = false;
|
||||
if s.installed_at == 0 {
|
||||
// an install from before the sweep existed counts as installed now: it gets its first-hour sweep
|
||||
s.installed_at = crate::platform::unix_now();
|
||||
dirty = true;
|
||||
}
|
||||
if s.sweep && !s.power_control {
|
||||
// the sweep is implied by power control (5 October 2026): an install from before that setting carried
|
||||
// sweep = true by default; it no longer prompts on its own
|
||||
s.sweep = false;
|
||||
dirty = true;
|
||||
}
|
||||
if dirty {
|
||||
s.save(path);
|
||||
}
|
||||
s
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ pub enum Cmd {
|
|||
SweepStop,
|
||||
SweepPin(String, bool),
|
||||
SweepEnable(bool),
|
||||
/// Settings > Power control (config.rs power_control): on asks for administrator rights once, at that moment
|
||||
PowerControl(bool),
|
||||
/// the probe that decides how caps are set during a sweep: Ok(true) = nvidia-smi -pl works from this process
|
||||
/// (the engine runs elevated), Ok(false) = the elevated helper is needed; Err = nvidia-smi did not answer
|
||||
SweepCapMode(Result<bool, String>),
|
||||
|
|
@ -113,7 +115,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, 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(), 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();
|
||||
|
|
@ -880,6 +882,11 @@ impl Engine {
|
|||
if applied.is_empty() && missing.is_empty() {
|
||||
self.shared.log(&format!("power cap: nothing to read back for {what}"));
|
||||
}
|
||||
match power_control_after_prompt(&r) {
|
||||
Some(note) => self.power_control_off(note),
|
||||
None if !applied.is_empty() => self.st().settings.power_note = "administrator rights given; the cap and the sweep run".into(),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Cmd::ElevatedDone(r) => {
|
||||
if let Some((line, what, want, _)) = self.power_via_host.take() {
|
||||
|
|
@ -903,6 +910,7 @@ impl Engine {
|
|||
match st.mining.cards.iter().find(|c| c.key == key) {
|
||||
Some(c) if !c.sweep_supported => (c.name.clone(), false, c.sweep_note.clone()),
|
||||
Some(c) if !c.enabled => (c.name.clone(), false, "the card is switched off".to_string()),
|
||||
Some(c) if !self.elevation_allowed() => (c.name.clone(), false, "switch Power control on in Settings first (Windows asks for administrator rights once)".to_string()),
|
||||
Some(c) => (c.name.clone(), true, String::new()),
|
||||
None => (key.clone(), false, "no such card".to_string()),
|
||||
}
|
||||
|
|
@ -938,10 +946,38 @@ impl Engine {
|
|||
self.shared.event("info", &if pinned { format!("{name}: cap pinned; the sweep records but does not change it") } else { format!("{name}: the sweep chooses the cap again from its next run") });
|
||||
}
|
||||
Cmd::SweepEnable(on) => {
|
||||
let allowed = self.elevation_allowed();
|
||||
let on = on && allowed;
|
||||
self.shared.settings.lock().unwrap().sweep = on;
|
||||
self.shared.save_settings();
|
||||
self.st().settings.sweep = on;
|
||||
self.shared.event("info", if on { "efficiency sweep on: once after install, then weekly" } else { "efficiency sweep off; Sweep now on a card still runs one" });
|
||||
self.shared.event("info", if on { "efficiency sweep on: once after install, then weekly" } else if allowed { "efficiency sweep off; Sweep now on a card still runs one" } else { "efficiency sweep stays off: switch Power control on in Settings first" });
|
||||
}
|
||||
Cmd::PowerControl(on) => {
|
||||
{
|
||||
let mut s = self.shared.settings.lock().unwrap();
|
||||
s.power_control = on;
|
||||
s.sweep = on; // implied
|
||||
}
|
||||
self.shared.save_settings();
|
||||
{
|
||||
let mut st = self.st();
|
||||
st.settings.power_control = on;
|
||||
st.settings.sweep = on;
|
||||
st.settings.power_note = if on { "asking Windows for administrator rights once".into() } else { String::new() };
|
||||
}
|
||||
if on {
|
||||
self.shared.event("info", "power control on: Windows asks for administrator rights once; the cap and the sweep need them");
|
||||
for c in self.st().mining.cards.iter_mut().filter(|c| c.vendor == "nvidia") {
|
||||
c.power_applied = false; // the one prompt sets every cap now
|
||||
}
|
||||
self.apply_power_limits("power control on");
|
||||
if !self.power_busy {
|
||||
self.st().settings.power_note = "on; no NVIDIA card needs a cap right now".into();
|
||||
}
|
||||
} else {
|
||||
self.power_control_off("power control off; the cap and the sweep do not run and nothing asks for administrator rights");
|
||||
}
|
||||
}
|
||||
Cmd::SweepCapMode(r) => self.sweep_mode_known(r),
|
||||
Cmd::SweepHelperDone(r) => {
|
||||
|
|
@ -1418,21 +1454,10 @@ impl Engine {
|
|||
self.shared.log(&format!("power cap ({why}): deferred, a sweep is running"));
|
||||
return;
|
||||
}
|
||||
let mut cmds = Vec::new();
|
||||
let mut what = Vec::new();
|
||||
{
|
||||
let mut st = self.st();
|
||||
for c in st.mining.cards.iter_mut().filter(|c| c.vendor == "nvidia" && c.enabled && c.power_default_w > 0.0) {
|
||||
let pct = if c.power_pct == 0 { 80 } else { c.power_pct.clamp(crate::sweep::MIN_PCT, 100) };
|
||||
c.power_pct = pct;
|
||||
let watts = requested_watts(c);
|
||||
if (c.power_limit_w - watts).abs() < 1.0 && c.power_applied {
|
||||
continue;
|
||||
}
|
||||
cmds.push(format!("\"{}\" -i {} -pl {}", crate::platform::tool("nvidia-smi").display(), c.device, watts as u64));
|
||||
what.push(format!("{} {} W ({}% of {} W)", c.name, watts as u64, pct, c.power_default_w as u64));
|
||||
c.power_note = "setting the power cap (administrator prompt)".into();
|
||||
}
|
||||
let allowed = self.elevation_allowed();
|
||||
let (cmds, what, held) = power_cap_plan(&mut self.st().mining.cards, allowed, &crate::platform::tool("nvidia-smi").display().to_string());
|
||||
if held > 0 {
|
||||
self.shared.log(&format!("power cap ({why}): not asked, Power control is off in Settings ({held} card(s) would need it)"));
|
||||
}
|
||||
if cmds.is_empty() {
|
||||
return;
|
||||
|
|
@ -1486,11 +1511,36 @@ impl Engine {
|
|||
if cmds.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.shared.log(&format!("restoring the GPU power limits: {}", cmds.join(" & ")));
|
||||
match crate::platform::run_elevated(&cmds.join(" & ")) {
|
||||
Ok(()) => self.shared.log("GPU power limits restored"),
|
||||
Err(e) => self.shared.log(&format!("GPU power limits not restored ({e}); they reset at the next reboot")),
|
||||
// no administrator prompt on quit (5 October 2026: the app never asks on its own); the limits reset at the
|
||||
// next reboot, and the next start with Power control on sets them again
|
||||
self.shared.log(&format!("GPU power limits left as set (they reset at the next reboot; no prompt on quit): {}", cmds.join(" & ")));
|
||||
}
|
||||
|
||||
/// Power control (config.rs power_control): may the engine ask for administrator rights for the cap or the sweep?
|
||||
/// The elevated PC sweep job (--sweep) sets caps directly and counts as allowed.
|
||||
fn elevation_allowed(&self) -> bool {
|
||||
elevation_allowed(self.shared.settings.lock().unwrap().power_control, self.shared.runtime.sweep_only)
|
||||
}
|
||||
|
||||
/// Power control off, with the reason beside the switch and in the feed; a running or queued sweep ends.
|
||||
fn power_control_off(&mut self, note: &str) {
|
||||
{
|
||||
let mut s = self.shared.settings.lock().unwrap();
|
||||
s.power_control = false;
|
||||
s.sweep = false;
|
||||
}
|
||||
self.shared.save_settings();
|
||||
{
|
||||
let mut st = self.st();
|
||||
st.settings.power_control = false;
|
||||
st.settings.sweep = false;
|
||||
st.settings.power_note = note.to_string();
|
||||
}
|
||||
self.sweep_queue.clear();
|
||||
if self.sweep.is_some() || self.sweep_pending.is_some() {
|
||||
self.sweep_abort("power control is off");
|
||||
}
|
||||
self.shared.event(if note.starts_with("power control off:") { "error" } else { "info" }, note);
|
||||
}
|
||||
|
||||
/// nvidia-smi -l 5: power draw, GPU and memory temperature, the limit in force, every 5 s, as a child whose
|
||||
|
|
@ -1519,16 +1569,10 @@ impl Engine {
|
|||
}
|
||||
if let Some((line, what, want, since)) = self.power_via_host.clone() {
|
||||
if now.duration_since(since) > Duration::from_secs(150) {
|
||||
// no second prompt through PowerShell (5 October 2026): an unanswered prompt is a refusal
|
||||
self.power_via_host = None;
|
||||
self.shared.log("the window host did not answer the elevated step in 150 s; running it through PowerShell");
|
||||
let shared = self.shared.clone();
|
||||
std::thread::spawn(move || {
|
||||
let r = crate::platform::run_elevated(&line);
|
||||
std::thread::sleep(Duration::from_millis(800));
|
||||
let back: std::collections::HashMap<String, f64> = crate::detect::nvidia_power_limits().into_iter().map(|(k, v)| (k, v.1)).collect();
|
||||
let _ = want;
|
||||
shared.send(Cmd::PowerApplied(what, r, back));
|
||||
});
|
||||
self.shared.log(&format!("the window host did not answer the elevated step in 150 s: {line}"));
|
||||
self.finish_power(what, Err("the administrator prompt was not answered in 150 s".into()), want);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1631,12 +1675,12 @@ impl Engine {
|
|||
if self.quitting || !self.running || self.power_busy || self.job_hold || self.jobs.holds_miners() {
|
||||
return;
|
||||
}
|
||||
let (auto_on, paused) = {
|
||||
let (auto_on, paused, allowed) = {
|
||||
let s = self.shared.settings.lock().unwrap();
|
||||
let st = self.st();
|
||||
(s.sweep, st.mining.paused)
|
||||
(s.sweep, st.mining.paused, elevation_allowed(s.power_control, self.shared.runtime.sweep_only))
|
||||
};
|
||||
if paused {
|
||||
if paused || !allowed {
|
||||
return;
|
||||
}
|
||||
let unix = crate::platform::unix_now();
|
||||
|
|
@ -1767,6 +1811,9 @@ impl Engine {
|
|||
|
||||
/// The elevated helper (src/sweep.rs helper_script_*): one administrator prompt; it polls <app>/sweep/cmd.txt.
|
||||
fn sweep_helper_start(&mut self, c: &CardState) -> Result<(), String> {
|
||||
if !self.elevation_allowed() {
|
||||
return Err("Power control is off in Settings".into());
|
||||
}
|
||||
let dir = self.sweep_dir();
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
let _ = std::fs::write(dir.join("cmd.txt"), "");
|
||||
|
|
@ -3002,6 +3049,51 @@ impl Engine {
|
|||
}
|
||||
|
||||
/// The watts a card's cap asks for: power_pct of the default limit, inside the card's min and max.
|
||||
/// the project lead, 5 October 2026: "if we don't have to ask then don't ask". The NVIDIA power cap and the efficiency sweep need
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// The notice when the one prompt was refused, cancelled or not answered: Power control goes back off, no retries.
|
||||
pub const POWER_CONTROL_REFUSED: &str = "power control off: administrator rights were not given";
|
||||
|
||||
/// After the elevated step: Some(notice) when the administrator prompt was refused, cancelled or timed out (the
|
||||
/// words platform::run_elevated and the window host use), None when rights were given, even if a card then
|
||||
/// disagreed with the readback.
|
||||
fn power_control_after_prompt(r: &Result<(), String>) -> Option<&'static str> {
|
||||
match r {
|
||||
Err(e) if e.contains("administrator prompt") || e.contains("refused") || e.contains("cancel") => Some(POWER_CONTROL_REFUSED),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The nvidia-smi -pl lines one elevated step runs, the human list of what they set, and how many cards were held
|
||||
/// back because Power control is off (their note says so). Nothing is built when not allowed.
|
||||
fn power_cap_plan(cards: &mut [CardState], allowed: bool, smi: &str) -> (Vec<String>, Vec<String>, usize) {
|
||||
let mut cmds = Vec::new();
|
||||
let mut what = Vec::new();
|
||||
let mut held = 0;
|
||||
for c in cards.iter_mut().filter(|c| c.vendor == "nvidia" && c.enabled && c.power_default_w > 0.0) {
|
||||
let pct = if c.power_pct == 0 { 80 } else { c.power_pct.clamp(crate::sweep::MIN_PCT, 100) };
|
||||
c.power_pct = pct;
|
||||
let watts = requested_watts(c);
|
||||
if (c.power_limit_w - watts).abs() < 1.0 && c.power_applied {
|
||||
continue;
|
||||
}
|
||||
if !allowed {
|
||||
c.power_note = "power cap not set: Power control is off in Settings".into();
|
||||
held += 1;
|
||||
continue;
|
||||
}
|
||||
cmds.push(format!("\"{smi}\" -i {} -pl {}", c.device, watts as u64));
|
||||
what.push(format!("{} {} W ({}% of {} W)", c.name, watts as u64, pct, c.power_default_w as u64));
|
||||
c.power_note = "setting the power cap (administrator prompt)".into();
|
||||
}
|
||||
(cmds, what, held)
|
||||
}
|
||||
|
||||
fn requested_watts(c: &CardState) -> f64 {
|
||||
let pct = if c.power_pct == 0 { 80 } else { c.power_pct.clamp(crate::sweep::MIN_PCT, 100) };
|
||||
let mut w = c.power_default_w * pct as f64 / 100.0;
|
||||
|
|
@ -3108,6 +3200,42 @@ pub fn parse_race(body: &str) -> Option<RaceParsed> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn power_control_off_builds_no_elevated_command() {
|
||||
// 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));
|
||||
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() },
|
||||
];
|
||||
let (cmds, what, held) = super::power_cap_plan(&mut cards, false, "nvidia-smi");
|
||||
assert!(cmds.is_empty() && what.is_empty(), "{cmds:?}");
|
||||
assert_eq!(held, 1);
|
||||
assert_eq!(cards[0].power_note, "power cap not set: Power control is off in Settings");
|
||||
let (cmds, what, held) = super::power_cap_plan(&mut cards, true, "nvidia-smi");
|
||||
assert_eq!(cmds, vec!["\"nvidia-smi\" -i 0 -pl 460".to_string()]);
|
||||
assert_eq!(what, vec!["RTX 5090 460 W (80% of 575 W)".to_string()]);
|
||||
assert_eq!(held, 0);
|
||||
// a cap already in force asks for nothing either way
|
||||
cards[0].power_limit_w = 460.0;
|
||||
cards[0].power_applied = true;
|
||||
assert!(super::power_cap_plan(&mut cards, true, "nvidia-smi").0.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_refused_prompt_switches_power_control_off_with_the_notice() {
|
||||
let refused = Err("the administrator prompt was refused, cancelled or timed out (exit 251)".to_string());
|
||||
assert_eq!(super::power_control_after_prompt(&refused), Some("power control off: administrator rights were not given"));
|
||||
assert_eq!(super::power_control_after_prompt(&Err("the administrator prompt was cancelled".into())), Some(super::POWER_CONTROL_REFUSED));
|
||||
assert_eq!(super::power_control_after_prompt(&Err("elevated fail: the administrator prompt was cancelled".into())), Some(super::POWER_CONTROL_REFUSED));
|
||||
assert_eq!(super::power_control_after_prompt(&Err("the administrator prompt was not answered in 150 s".into())), Some(super::POWER_CONTROL_REFUSED));
|
||||
// rights given: the step ran, whatever the card then said
|
||||
assert_eq!(super::power_control_after_prompt(&Ok(())), None);
|
||||
assert_eq!(super::power_control_after_prompt(&Err("the elevated step exited with code 2".into())), None);
|
||||
}
|
||||
|
||||
use super::{parse_race, sync_decision, Reading};
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -429,7 +429,7 @@ pub fn run_elevated(cmdline: &str) -> Result<(), String> {
|
|||
Ok(())
|
||||
} else {
|
||||
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
||||
Err(if err.contains("canceled") || err.contains("cancelled") || err.is_empty() { "the administrator prompt was cancelled".into() } else { err })
|
||||
Err(elevated_failure(out.status.code(), &err))
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
|
|
@ -444,6 +444,19 @@ pub fn run_elevated(cmdline: &str) -> Result<(), String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The reason an elevated step failed, from the launcher's exit code and stderr: exit 251 (the prompt refused,
|
||||
/// cancelled or timed out, `elevated_ps_line`) and the "canceled" wording name the prompt; any other code is the
|
||||
/// step's own exit (the engine then keeps Power control on: rights were given).
|
||||
pub fn elevated_failure(code: Option<i32>, stderr: &str) -> String {
|
||||
if code == Some(ELEVATED_LAUNCH_FAILED) || stderr.contains("canceled") || stderr.contains("cancelled") {
|
||||
"the administrator prompt was refused, cancelled or timed out".into()
|
||||
} else if stderr.is_empty() {
|
||||
format!("the elevated step exited with code {}", code.map(|c| c.to_string()).unwrap_or_else(|| "?".into()))
|
||||
} else {
|
||||
stderr.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Doubles the single quotes of `s` for a single-quoted PowerShell literal.
|
||||
pub fn ps_quote(s: &str) -> String {
|
||||
s.replace('\'', "''")
|
||||
|
|
@ -500,6 +513,10 @@ mod tests {
|
|||
assert!(l.contains("exit 251 }; if ($null -eq $p) { Write-Error 'elevated launch failed: no process'; exit 251 }; exit $p.ExitCode"), "{l}");
|
||||
assert!(l.ends_with("exit $p.ExitCode"), "{l}");
|
||||
assert_eq!(super::ELEVATED_LAUNCH_FAILED, 251);
|
||||
assert_eq!(super::elevated_failure(Some(251), "elevated launch failed (UAC refused, cancelled or timed out): ..."), "the administrator prompt was refused, cancelled or timed out");
|
||||
assert_eq!(super::elevated_failure(Some(1), "The operation was canceled by the user."), "the administrator prompt was refused, cancelled or timed out");
|
||||
assert_eq!(super::elevated_failure(Some(2), ""), "the elevated step exited with code 2");
|
||||
assert_eq!(super::elevated_failure(Some(3), "nvidia-smi: bad"), "nvidia-smi: bad");
|
||||
assert_eq!(super::ps_quote("a'b''c"), "a''b''''c");
|
||||
assert_eq!(super::ps_quote("plain"), "plain");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -319,6 +319,12 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
|
|||
shared.send(Cmd::SweepPin(key, pinned));
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
// Power control (config.rs power_control): on = one administrator prompt now for the cap, off = nothing asks
|
||||
"/api/power/control" => {
|
||||
let on = body.get("on").and_then(|v| v.as_bool()).ok_or("on missing")?;
|
||||
shared.send(Cmd::PowerControl(on));
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
"/api/sweep/enable" => {
|
||||
let on = body.get("on").and_then(|v| v.as_bool()).ok_or("on missing")?;
|
||||
shared.send(Cmd::SweepEnable(on));
|
||||
|
|
|
|||
|
|
@ -205,8 +205,13 @@ pub struct SettingsState {
|
|||
pub remote_jobs: bool,
|
||||
/// the prover service (src/prover.rs)
|
||||
pub prove: bool,
|
||||
/// the efficiency sweep (src/sweep.rs): once after install, then weekly
|
||||
/// the efficiency sweep (src/sweep.rs): once after install, then weekly; effective only with `power_control`
|
||||
pub sweep: bool,
|
||||
/// the NVIDIA power cap and the sweep may ask for administrator rights (config.rs: default off, one prompt when
|
||||
/// switched on)
|
||||
pub power_control: bool,
|
||||
/// the line beside the Power control switch: why it is off, or that the rights were given
|
||||
pub power_note: String,
|
||||
/// 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`)
|
||||
|
|
|
|||
|
|
@ -392,6 +392,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
$('s-trust').addEventListener('change', function () { var on = this.checked; api('api/settings', { proof_verify_trust: on }).then(function (r) { if (r.ok) toast(on ? 'Trust mode on (devnet only); the node restarts' : 'Trust mode off; the node restarts'); else { toast(r.error || 'could not change'); $('s-trust').checked = !on; } }); });
|
||||
$('pv-setup').addEventListener('click', function () { api('api/prove/setup', {}).then(function (r) { toast(r.ok ? 'Setup started in its own window' : (r.error || 'could not start')); }); });
|
||||
$('s-jobs-allow').addEventListener('change', function () { api('api/jobs/allow', { on: this.checked }); setTimeout(fillSettings, 800); });
|
||||
$('s-power-control').addEventListener('change', function () { var on = this.checked; api('api/power/control', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Power control on: Windows asks for administrator rights once' : 'Power control off: nothing asks for administrator rights'); else { toast(r.error || 'could not change'); $('s-power-control').checked = !on; } setTimeout(fillSettings, 1500); }); });
|
||||
$('s-sweep').addEventListener('change', function () { api('api/sweep/enable', { on: this.checked }).then(function (r) { if (r.ok) toast($('s-sweep').checked ? 'Sweep on: once after install, then weekly' : 'Sweep off'); }); });
|
||||
$('s-jobs-check').addEventListener('click', function () { api('api/jobs/check', {}); $('s-jobs-note').textContent = 'Checking.'; setTimeout(fillSettings, 4000); });
|
||||
|
||||
|
|
@ -1052,7 +1053,11 @@ if (typeof document !== 'undefined') (function () {
|
|||
$('s-jobs-allow').checked = !!j.allowed;
|
||||
$('s-prove').checked = !!(state.settings && state.settings.prove);
|
||||
$('s-trust').checked = !!(state.settings && state.settings.proof_verify_trust);
|
||||
var pc = !!(state.settings && state.settings.power_control);
|
||||
$('s-power-control').checked = pc;
|
||||
$('s-power-note').textContent = (state.settings && state.settings.power_note) || '';
|
||||
$('s-sweep').checked = !!(state.settings && state.settings.sweep);
|
||||
$('s-sweep').disabled = !pc;
|
||||
$('s-jobs-key').textContent = j.key_fingerprint ? 'signing key sha256:' + j.key_fingerprint : '';
|
||||
var parts = [];
|
||||
if (j.account) parts.push(j.account);
|
||||
|
|
|
|||
|
|
@ -299,6 +299,11 @@
|
|||
<p class="note" id="s-trust-note">Only when no verifier is found next to the engine: the node then includes proof records it never checked. Never on a testnet. A found verifier always wins. Changing this restarts the node.</p>
|
||||
<label class="switch"><input type="checkbox" id="s-login"><span class="track"></span><span>Start at login</span></label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">power control</div>
|
||||
<label class="switch"><input type="checkbox" id="s-power-control"><span class="track"></span><span>Cap the NVIDIA cards' power and run the efficiency sweep</span></label>
|
||||
<p class="note">Windows asks for administrator rights once; the cap and the sweep need them. Off, the app never asks. <span id="s-power-note"></span></p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">efficiency sweep</div>
|
||||
<label class="switch"><input type="checkbox" id="s-sweep"><span class="track"></span><span>Find each NVIDIA card's best MH per watt (once after install, then weekly)</span></label>
|
||||
|
|
|
|||
28
tools/windows/power-prompts-off.ps1
Normal file
28
tools/windows/power-prompts-off.ps1
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Stops the administrator prompts a 0.3.9 Igneum Miner app raises on its own on a PC (the project lead, 5 October 2026: "if we
|
||||
# don't have to ask then don't ask"), through the settings API that app has, until the build with the Power control
|
||||
# setting ships: the weekly/first-hour efficiency sweep off (api/sweep/enable), a running sweep stopped
|
||||
# (api/sweep/stop). The power cap has no off switch in 0.3.9 (it asks at every app start and on a slider change, and
|
||||
# nowhere else); this script reports the cards' cap state so the next prompt's source is known. A signed `run` job,
|
||||
# not elevated, a few seconds:
|
||||
# packaging/ota/publish-jobs.sh add --kind run --target ae432dc7 --timeout-minutes 3 \
|
||||
# --script tools/windows/power-prompts-off.ps1 --title "PC 1: sweep off (no administrator prompts)" --deploy
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$base = (Get-Content (Join-Path $env:LOCALAPPDATA 'igneum\app\app.url') -Raw).Trim().TrimEnd('/')
|
||||
function Snapshot([string] $tag) {
|
||||
$s = Invoke-RestMethod -Uri "$base/api/state" -TimeoutSec 20
|
||||
$set = $s.settings
|
||||
"RESULT $tag settings: sweep " + $set.sweep + " | power_control " + $(if ($null -ne $set.power_control) { $set.power_control } else { '(not in this build)' }) + " | prove " + $set.prove + " | remote_jobs " + $set.remote_jobs + " | version " + $s.version
|
||||
foreach ($c in $s.mining.cards) {
|
||||
if ($c.vendor -ne 'nvidia') { continue }
|
||||
"RESULT $tag card: " + $c.name + " | enabled " + $c.enabled + " | power_pct " + $c.power_pct + " | limit " + $c.power_limit_w + " W of " + $c.power_default_w + " W default | applied " + $c.power_applied + " | pinned " + $c.pinned + " | sweep_state " + $c.sweep_state + " | note: " + $c.power_note + " | " + $c.sweep_note
|
||||
}
|
||||
}
|
||||
Snapshot 'before'
|
||||
$r = Invoke-RestMethod -Uri "$base/api/sweep/stop" -Method Post -ContentType 'application/json' -Body '{}' -TimeoutSec 30
|
||||
"RESULT api/sweep/stop: " + ($r | ConvertTo-Json -Compress)
|
||||
$r = Invoke-RestMethod -Uri "$base/api/sweep/enable" -Method Post -ContentType 'application/json' -Body '{"on":false}' -TimeoutSec 30
|
||||
"RESULT api/sweep/enable off: " + ($r | ConvertTo-Json -Compress)
|
||||
Start-Sleep -Seconds 3
|
||||
Snapshot 'after'
|
||||
"RESULT note: the 0.3.9 power cap asks only at an app start or a slider change; no setting turns it off before the Power control build"
|
||||
exit 0
|
||||
Loading…
Reference in a new issue