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)]
|
#[serde(default)]
|
||||||
pub prove: bool,
|
pub prove: bool,
|
||||||
/// The efficiency sweep (src/sweep.rs): once after install, then weekly, each NVIDIA card's cap is stepped from
|
/// 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.
|
/// 100% to 50% on the live program and held at the best MH per watt. Default off; implied by `power_control`
|
||||||
#[serde(default = "yes")]
|
/// (on when that is switched on, never effective while it is off). A pinned card is skipped.
|
||||||
|
#[serde(default)]
|
||||||
pub sweep: bool,
|
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.
|
/// When this install first ran (unix s), for the "first hour after install" sweep.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub installed_at: u64,
|
pub installed_at: u64,
|
||||||
|
|
@ -98,16 +105,26 @@ fn yes() -> bool {
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
fn default() -> 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 {
|
impl Settings {
|
||||||
pub fn load(path: &Path) -> 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 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 {
|
if s.installed_at == 0 {
|
||||||
// an install from before the sweep existed counts as installed now: it gets its first-hour sweep
|
// an install from before the sweep existed counts as installed now: it gets its first-hour sweep
|
||||||
s.installed_at = crate::platform::unix_now();
|
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.save(path);
|
||||||
}
|
}
|
||||||
s
|
s
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,8 @@ pub enum Cmd {
|
||||||
SweepStop,
|
SweepStop,
|
||||||
SweepPin(String, bool),
|
SweepPin(String, bool),
|
||||||
SweepEnable(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 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
|
/// (the engine runs elevated), Ok(false) = the elevated helper is needed; Err = nvidia-smi did not answer
|
||||||
SweepCapMode(Result<bool, String>),
|
SweepCapMode(Result<bool, String>),
|
||||||
|
|
@ -113,7 +115,7 @@ impl Shared {
|
||||||
st.mining.accepted_total = settings.accepted_total;
|
st.mining.accepted_total = settings.accepted_total;
|
||||||
st.mining.fee_total = settings.fee_total;
|
st.mining.fee_total = settings.fee_total;
|
||||||
st.address = address_state(&settings, &wallet_path);
|
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.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.live_page = packaged.live_page.clone();
|
||||||
st.finality.message = "waiting for the miner".into();
|
st.finality.message = "waiting for the miner".into();
|
||||||
|
|
@ -880,6 +882,11 @@ impl Engine {
|
||||||
if applied.is_empty() && missing.is_empty() {
|
if applied.is_empty() && missing.is_empty() {
|
||||||
self.shared.log(&format!("power cap: nothing to read back for {what}"));
|
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) => {
|
Cmd::ElevatedDone(r) => {
|
||||||
if let Some((line, what, want, _)) = self.power_via_host.take() {
|
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) {
|
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.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 !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()),
|
Some(c) => (c.name.clone(), true, String::new()),
|
||||||
None => (key.clone(), false, "no such card".to_string()),
|
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") });
|
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) => {
|
Cmd::SweepEnable(on) => {
|
||||||
|
let allowed = self.elevation_allowed();
|
||||||
|
let on = on && allowed;
|
||||||
self.shared.settings.lock().unwrap().sweep = on;
|
self.shared.settings.lock().unwrap().sweep = on;
|
||||||
self.shared.save_settings();
|
self.shared.save_settings();
|
||||||
self.st().settings.sweep = on;
|
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::SweepCapMode(r) => self.sweep_mode_known(r),
|
||||||
Cmd::SweepHelperDone(r) => {
|
Cmd::SweepHelperDone(r) => {
|
||||||
|
|
@ -1418,21 +1454,10 @@ impl Engine {
|
||||||
self.shared.log(&format!("power cap ({why}): deferred, a sweep is running"));
|
self.shared.log(&format!("power cap ({why}): deferred, a sweep is running"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut cmds = Vec::new();
|
let allowed = self.elevation_allowed();
|
||||||
let mut what = Vec::new();
|
let (cmds, what, held) = power_cap_plan(&mut self.st().mining.cards, allowed, &crate::platform::tool("nvidia-smi").display().to_string());
|
||||||
{
|
if held > 0 {
|
||||||
let mut st = self.st();
|
self.shared.log(&format!("power cap ({why}): not asked, Power control is off in Settings ({held} card(s) would need it)"));
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if cmds.is_empty() {
|
if cmds.is_empty() {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1486,11 +1511,36 @@ impl Engine {
|
||||||
if cmds.is_empty() {
|
if cmds.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.shared.log(&format!("restoring the GPU power limits: {}", cmds.join(" & ")));
|
// no administrator prompt on quit (5 October 2026: the app never asks on its own); the limits reset at the
|
||||||
match crate::platform::run_elevated(&cmds.join(" & ")) {
|
// next reboot, and the next start with Power control on sets them again
|
||||||
Ok(()) => self.shared.log("GPU power limits restored"),
|
self.shared.log(&format!("GPU power limits left as set (they reset at the next reboot; no prompt on quit): {}", cmds.join(" & ")));
|
||||||
Err(e) => self.shared.log(&format!("GPU power limits not restored ({e}); they reset at the next reboot")),
|
}
|
||||||
|
|
||||||
|
/// 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
|
/// 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 let Some((line, what, want, since)) = self.power_via_host.clone() {
|
||||||
if now.duration_since(since) > Duration::from_secs(150) {
|
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.power_via_host = None;
|
||||||
self.shared.log("the window host did not answer the elevated step in 150 s; running it through PowerShell");
|
self.shared.log(&format!("the window host did not answer the elevated step in 150 s: {line}"));
|
||||||
let shared = self.shared.clone();
|
self.finish_power(what, Err("the administrator prompt was not answered in 150 s".into()), want);
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1631,12 +1675,12 @@ impl Engine {
|
||||||
if self.quitting || !self.running || self.power_busy || self.job_hold || self.jobs.holds_miners() {
|
if self.quitting || !self.running || self.power_busy || self.job_hold || self.jobs.holds_miners() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let (auto_on, paused) = {
|
let (auto_on, paused, allowed) = {
|
||||||
let s = self.shared.settings.lock().unwrap();
|
let s = self.shared.settings.lock().unwrap();
|
||||||
let st = self.st();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
let unix = crate::platform::unix_now();
|
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.
|
/// 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> {
|
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();
|
let dir = self.sweep_dir();
|
||||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||||
let _ = std::fs::write(dir.join("cmd.txt"), "");
|
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 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 {
|
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 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;
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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};
|
use super::{parse_race, sync_decision, Reading};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -429,7 +429,7 @@ pub fn run_elevated(cmdline: &str) -> Result<(), String> {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
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")]
|
#[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.
|
/// Doubles the single quotes of `s` for a single-quoted PowerShell literal.
|
||||||
pub fn ps_quote(s: &str) -> String {
|
pub fn ps_quote(s: &str) -> String {
|
||||||
s.replace('\'', "''")
|
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.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!(l.ends_with("exit $p.ExitCode"), "{l}");
|
||||||
assert_eq!(super::ELEVATED_LAUNCH_FAILED, 251);
|
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("a'b''c"), "a''b''''c");
|
||||||
assert_eq!(super::ps_quote("plain"), "plain");
|
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));
|
shared.send(Cmd::SweepPin(key, pinned));
|
||||||
Ok(json!({ "ok": true }))
|
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" => {
|
"/api/sweep/enable" => {
|
||||||
let on = body.get("on").and_then(|v| v.as_bool()).ok_or("on missing")?;
|
let on = body.get("on").and_then(|v| v.as_bool()).ok_or("on missing")?;
|
||||||
shared.send(Cmd::SweepEnable(on));
|
shared.send(Cmd::SweepEnable(on));
|
||||||
|
|
|
||||||
|
|
@ -205,8 +205,13 @@ pub struct SettingsState {
|
||||||
pub remote_jobs: bool,
|
pub remote_jobs: bool,
|
||||||
/// the prover service (src/prover.rs)
|
/// the prover service (src/prover.rs)
|
||||||
pub prove: bool,
|
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,
|
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)
|
/// the miner software's dev fee switch (settings; `--dev-fee 0` when off)
|
||||||
pub dev_fee: bool,
|
pub dev_fee: bool,
|
||||||
/// devnet only: the node trusts proof records without a verifier (`IGNEUM_PROOF_VERIFY=trust`)
|
/// 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; } }); });
|
$('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')); }); });
|
$('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-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-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); });
|
$('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-jobs-allow').checked = !!j.allowed;
|
||||||
$('s-prove').checked = !!(state.settings && state.settings.prove);
|
$('s-prove').checked = !!(state.settings && state.settings.prove);
|
||||||
$('s-trust').checked = !!(state.settings && state.settings.proof_verify_trust);
|
$('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').checked = !!(state.settings && state.settings.sweep);
|
||||||
|
$('s-sweep').disabled = !pc;
|
||||||
$('s-jobs-key').textContent = j.key_fingerprint ? 'signing key sha256:' + j.key_fingerprint : '';
|
$('s-jobs-key').textContent = j.key_fingerprint ? 'signing key sha256:' + j.key_fingerprint : '';
|
||||||
var parts = [];
|
var parts = [];
|
||||||
if (j.account) parts.push(j.account);
|
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>
|
<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>
|
<label class="switch"><input type="checkbox" id="s-login"><span class="track"></span><span>Start at login</span></label>
|
||||||
</div>
|
</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="field">
|
||||||
<div class="k">efficiency sweep</div>
|
<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>
|
<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