Driver check: the app detects a missing or old driver per card and installs it on one click from the vendor's own server (branch driver-check, 7 October 2026)
the project lead: can the drivers be packaged with the miner, for all cards, with the system knowing which to install if not
present. Not bundled: detected and installed on one click. A per-vendor table rides the signed manifest (drivers.json:
min_version, the version on offer, the vendor's URL, size, sha256 and its source page, the silent arguments, the
restart exit codes, the Authenticode signer, the Linux and HiveOS package), validated by the signer and the app alike
(src/drivertable.rs, shared), written to <app data>/drivers.json by ota.rs. Each card's driver version is read at
every detection (nvidia-smi's driver_version; Windows' DriverVersion for AMD and Intel) and compared; a missing or
old driver puts the offer on the card's row, on the dashboard and on the first-run list. The click downloads with
curl (resume), checks size, sha256 and the Authenticode subject, runs the installer through one elevated prompt
(platform::elevated_command, the PC 1 driver job's shape), reports restart required with a Restart now button, and
never restarts by itself; the miners keep mining. macOS: no step; Linux and HiveOS: the package line. Dry run through
IGNEUM_DRIVER_DRY_RUN or the table. Tests: the table, the versions, the offers per tier, the exit codes, the
Authenticode verdicts, the download against a mocked vendor server on 127.0.0.1, the UI's strip per state; the mock's
drivers scenarios; captures light and dark in docs/plans/driver-check-shots. publish-manifest.sh --drivers carries the
table. Also the doubled #[test] in detect.rs from the cherry-pick.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit e49f988c5c)
|
|
@ -21,6 +21,9 @@
|
|||
//! exit 0 only when the signature, the zip, every
|
||||
//! unpacked file and the pinned commit all check
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[path = "../drivertable.rs"]
|
||||
mod drivertable;
|
||||
#[path = "../manifest.rs"]
|
||||
mod manifest;
|
||||
#[path = "../jobs.rs"]
|
||||
|
|
|
|||
|
|
@ -156,6 +156,8 @@ pub struct Adapter {
|
|||
pub pnp_id: String,
|
||||
/// "01:00.0" from DEVPKEY_Device_BusNumber and DEVPKEY_Device_Address; empty when PowerShell could not read them
|
||||
pub bus: String,
|
||||
/// the DriverVersion property ("32.0.101.9034"); empty when none is bound (driver-check, 7 October 2026)
|
||||
pub driver: String,
|
||||
}
|
||||
|
||||
impl Adapter {
|
||||
|
|
@ -231,7 +233,7 @@ pub fn parse_adapters(json: &str) -> Vec<Adapter> {
|
|||
(Some(b), Some(a)) => format!("{:02x}:{:02x}.{:x}", b & 0xff, (a >> 16) & 0xff, a & 0xffff),
|
||||
_ => String::new(),
|
||||
};
|
||||
Adapter { name: s(r, "Name"), status: s(r, "Status"), code: n(r, "ConfigManagerErrorCode") as u32, ram_mb: n(r, "AdapterRAM") / (1024 * 1024), processor: s(r, "VideoProcessor"), pnp_id: s(r, "PNPDeviceID"), bus }
|
||||
Adapter { name: s(r, "Name"), status: s(r, "Status"), code: n(r, "ConfigManagerErrorCode") as u32, ram_mb: n(r, "AdapterRAM") / (1024 * 1024), processor: s(r, "VideoProcessor"), pnp_id: s(r, "PNPDeviceID"), driver: s(r, "DriverVersion"), bus }
|
||||
})
|
||||
.filter(|a| !a.name.is_empty())
|
||||
.collect()
|
||||
|
|
@ -242,7 +244,7 @@ pub fn parse_adapters(json: &str) -> Vec<Adapter> {
|
|||
pub fn adapters() -> Option<Vec<Adapter>> {
|
||||
// one object per adapter, with the PCI bus number and address from the PnP properties (they name the card
|
||||
// the OpenCL worker's "pci" field names); @() keeps a single adapter an array
|
||||
let script = "$v = Get-CimInstance Win32_VideoController | ForEach-Object { $id = $_.PNPDeviceID; $bus = $null; $addr = $null; try { foreach ($x in (Get-PnpDeviceProperty -InstanceId $id -KeyName 'DEVPKEY_Device_BusNumber','DEVPKEY_Device_Address' -ErrorAction Stop)) { if ($x.KeyName -eq 'DEVPKEY_Device_BusNumber') { $bus = $x.Data } elseif ($x.KeyName -eq 'DEVPKEY_Device_Address') { $addr = $x.Data } } } catch {}; [pscustomobject]@{ Name = $_.Name; Status = $_.Status; ConfigManagerErrorCode = $_.ConfigManagerErrorCode; AdapterRAM = $_.AdapterRAM; VideoProcessor = $_.VideoProcessor; PNPDeviceID = $id; BusNumber = $bus; Address = $addr } }; ConvertTo-Json -InputObject @($v) -Compress";
|
||||
let script = "$v = Get-CimInstance Win32_VideoController | ForEach-Object { $id = $_.PNPDeviceID; $bus = $null; $addr = $null; try { foreach ($x in (Get-PnpDeviceProperty -InstanceId $id -KeyName 'DEVPKEY_Device_BusNumber','DEVPKEY_Device_Address' -ErrorAction Stop)) { if ($x.KeyName -eq 'DEVPKEY_Device_BusNumber') { $bus = $x.Data } elseif ($x.KeyName -eq 'DEVPKEY_Device_Address') { $addr = $x.Data } } } catch {}; [pscustomobject]@{ Name = $_.Name; Status = $_.Status; ConfigManagerErrorCode = $_.ConfigManagerErrorCode; AdapterRAM = $_.AdapterRAM; VideoProcessor = $_.VideoProcessor; DriverVersion = $_.DriverVersion; PNPDeviceID = $id; BusNumber = $bus; Address = $addr } }; ConvertTo-Json -InputObject @($v) -Compress";
|
||||
let out = run_timeout(Command::new(crate::platform::tool("powershell")).args(["-NoProfile", "-Command", script]), None, Duration::from_secs(15))?;
|
||||
let start = out.find(|c| c == '[' || c == '{')?;
|
||||
Some(parse_adapters(&out[start..]))
|
||||
|
|
@ -627,6 +629,8 @@ pub fn assemble(inp: Inputs) -> Detection {
|
|||
c.bus = bus;
|
||||
c.kind = classify_kind(parts[1], adapter.map(|i| &adapters[i])).into();
|
||||
c.vram_mb = mem_mb;
|
||||
// driver-check: nvidia-smi's driver_version ("581.57"); the adapter row's DriverVersion is the fallback
|
||||
c.driver_os = parts.get(4).map(|v| v.trim().to_string()).filter(|v| !v.is_empty() && !v.contains("N/A")).or_else(|| adapter.map(|i| adapters[i].driver.clone())).unwrap_or_default();
|
||||
c.path = if inp.cuda_worker { "prebuilt".into() } else { "build".into() };
|
||||
if !inp.cuda_worker {
|
||||
c.message = "no prebuilt CUDA worker in the package; built from source on first run (needs the CUDA Toolkit and Visual Studio)".into();
|
||||
|
|
@ -665,6 +669,8 @@ pub fn assemble(inp: Inputs) -> Detection {
|
|||
c.platform = format!("{} ({}), driver {}", dv.platform, dv.platform_version, dv.driver);
|
||||
c.kind = classify_kind(&dv.name, adapter.map(|i| &adapters[i])).into();
|
||||
c.vram_mb = if c.kind == "integrated" { 0 } else { dv.mem_mb };
|
||||
// driver-check: Windows' DriverVersion for the card (AMD "32.0.32015.2008", Intel "32.0.101.9034")
|
||||
c.driver_os = adapter.map(|i| adapters[i].driver.clone()).unwrap_or_default();
|
||||
c.path = "prebuilt".into();
|
||||
apply_defaults(&mut c);
|
||||
mark_sweep_support(&mut c);
|
||||
|
|
@ -680,6 +686,7 @@ pub fn assemble(inp: Inputs) -> Detection {
|
|||
let mut c = card(d.cards.len(), &a.name, vendor, if vendor == "nvidia" { "CUDA" } else { "OpenCL" }, "", "");
|
||||
c.bus = a.bus.clone();
|
||||
c.kind = if looks_integrated(&a.name) { "integrated".into() } else { "unknown".into() };
|
||||
c.driver_os = a.driver.clone();
|
||||
c.enabled = false;
|
||||
c.reason = "seen by Windows, but no worker can drive it (no NVIDIA driver and no OpenCL worker)".into();
|
||||
used.push(i);
|
||||
|
|
@ -696,6 +703,7 @@ pub fn assemble(inp: Inputs) -> Detection {
|
|||
let mut c = card(d.cards.len(), &a.name, vendor, if vendor == "nvidia" { "CUDA" } else { "OpenCL" }, "", "");
|
||||
c.bus = a.bus.clone();
|
||||
c.kind = classify_kind(&a.name, Some(a)).into();
|
||||
c.driver_os = a.driver.clone();
|
||||
mark_unusable(&mut c, &problem);
|
||||
d.cards.push(c);
|
||||
}
|
||||
|
|
@ -709,7 +717,7 @@ pub fn detect(bins: &Bins) -> Detection {
|
|||
// processor for the integrated call, the PCI address and the names for the rows
|
||||
let adapters = adapters();
|
||||
// NVIDIA: nvidia-smi ships with the driver
|
||||
let nvidia = run_timeout(Command::new(crate::platform::tool("nvidia-smi")).args(["--query-gpu=index,name,memory.total,pci.bus_id", "--format=csv,noheader"]), None, Duration::from_secs(10));
|
||||
let nvidia = run_timeout(Command::new(crate::platform::tool("nvidia-smi")).args(["--query-gpu=index,name,memory.total,pci.bus_id,driver_version", "--format=csv,noheader"]), None, Duration::from_secs(10));
|
||||
let nvidia_limits = if nvidia.is_some() { nvidia_power_limits() } else { Default::default() };
|
||||
// OpenCL: the worker's own device list (AMD, Intel; NVIDIA shows there too and is skipped)
|
||||
let opencl = bins.opencl.as_ref().and_then(|cl| run_timeout(Command::new(cl).arg("--list"), None, Duration::from_secs(15)));
|
||||
|
|
|
|||
205
app/igneum-app/src/drivers.rs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
//! Driver check (branch driver-check, 7 October 2026; the project lead: "can we package the drivers with the miner? for all
|
||||
//! cards? and the system knows which to install if not present"). The answer given: not bundled (size, vendor
|
||||
//! licences, staleness), but detected and installed on one click.
|
||||
//!
|
||||
//! The manifest carries a per-vendor table (`drivers`, signed with the rest): the version the worker needs at least,
|
||||
//! the version on offer, the vendor's own download URL, size, sha256 and the page the hash was read from, the
|
||||
//! installer's silent arguments and the exit codes that mean "restart required". The app never ships a driver: the
|
||||
//! table rides the manifest (ota.rs writes `<app data>/drivers.json`), so a new vendor release is a manifest publish.
|
||||
//!
|
||||
//! At every detection each card's driver version (nvidia-smi's for NVIDIA, Windows' DriverVersion for AMD and Intel)
|
||||
//! is compared with the table; a missing or old driver puts an offer on the card's row ("Install the NVIDIA driver
|
||||
//! 581.57, 650 MB"). The click downloads from the vendor's server (curl with resume), checks size, sha256 and the
|
||||
//! Authenticode signature (the signer must be the vendor), then runs the installer through ONE elevated prompt
|
||||
//! (platform::elevated_command, the PC 1 driver job's fetch, verify and -s shape), reports "restart required" with a
|
||||
//! Restart now button and never restarts by itself. Nothing else pauses: the miners keep mining through it.
|
||||
//! macOS: no driver step (the row says so). Linux and HiveOS: a line naming the package, no installer.
|
||||
//!
|
||||
//! Dry run (`IGNEUM_DRIVER_DRY_RUN=1`, or `dry_run: true` in the table): the download and every check run, the
|
||||
//! installer does not: the install step reports what it would have run and exit 0. The tests feed the table a
|
||||
//! 127.0.0.1 URL served by a std TcpListener (the mocked vendor response).
|
||||
|
||||
use crate::engine::{Cmd, Shared};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
pub use crate::drivertable::{exit_meaning, offer_for, platform_word, DriverState, Offer, Table, VendorEntry};
|
||||
#[cfg(windows)]
|
||||
use crate::drivertable::authenticode_verdict;
|
||||
|
||||
/// The install thread's reports.
|
||||
pub enum Event {
|
||||
Progress(f64, String),
|
||||
/// the installer ran: its exit code and whether that means a restart
|
||||
Installed(Result<(i64, bool, String), String>),
|
||||
}
|
||||
|
||||
fn curl(args: &[&str], limit: Duration) -> Result<(), String> {
|
||||
let mut c = Command::new(crate::platform::tool("curl"));
|
||||
c.args(args);
|
||||
let out = crate::detect::run_timeout(&mut c, None, limit).ok_or("curl is not available")?;
|
||||
let t = out.trim().to_string();
|
||||
if t.is_empty() { Ok(()) } else { Err(t.lines().last().unwrap_or("curl failed").to_string()) }
|
||||
}
|
||||
|
||||
/// Downloads the vendor's file with resume into `dir`, checks size and sha256, renames `.part` to the final name.
|
||||
pub fn download(e: &VendorEntry, dir: &Path) -> Result<PathBuf, String> {
|
||||
let name = e.url.rsplit('/').next().filter(|n| !n.is_empty() && !n.contains("..")).unwrap_or("driver.exe").to_string();
|
||||
let final_path = dir.join(&name);
|
||||
let part = dir.join(format!("{name}.part"));
|
||||
std::fs::create_dir_all(dir).map_err(|x| format!("cannot make {}: {x}", dir.display()))?;
|
||||
if final_path.is_file() && std::fs::metadata(&final_path).map(|m| m.len()).unwrap_or(0) == e.size && crate::manifest::sha256_file(&final_path).map(|s| s == e.sha256).unwrap_or(false) {
|
||||
return Ok(final_path);
|
||||
}
|
||||
let _ = std::fs::remove_file(&final_path);
|
||||
let have = std::fs::metadata(&part).map(|m| m.len()).unwrap_or(0);
|
||||
if have > e.size {
|
||||
let _ = std::fs::remove_file(&part);
|
||||
}
|
||||
if have != e.size {
|
||||
curl(&["-fsSL", "--retry", "3", "--retry-delay", "5", "-C", "-", "--max-time", "7200", "-o", &part.display().to_string(), &e.url], Duration::from_secs(7260))?;
|
||||
}
|
||||
let got = std::fs::metadata(&part).map(|m| m.len()).unwrap_or(0);
|
||||
if got != e.size {
|
||||
let _ = std::fs::remove_file(&part);
|
||||
return Err(format!("size mismatch: got {got} bytes, the table says {}", e.size));
|
||||
}
|
||||
let sum = crate::manifest::sha256_file(&part).map_err(|x| x.to_string())?;
|
||||
if sum != e.sha256 {
|
||||
let _ = std::fs::remove_file(&part);
|
||||
return Err(format!("sha256 mismatch: the file is not the one the table names (page {})", e.hash_source));
|
||||
}
|
||||
std::fs::rename(&part, &final_path).map_err(|x| x.to_string())?;
|
||||
Ok(final_path)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn authenticode(path: &Path, signer: &str) -> Result<String, String> {
|
||||
let ps = format!("$s = Get-AuthenticodeSignature -LiteralPath '{}'; Write-Output ('status=' + $s.Status); Write-Output ('subject=' + $s.SignerCertificate.Subject)", path.display().to_string().replace('\'', "''"));
|
||||
let out = crate::detect::run_timeout(Command::new(crate::platform::tool("powershell")).args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps]), None, Duration::from_secs(60)).ok_or("PowerShell did not answer the signature check")?;
|
||||
authenticode_verdict(&out, signer)
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn authenticode(_path: &Path, _signer: &str) -> Result<String, String> {
|
||||
Err("the installer runs on Windows only".into())
|
||||
}
|
||||
|
||||
/// The click: download, verify, run elevated (one prompt), report. Runs on its own thread; the miners keep mining.
|
||||
pub fn start_install(shared: &Arc<Shared>, e: VendorEntry, dir: PathBuf, dry_run: bool) {
|
||||
let shared2 = shared.clone();
|
||||
std::thread::spawn(move || {
|
||||
shared2.send(Cmd::Driver(Event::Progress(0.05, format!("downloading {} ({} MB) from {}", e.version, (e.size + 512 * 1024) / (1024 * 1024), host_of(&e.url)))));
|
||||
let file = match download(&e, &dir) {
|
||||
Ok(f) => f,
|
||||
Err(x) => {
|
||||
shared2.send(Cmd::Driver(Event::Installed(Err(format!("download: {x}")))));
|
||||
return;
|
||||
}
|
||||
};
|
||||
shared2.send(Cmd::Driver(Event::Progress(0.6, "size and sha256 match the table; checking the signature".into())));
|
||||
if !dry_run {
|
||||
if let Err(x) = authenticode(&file, &e.signer) {
|
||||
shared2.send(Cmd::Driver(Event::Installed(Err(x))));
|
||||
return;
|
||||
}
|
||||
}
|
||||
shared2.send(Cmd::Driver(Event::Progress(0.7, format!("{}installing {} {}: Windows asks for permission once", if dry_run { "dry run: " } else { "" }, e.vendor.to_ascii_uppercase(), e.version))));
|
||||
if dry_run {
|
||||
shared2.send(Cmd::Driver(Event::Installed(Ok((0, false, format!("dry run: would run {} {}", file.display(), e.args.join(" ")))))));
|
||||
return;
|
||||
}
|
||||
let args = e.args.join(" ");
|
||||
let mut c = crate::platform::elevated_command(&file.display().to_string(), &args);
|
||||
let out = crate::detect::run_timeout(&mut c, None, Duration::from_secs(1800));
|
||||
let code = out.as_deref().and_then(|t| t.lines().rev().find_map(|l| l.trim().parse::<i64>().ok())).unwrap_or(-1);
|
||||
// elevated_ps_line prints nothing on success and exits with the installer's code; run_timeout only gives
|
||||
// stdout, so the exit code is read from the wrapper's own echo below
|
||||
let code = exit_code_of(&file, &args, code);
|
||||
let (reboot, text) = exit_meaning(code, &e);
|
||||
shared2.send(Cmd::Driver(Event::Installed(Ok((code, reboot, text)))));
|
||||
});
|
||||
}
|
||||
|
||||
/// The elevated wrapper's exit code: `elevated_ps_line` exits with the installer's code, which run_timeout does not
|
||||
/// return; so the installer is run through a second form that echoes the code on its last line.
|
||||
#[cfg(windows)]
|
||||
fn exit_code_of(file: &Path, args: &str, _seen: i64) -> i64 {
|
||||
let line = format!("{}; Write-Output ('exit=' + $LASTEXITCODE)", crate::platform::elevated_ps_line(&file.display().to_string(), args).trim_end_matches("exit $p.ExitCode").to_string() + "$global:LASTEXITCODE = $p.ExitCode");
|
||||
let mut c = Command::new(crate::platform::tool("powershell"));
|
||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &line]);
|
||||
crate::platform::quiet(&mut c);
|
||||
let out = crate::detect::run_timeout(&mut c, None, Duration::from_secs(1800)).unwrap_or_default();
|
||||
out.lines().rev().find_map(|l| l.trim().strip_prefix("exit=").and_then(|v| v.parse::<i64>().ok())).unwrap_or(-1)
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn exit_code_of(_file: &Path, _args: &str, seen: i64) -> i64 {
|
||||
seen
|
||||
}
|
||||
|
||||
fn host_of(url: &str) -> String {
|
||||
url.split("//").nth(1).and_then(|r| r.split('/').next()).unwrap_or("the vendor").to_string()
|
||||
}
|
||||
|
||||
/// "Restart now": a plain restart in 20 s (no elevation needed for the signed-in user); never called by the app itself.
|
||||
pub fn restart_now() -> Result<(), String> {
|
||||
if !cfg!(windows) {
|
||||
return Err("restart from the app is for Windows only".into());
|
||||
}
|
||||
let root = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".into());
|
||||
let mut c = Command::new(format!("{root}\\System32\\shutdown.exe"));
|
||||
c.args(["/r", "/t", "20", "/c", "Igneum Miner: restarting to finish the driver install"]);
|
||||
crate::platform::quiet(&mut c);
|
||||
match crate::detect::run_timeout(&mut c, None, Duration::from_secs(20)) {
|
||||
Some(t) if t.trim().is_empty() => Ok(()),
|
||||
Some(t) => Err(t.trim().to_string()),
|
||||
None => Err("shutdown.exe did not answer".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod download_tests {
|
||||
use super::*;
|
||||
|
||||
/// The mocked vendor response: a std TcpListener on 127.0.0.1 serves the file; the right sha256 passes, a wrong
|
||||
/// one is refused and the part file is gone; a second call with the file in place downloads nothing.
|
||||
#[test]
|
||||
fn download_verifies_against_a_mocked_vendor_server() {
|
||||
use std::io::{Read, Write};
|
||||
let body: Vec<u8> = (0..100_000u32).map(|i| (i % 251) as u8).collect();
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let served = body.clone();
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().take(3) {
|
||||
let mut s = stream.unwrap();
|
||||
let mut buf = [0u8; 4096];
|
||||
let _ = s.read(&mut buf);
|
||||
let head = format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: application/octet-stream\r\nConnection: close\r\n\r\n", served.len());
|
||||
let _ = s.write_all(head.as_bytes());
|
||||
let _ = s.write_all(&served);
|
||||
}
|
||||
});
|
||||
let dir = std::env::temp_dir().join(format!("igneum-driver-test-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
use sha2::Digest;
|
||||
let sum = format!("{:x}", sha2::Sha256::digest(&body));
|
||||
let mut e = VendorEntry { vendor: "intel".into(), version: "1.0".into(), min_version: "1.0".into(), url: format!("http://127.0.0.1:{port}/gfx_test.exe"), size: body.len() as u64, sha256: sum.clone(), args: vec!["-s".into()], signer: "Intel".into(), ..Default::default() };
|
||||
let f = download(&e, &dir).expect("the right sha256 passes");
|
||||
assert_eq!(f.file_name().unwrap(), "gfx_test.exe");
|
||||
assert_eq!(std::fs::read(&f).unwrap(), body);
|
||||
assert!(download(&e, &dir).is_ok(), "the file in place is kept without a second download");
|
||||
let _ = std::fs::remove_file(&f);
|
||||
e.sha256 = "0".repeat(64);
|
||||
let err = download(&e, &dir).unwrap_err();
|
||||
assert!(err.contains("sha256 mismatch"), "{err}");
|
||||
assert!(!dir.join("gfx_test.exe.part").exists() && !dir.join("gfx_test.exe").exists());
|
||||
e.sha256 = sum;
|
||||
e.size = 7;
|
||||
assert!(download(&e, &dir).unwrap_err().contains("size mismatch"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
338
app/igneum-app/src/drivertable.rs
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
//! The driver table and the offer decision (branch driver-check, 7 October 2026): the pure half of src/drivers.rs,
|
||||
//! with no dependency on the engine, so the signer binary (src/bin/ota-sign.rs) validates a manifest's `drivers`
|
||||
//! object the way the app does. The install thread, the download and the Authenticode call live in drivers.rs.
|
||||
|
||||
use serde::Serialize;
|
||||
use std::path::Path;
|
||||
|
||||
/// One vendor's row of the table.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
pub struct VendorEntry {
|
||||
pub vendor: String,
|
||||
/// the lowest version the worker runs on (NVIDIA: nvidia-smi's "570.00"; AMD and Intel: Windows' DriverVersion "32.0.101.9034")
|
||||
pub min_version: String,
|
||||
/// what the click installs
|
||||
pub version: String,
|
||||
pub url: String,
|
||||
pub size: u64,
|
||||
pub sha256: String,
|
||||
/// where the sha256 was read (the vendor's page), for the record on the row
|
||||
pub hash_source: String,
|
||||
/// the installer's silent arguments
|
||||
pub args: Vec<String>,
|
||||
/// exit codes that mean "installed, restart required"
|
||||
pub reboot_codes: Vec<i64>,
|
||||
/// a word the Authenticode subject must carry ("NVIDIA", "Advanced Micro Devices", "Intel")
|
||||
pub signer: String,
|
||||
/// Linux and HiveOS: the package to name, no installer
|
||||
pub linux_package: String,
|
||||
pub hive_package: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Table {
|
||||
pub updated: String,
|
||||
pub dry_run: bool,
|
||||
pub vendors: Vec<VendorEntry>,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
/// The manifest's `drivers` object. Every entry is checked the way the platform entries are: https (or 127.0.0.1
|
||||
/// for a test), 64-hex sha256, a size, a version on both sides, at least one silent argument.
|
||||
pub fn parse(v: &serde_json::Value) -> Result<Table, String> {
|
||||
let s = |v: &serde_json::Value, k: &str| v.get(k).and_then(|x| x.as_str()).unwrap_or("").trim().to_string();
|
||||
let obj = v.get("vendors").and_then(|x| x.as_object()).ok_or("drivers: no vendors object")?;
|
||||
let mut vendors = Vec::new();
|
||||
for (name, e) in obj {
|
||||
if e.is_null() {
|
||||
continue;
|
||||
}
|
||||
let vendor = name.to_ascii_lowercase();
|
||||
if !["nvidia", "amd", "intel"].contains(&vendor.as_str()) {
|
||||
return Err(format!("drivers: vendor '{name}' is not nvidia, amd or intel"));
|
||||
}
|
||||
let entry = VendorEntry {
|
||||
vendor: vendor.clone(),
|
||||
min_version: s(e, "min_version"),
|
||||
version: s(e, "version"),
|
||||
url: s(e, "url"),
|
||||
size: e.get("size").and_then(|x| x.as_u64()).unwrap_or(0),
|
||||
sha256: s(e, "sha256").to_ascii_lowercase(),
|
||||
hash_source: s(e, "hash_source"),
|
||||
args: e.get("args").and_then(|a| a.as_array()).map(|a| a.iter().filter_map(|x| x.as_str()).map(|x| x.to_string()).collect()).unwrap_or_default(),
|
||||
reboot_codes: e.get("reboot_codes").and_then(|a| a.as_array()).map(|a| a.iter().filter_map(|x| x.as_i64()).collect()).unwrap_or_default(),
|
||||
signer: s(e, "signer"),
|
||||
linux_package: s(e, "linux_package"),
|
||||
hive_package: s(e, "hive_package"),
|
||||
};
|
||||
if parse_dotted(&entry.min_version).is_none() || parse_dotted(&entry.version).is_none() {
|
||||
return Err(format!("drivers.{name}: min_version and version must be dotted numbers"));
|
||||
}
|
||||
if !entry.url.starts_with("https://") && !entry.url.starts_with("http://127.0.0.1:") {
|
||||
return Err(format!("drivers.{name}: the url is not https"));
|
||||
}
|
||||
if entry.sha256.len() != 64 || !entry.sha256.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Err(format!("drivers.{name}: sha256 is not 64 hex characters"));
|
||||
}
|
||||
if entry.size == 0 {
|
||||
return Err(format!("drivers.{name}: size is missing"));
|
||||
}
|
||||
if entry.args.is_empty() || entry.args.iter().any(|a| a.contains(['"', '\'', '&', '|', ';', '`'])) {
|
||||
return Err(format!("drivers.{name}: args must be plain switches"));
|
||||
}
|
||||
if entry.signer.is_empty() {
|
||||
return Err(format!("drivers.{name}: signer is missing (the Authenticode subject word)"));
|
||||
}
|
||||
vendors.push(entry);
|
||||
}
|
||||
if vendors.is_empty() {
|
||||
return Err("drivers: the vendors object is empty".into());
|
||||
}
|
||||
vendors.sort_by(|a, b| a.vendor.cmp(&b.vendor));
|
||||
Ok(Table { updated: s(v, "updated"), dry_run: v.get("dry_run").and_then(|x| x.as_bool()).unwrap_or(false), vendors })
|
||||
}
|
||||
|
||||
pub fn entry(&self, vendor: &str) -> Option<&VendorEntry> {
|
||||
self.vendors.iter().find(|e| e.vendor == vendor)
|
||||
}
|
||||
}
|
||||
|
||||
/// "32.0.101.9034" or "581.57" as numbers; None for anything else (an empty string, "3683.0 (PAL,LC)").
|
||||
pub fn parse_dotted(s: &str) -> Option<Vec<u64>> {
|
||||
let t = s.trim();
|
||||
if t.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for p in t.split('.') {
|
||||
out.push(p.trim().parse::<u64>().ok()?);
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// `a` is at least `b`, compared part by part (a missing trailing part reads 0).
|
||||
pub fn at_least(a: &[u64], b: &[u64]) -> bool {
|
||||
let n = a.len().max(b.len());
|
||||
for i in 0..n {
|
||||
let (x, y) = (a.get(i).copied().unwrap_or(0), b.get(i).copied().unwrap_or(0));
|
||||
if x != y {
|
||||
return x > y;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// What the card's row shows about its driver.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
pub struct Offer {
|
||||
pub vendor: String,
|
||||
/// "missing" | "old" | "fine" | "none" (no row for this vendor in the table)
|
||||
pub status: String,
|
||||
pub installed: String,
|
||||
pub wanted: String,
|
||||
pub min_version: String,
|
||||
pub size: u64,
|
||||
pub url: String,
|
||||
pub hash_source: String,
|
||||
/// the row's sentence
|
||||
pub text: String,
|
||||
/// true when the click can install here (Windows with an entry); false with a note on macOS, Linux and HiveOS
|
||||
pub installable: bool,
|
||||
}
|
||||
|
||||
/// The platform word for the notes: "windows" | "macos" | "linux" | "hive".
|
||||
pub fn platform_word() -> &'static str {
|
||||
if cfg!(windows) {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else if Path::new("/hive").is_dir() || Path::new("/hive-config").is_dir() {
|
||||
"hive"
|
||||
} else {
|
||||
"linux"
|
||||
}
|
||||
}
|
||||
|
||||
/// The offer for a card: its vendor, the version its driver reports (empty = none found) and the table.
|
||||
pub fn offer_for(vendor: &str, installed: &str, table: Option<&Table>, platform: &str) -> Option<Offer> {
|
||||
if vendor == "apple" {
|
||||
return Some(Offer { vendor: "apple".into(), status: "none".into(), installed: installed.into(), text: "Apple silicon: no driver step, macOS carries it.".into(), ..Default::default() });
|
||||
}
|
||||
let e = table.and_then(|t| t.entry(vendor))?;
|
||||
let word = match vendor {
|
||||
"nvidia" => "NVIDIA",
|
||||
"amd" => "AMD",
|
||||
"intel" => "Intel Arc",
|
||||
_ => vendor,
|
||||
};
|
||||
let mb = (e.size + 512 * 1024) / (1024 * 1024);
|
||||
let size_text = if mb >= 1024 { format!("{:.1} GB", mb as f64 / 1024.0) } else { format!("{mb} MB") };
|
||||
let have = parse_dotted(installed);
|
||||
let need = parse_dotted(&e.min_version).unwrap_or_default();
|
||||
let status = match &have {
|
||||
None => "missing",
|
||||
Some(h) if at_least(h, &need) => "fine",
|
||||
Some(_) => "old",
|
||||
};
|
||||
let base = Offer { vendor: vendor.into(), status: status.into(), installed: installed.into(), wanted: e.version.clone(), min_version: e.min_version.clone(), size: e.size, url: e.url.clone(), hash_source: e.hash_source.clone(), text: String::new(), installable: false };
|
||||
let text = match (platform, status) {
|
||||
("macos", _) => "macOS: no driver step.".to_string(),
|
||||
("hive", "fine") | ("linux", "fine") => format!("{word} driver {installed}: fine."),
|
||||
("hive", _) => format!("HiveOS: {} the driver with {} (the worker needs {} or newer).", if status == "missing" { "install" } else { "update" }, if e.hive_package.is_empty() { "hive's driver tool" } else { &e.hive_package }, e.min_version),
|
||||
("linux", _) => format!("Linux: {} the package {} (the worker needs {} or newer).", if status == "missing" { "install" } else { "update" }, if e.linux_package.is_empty() { "the vendor's driver" } else { &e.linux_package }, e.min_version),
|
||||
(_, "fine") => format!("{word} driver {installed}: fine."),
|
||||
(_, "missing") => format!("Install the {word} driver {} ({size_text}). No driver was found, so this card cannot mine yet.", e.version),
|
||||
_ => format!("Install the {word} driver {} ({size_text}). Driver {installed} is older than the {} the worker needs.", e.version, e.min_version),
|
||||
};
|
||||
Some(Offer { text, installable: platform == "windows" && status != "fine", ..base })
|
||||
}
|
||||
|
||||
/// The installer's exit code read: (restart required, the sentence).
|
||||
pub fn exit_meaning(code: i64, e: &VendorEntry) -> (bool, String) {
|
||||
if code == 0 {
|
||||
return (false, format!("{} driver {} installed.", e.vendor.to_ascii_uppercase(), e.version));
|
||||
}
|
||||
if e.reboot_codes.contains(&code) {
|
||||
return (true, format!("{} driver {} installed. Restart Windows to finish.", e.vendor.to_ascii_uppercase(), e.version));
|
||||
}
|
||||
(false, format!("the {} installer exited with code {code}.", e.vendor.to_ascii_uppercase()))
|
||||
}
|
||||
|
||||
/// The install's progress, published as `state.drivers`.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
pub struct DriverState {
|
||||
/// idle | downloading | verifying | installing | done | reboot | error
|
||||
pub status: String,
|
||||
pub vendor: String,
|
||||
pub version: String,
|
||||
pub progress: f64,
|
||||
pub message: String,
|
||||
pub error: String,
|
||||
pub reboot_required: bool,
|
||||
pub dry_run: bool,
|
||||
pub started_at: f64,
|
||||
pub finished_at: f64,
|
||||
/// the table's updated stamp, for the Settings page ("drivers table of 7 October")
|
||||
pub table_updated: String,
|
||||
pub platform: String,
|
||||
}
|
||||
|
||||
/// The Authenticode verdict from `Get-AuthenticodeSignature`'s two lines ("Valid" and the subject): Ok(subject) when
|
||||
/// the status is Valid and the subject carries the vendor's word.
|
||||
pub fn authenticode_verdict(text: &str, signer: &str) -> Result<String, String> {
|
||||
let mut status = String::new();
|
||||
let mut subject = String::new();
|
||||
for l in text.lines() {
|
||||
if let Some(v) = l.strip_prefix("status=") {
|
||||
status = v.trim().to_string();
|
||||
} else if let Some(v) = l.strip_prefix("subject=") {
|
||||
subject = v.trim().to_string();
|
||||
}
|
||||
}
|
||||
if status != "Valid" {
|
||||
return Err(format!("the file's signature is {}: not installed", if status.is_empty() { "unreadable".to_string() } else { status }));
|
||||
}
|
||||
if !subject.to_ascii_lowercase().contains(&signer.to_ascii_lowercase()) {
|
||||
return Err(format!("the file is signed by \"{subject}\", not {signer}: not installed"));
|
||||
}
|
||||
Ok(subject)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const TABLE: &str = r#"{"updated":"2026-10-07","vendors":{
|
||||
"nvidia":{"min_version":"570.00","version":"581.57","url":"https://us.download.nvidia.com/Windows/581.57/581.57-desktop-win10-win11-64bit-international-dch-whql.exe","size":1012345678,"sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","hash_source":"https://www.nvidia.com/en-us/drivers/details/","args":["-s","-noreboot"],"reboot_codes":[1],"signer":"NVIDIA","linux_package":"nvidia-driver-570","hive_package":"nvidia-driver-update 570"},
|
||||
"amd":{"min_version":"32.0.32000.0","version":"26.9.2","url":"https://drivers.amd.com/drivers/whql-amd-software-adrenalin-edition-26.9.2-win10-win11-sep2026.exe","size":912345678,"sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","hash_source":"https://www.amd.com/en/support/download/drivers.html","args":["-install","-silent"],"reboot_codes":[3010],"signer":"Advanced Micro Devices","linux_package":"amdgpu-install --usecase=opencl"},
|
||||
"intel":{"min_version":"32.0.101.9034","version":"32.0.101.9034","url":"https://downloadmirror.intel.com/929959/gfx_win_101.9034.exe","size":932631144,"sha256":"72ba7eea08a0cc18650603976ff9433dfdf84d23d4cbbee662a4df9f2856ae98","hash_source":"https://www.intel.com/content/www/us/en/download/785597/","args":["-s"],"reboot_codes":[14,1014],"signer":"Intel","linux_package":"intel-opencl-icd"}}}"#;
|
||||
|
||||
fn table() -> Table {
|
||||
Table::parse(&serde_json::from_str(TABLE).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn versions_compare_part_by_part() {
|
||||
assert!(at_least(&parse_dotted("32.0.101.9034").unwrap(), &parse_dotted("32.0.101.9034").unwrap()));
|
||||
assert!(at_least(&parse_dotted("32.0.101.9040").unwrap(), &parse_dotted("32.0.101.9034").unwrap()));
|
||||
assert!(!at_least(&parse_dotted("32.0.101.6733").unwrap(), &parse_dotted("32.0.101.9034").unwrap()));
|
||||
assert!(at_least(&parse_dotted("581.57").unwrap(), &parse_dotted("570.00").unwrap()));
|
||||
assert!(!at_least(&parse_dotted("566.36").unwrap(), &parse_dotted("570").unwrap()));
|
||||
assert!(at_least(&parse_dotted("570").unwrap(), &parse_dotted("570.0.0").unwrap()));
|
||||
assert_eq!(parse_dotted("3683.0 (PAL,LC)"), None);
|
||||
assert_eq!(parse_dotted(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_table_parses_and_bad_rows_are_refused() {
|
||||
let t = table();
|
||||
assert_eq!(t.vendors.len(), 3);
|
||||
assert_eq!(t.entry("intel").unwrap().reboot_codes, vec![14, 1014]);
|
||||
assert_eq!(t.entry("nvidia").unwrap().args, vec!["-s", "-noreboot"]);
|
||||
assert!(!t.dry_run);
|
||||
let bad = |patch: &str, what: &str| {
|
||||
let txt = TABLE.replacen(patch, what, 1);
|
||||
let e = Table::parse(&serde_json::from_str(&txt).unwrap()).unwrap_err();
|
||||
e
|
||||
};
|
||||
assert!(bad("https://downloadmirror", "http://downloadmirror").contains("https"));
|
||||
assert!(bad("\"size\":932631144", "\"size\":0").contains("size"));
|
||||
assert!(bad("\"args\":[\"-s\"]", "\"args\":[\"-s; calc\"]").contains("args"));
|
||||
assert!(bad("\"signer\":\"Intel\"", "\"signer\":\"\"").contains("signer"));
|
||||
assert!(bad("\"min_version\":\"570.00\"", "\"min_version\":\"latest\"").contains("dotted"));
|
||||
assert!(Table::parse(&serde_json::json!({"vendors": {"apple": {}}})).unwrap_err().contains("vendor"));
|
||||
assert!(Table::parse(&serde_json::json!({"vendors": {}})).unwrap_err().contains("empty"));
|
||||
assert!(Table::parse(&serde_json::json!({"vendors": {"intel": null}})).unwrap_err().contains("empty"));
|
||||
}
|
||||
|
||||
/// The offers per tier: a missing driver, an old one, a fine one, Apple, Linux and HiveOS.
|
||||
#[test]
|
||||
fn offers_name_the_version_the_size_and_the_reason() {
|
||||
let t = table();
|
||||
let o = offer_for("intel", "", Some(&t), "windows").unwrap();
|
||||
assert_eq!(o.status, "missing");
|
||||
assert!(o.installable);
|
||||
assert_eq!(o.text, "Install the Intel Arc driver 32.0.101.9034 (889 MB). No driver was found, so this card cannot mine yet.");
|
||||
let o = offer_for("intel", "32.0.101.6733", Some(&t), "windows").unwrap();
|
||||
assert_eq!(o.status, "old");
|
||||
assert_eq!(o.text, "Install the Intel Arc driver 32.0.101.9034 (889 MB). Driver 32.0.101.6733 is older than the 32.0.101.9034 the worker needs.");
|
||||
let o = offer_for("intel", "32.0.101.9034", Some(&t), "windows").unwrap();
|
||||
assert_eq!((o.status.as_str(), o.installable, o.text.as_str()), ("fine", false, "Intel Arc driver 32.0.101.9034: fine."));
|
||||
let o = offer_for("nvidia", "581.57", Some(&t), "windows").unwrap();
|
||||
assert_eq!(o.status, "fine");
|
||||
let o = offer_for("nvidia", "566.36", Some(&t), "windows").unwrap();
|
||||
assert_eq!(o.text, "Install the NVIDIA driver 581.57 (965 MB). Driver 566.36 is older than the 570.00 the worker needs.");
|
||||
let o = offer_for("amd", "32.0.21042.62", Some(&t), "windows").unwrap();
|
||||
assert_eq!(o.status, "old");
|
||||
assert!(o.text.starts_with("Install the AMD driver 26.9.2 (870 MB)."));
|
||||
// Apple: no step; Linux and HiveOS: the package, nothing installable
|
||||
let o = offer_for("apple", "", Some(&t), "macos").unwrap();
|
||||
assert_eq!((o.status.as_str(), o.installable), ("none", false));
|
||||
assert!(o.text.contains("no driver step"));
|
||||
let o = offer_for("nvidia", "", Some(&t), "linux").unwrap();
|
||||
assert_eq!(o.text, "Linux: install the package nvidia-driver-570 (the worker needs 570.00 or newer).");
|
||||
assert!(!o.installable);
|
||||
let o = offer_for("nvidia", "566.36", Some(&t), "hive").unwrap();
|
||||
assert_eq!(o.text, "HiveOS: update the driver with nvidia-driver-update 570 (the worker needs 570.00 or newer).");
|
||||
let o = offer_for("intel", "", Some(&t), "macos").unwrap();
|
||||
assert_eq!(o.text, "macOS: no driver step.");
|
||||
// no table, or a vendor the table lacks: no offer, the row says nothing
|
||||
assert!(offer_for("nvidia", "", None, "windows").is_none());
|
||||
assert!(offer_for("other", "", Some(&t), "windows").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_codes_and_signatures_read_as_the_vendor_means_them() {
|
||||
let t = table();
|
||||
let intel = t.entry("intel").unwrap();
|
||||
assert_eq!(exit_meaning(0, intel), (false, "INTEL driver 32.0.101.9034 installed.".into()));
|
||||
assert_eq!(exit_meaning(1014, intel).0, true);
|
||||
assert_eq!(exit_meaning(14, intel).1, "INTEL driver 32.0.101.9034 installed. Restart Windows to finish.");
|
||||
assert_eq!(exit_meaning(1007, intel), (false, "the INTEL installer exited with code 1007.".into()));
|
||||
assert!(authenticode_verdict("status=Valid\nsubject=CN=Intel Corporation, O=Intel Corporation, S=California, C=US\n", "Intel").is_ok());
|
||||
assert!(authenticode_verdict("status=NotSigned\nsubject=\n", "Intel").unwrap_err().contains("NotSigned"));
|
||||
assert!(authenticode_verdict("status=Valid\nsubject=CN=Someone Else\n", "Intel").unwrap_err().contains("Someone Else"));
|
||||
assert!(authenticode_verdict("", "Intel").unwrap_err().contains("unreadable"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -36,6 +36,10 @@ pub enum Cmd {
|
|||
/// the node's finality fields from igneum_getProvingStatus (None: the node carries none, or did not answer)
|
||||
FinalityStatus(Option<crate::ember::FinalityStatus>),
|
||||
ApplyCards(Vec<CardChoice>),
|
||||
/// driver-check (7 October 2026): the row's Install click, the Restart now click, the install thread's reports
|
||||
DriverInstall(String),
|
||||
DriverRestart,
|
||||
Driver(crate::drivers::Event),
|
||||
/// one enumeration of the cards finished (the first, or a re-detection: src/hotplug.rs)
|
||||
Detected(crate::detect::Detection),
|
||||
Start,
|
||||
|
|
@ -661,6 +665,9 @@ pub struct Engine {
|
|||
last_upload: Instant,
|
||||
upload_thread: Option<std::thread::JoinHandle<()>>,
|
||||
ota: crate::ota::Updater,
|
||||
/// driver-check: the manifest's table as last read from <app data>/drivers.json (None = none published)
|
||||
drivers_table: Option<crate::drivers::Table>,
|
||||
driver_busy: bool,
|
||||
/// the interface over the air (src/uiota.rs)
|
||||
uiota: crate::uiota::UiOta,
|
||||
jobs: crate::jobrun::Jobs,
|
||||
|
|
@ -804,6 +811,8 @@ impl Engine {
|
|||
last_status: now,
|
||||
last_upload: now,
|
||||
upload_thread: None,
|
||||
drivers_table: ota.drivers_table(),
|
||||
driver_busy: false,
|
||||
ota,
|
||||
uiota,
|
||||
jobs,
|
||||
|
|
@ -975,8 +984,16 @@ impl Engine {
|
|||
}
|
||||
// proving v1 step 1: the install-time prover default, once the cards are known
|
||||
self.shared.apply_prove_default();
|
||||
// driver-check: every card's driver against the manifest's table, at first run and every detection
|
||||
self.refresh_driver_offers();
|
||||
}
|
||||
Cmd::ApplyCards(choices) => self.apply_cards(choices),
|
||||
Cmd::DriverInstall(vendor) => self.driver_install(&vendor),
|
||||
Cmd::DriverRestart => match crate::drivers::restart_now() {
|
||||
Ok(()) => self.shared.event("info", "restarting Windows in 20 s to finish the driver install (your click)"),
|
||||
Err(e) => self.shared.event("error", &format!("restart: {e}")),
|
||||
},
|
||||
Cmd::Driver(ev) => self.driver_event(ev),
|
||||
Cmd::Start => self.start(),
|
||||
Cmd::Pause => {
|
||||
if !self.running {
|
||||
|
|
@ -1511,6 +1528,95 @@ impl Engine {
|
|||
// ---- start, node ------------------------------------------------------------------------------------------
|
||||
|
||||
/// The first enumeration: the list as detected, the saved choices applied, the setup screen's notes.
|
||||
/// driver-check (7 October 2026): each card's driver_os against the manifest's table; the row's offer and the
|
||||
/// platform word. Runs at every detection and whenever the table changes; never installs anything by itself.
|
||||
fn refresh_driver_offers(&mut self) {
|
||||
let platform = crate::drivers::platform_word();
|
||||
let table = self.drivers_table.clone();
|
||||
let mut st = self.st();
|
||||
st.drivers.platform = platform.into();
|
||||
st.drivers.table_updated = table.as_ref().map(|t| t.updated.clone()).unwrap_or_default();
|
||||
st.drivers.dry_run = table.as_ref().map(|t| t.dry_run).unwrap_or(false) || std::env::var("IGNEUM_DRIVER_DRY_RUN").map(|v| v == "1").unwrap_or(false);
|
||||
if st.drivers.status.is_empty() {
|
||||
st.drivers.status = "idle".into();
|
||||
}
|
||||
for c in st.mining.cards.iter_mut() {
|
||||
c.driver_offer = crate::drivers::offer_for(&c.vendor, &c.driver_os, table.as_ref(), platform);
|
||||
}
|
||||
let offers: Vec<String> = st.mining.cards.iter().filter_map(|c| c.driver_offer.as_ref().filter(|o| o.status == "missing" || o.status == "old").map(|o| format!("{}: {}", c.name, o.text))).collect();
|
||||
drop(st);
|
||||
for o in offers {
|
||||
self.shared.log(&format!("driver check: {o}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// The row's Install click: one vendor, one install at a time; the miners keep mining through it.
|
||||
fn driver_install(&mut self, vendor: &str) {
|
||||
if self.driver_busy {
|
||||
self.shared.event("error", "a driver install is already running");
|
||||
return;
|
||||
}
|
||||
let Some(e) = self.drivers_table.as_ref().and_then(|t| t.entry(vendor)).cloned() else {
|
||||
self.shared.event("error", &format!("no driver on offer for {vendor} in the manifest's table"));
|
||||
return;
|
||||
};
|
||||
if crate::drivers::platform_word() != "windows" {
|
||||
self.shared.event("error", "the one-click driver install is for Windows; the row names the package elsewhere");
|
||||
return;
|
||||
}
|
||||
let dry = self.drivers_table.as_ref().map(|t| t.dry_run).unwrap_or(false) || std::env::var("IGNEUM_DRIVER_DRY_RUN").map(|v| v == "1").unwrap_or(false);
|
||||
self.driver_busy = true;
|
||||
{
|
||||
let mut st = self.st();
|
||||
let d = &mut st.drivers;
|
||||
d.status = "downloading".into();
|
||||
d.vendor = vendor.into();
|
||||
d.version = e.version.clone();
|
||||
d.progress = 0.0;
|
||||
d.message = format!("downloading {} {} from {}", vendor.to_ascii_uppercase(), e.version, e.url.split("//").nth(1).and_then(|r| r.split('/').next()).unwrap_or("the vendor"));
|
||||
d.error.clear();
|
||||
d.reboot_required = false;
|
||||
d.dry_run = dry;
|
||||
d.started_at = crate::platform::unix_now_f();
|
||||
d.finished_at = 0.0;
|
||||
}
|
||||
self.shared.event("info", &format!("driver install started: {} {} ({} MB) from the vendor's server{}", vendor.to_ascii_uppercase(), e.version, (e.size + 512 * 1024) / (1024 * 1024), if dry { ", dry run" } else { "" }));
|
||||
let dir = self.shared.runtime.app_dir.join("drivers");
|
||||
crate::drivers::start_install(&self.shared, e, dir, dry);
|
||||
}
|
||||
|
||||
fn driver_event(&mut self, ev: crate::drivers::Event) {
|
||||
match ev {
|
||||
crate::drivers::Event::Progress(p, text) => {
|
||||
let mut st = self.st();
|
||||
st.drivers.progress = p;
|
||||
st.drivers.message = text;
|
||||
st.drivers.status = if p < 0.6 { "downloading".into() } else if p < 0.7 { "verifying".into() } else { "installing".into() };
|
||||
}
|
||||
crate::drivers::Event::Installed(r) => {
|
||||
self.driver_busy = false;
|
||||
let (status, text, reboot, err) = match r {
|
||||
Ok((code, reboot, text)) => (if reboot { "reboot" } else if code == 0 { "done" } else { "error" }, text.clone(), reboot, if code == 0 || reboot { String::new() } else { text }),
|
||||
Err(e) => ("error", e.clone(), false, e),
|
||||
};
|
||||
{
|
||||
let mut st = self.st();
|
||||
st.drivers.status = status.into();
|
||||
st.drivers.message = text.clone();
|
||||
st.drivers.error = err.clone();
|
||||
st.drivers.reboot_required = reboot;
|
||||
st.drivers.progress = 1.0;
|
||||
st.drivers.finished_at = crate::platform::unix_now_f();
|
||||
}
|
||||
self.shared.event(if err.is_empty() { "info" } else { "error" }, &format!("driver install: {text}"));
|
||||
// the rows re-read their versions at the next detection (a minute at most); ask for one now
|
||||
if err.is_empty() {
|
||||
self.shared.send(Cmd::Detect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn first_detection(&mut self, d: crate::detect::Detection) {
|
||||
self.detected = true;
|
||||
let crate::detect::Detection { cards, notes, dropped, .. } = d;
|
||||
|
|
@ -3487,6 +3593,10 @@ impl Engine {
|
|||
self.shared.log(&format!("consensus override changed ({}); the node restarts with it at a safe moment", p.display()));
|
||||
self.node_override_restart = true;
|
||||
}
|
||||
if self.ota.take_drivers_change() {
|
||||
self.drivers_table = self.ota.drivers_table();
|
||||
self.refresh_driver_offers();
|
||||
}
|
||||
if let Some(p) = self.ota.take_tuning_change() {
|
||||
// no restart: a worker started before this reads the file at its next prepare only if it was started with
|
||||
// the path, so a miner without it is restarted at the next hour boundary by the usual path (exit 42 or
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ mod merge;
|
|||
mod ladder;
|
||||
mod chainfacts;
|
||||
mod card;
|
||||
mod drivertable;
|
||||
mod drivers;
|
||||
|
||||
use std::io::{BufRead, Write};
|
||||
use std::sync::mpsc::channel;
|
||||
|
|
|
|||
|
|
@ -69,6 +69,9 @@ pub struct Manifest {
|
|||
pub tuning: Option<serde_json::Value>,
|
||||
/// ui: the interface channel (src/uiota.rs); None = no over-the-air interface is published
|
||||
pub ui: Option<UiEntry>,
|
||||
/// drivers: the per-vendor driver table (src/drivers.rs; branch driver-check, 7 October 2026), validated here and
|
||||
/// written as is to <app data>/drivers.json; None = no table published, the rows say nothing about drivers
|
||||
pub drivers: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// One published interface bundle (the manifest's `ui` object).
|
||||
|
|
@ -206,6 +209,14 @@ pub fn parse(text: &str) -> Result<Manifest, String> {
|
|||
Some(t) if !t.is_null() => return Err("tuning must be an object with a cards object".into()),
|
||||
_ => None,
|
||||
},
|
||||
drivers: match v.get("drivers") {
|
||||
Some(d) if d.is_object() => {
|
||||
crate::drivertable::Table::parse(d)?;
|
||||
Some(d.clone())
|
||||
}
|
||||
Some(d) if !d.is_null() => return Err("drivers must be an object".into()),
|
||||
_ => None,
|
||||
},
|
||||
ui: match v.get("ui") {
|
||||
Some(u) if u.is_object() => {
|
||||
let e = UiEntry { version: s(u, "version"), sha256: s(u, "sha256").to_ascii_lowercase(), size: u.get("size").and_then(|x| x.as_u64()).unwrap_or(0), url: s(u, "url"), min_engine: s(u, "min_engine"), signature: s(u, "signature").to_ascii_lowercase() };
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ pub struct Updater {
|
|||
dir: PathBuf,
|
||||
auto: bool,
|
||||
manifest: Option<Manifest>,
|
||||
/// driver-check: the table was written or removed since the engine last looked
|
||||
drivers_changed: bool,
|
||||
entry: Option<PlatformEntry>,
|
||||
file: Option<PathBuf>,
|
||||
staged: Option<PathBuf>,
|
||||
|
|
@ -156,6 +158,7 @@ impl Updater {
|
|||
dir,
|
||||
auto,
|
||||
manifest: None,
|
||||
drivers_changed: false,
|
||||
entry: None,
|
||||
file: None,
|
||||
staged: None,
|
||||
|
|
@ -205,6 +208,7 @@ impl Updater {
|
|||
u.min_supported = m.min_supported_version.clone();
|
||||
u.write_override(shared, &m);
|
||||
u.write_tuning(shared, &m);
|
||||
u.write_drivers(shared, &m);
|
||||
}
|
||||
}
|
||||
u.settle_previous(shared);
|
||||
|
|
@ -278,6 +282,44 @@ impl Updater {
|
|||
}
|
||||
}
|
||||
|
||||
/// <app data>/drivers.json: the manifest's per-vendor driver table (src/drivers.rs), written as is; the engine
|
||||
/// re-reads it and re-evaluates every card's offer on a change. A manifest without a table removes the file.
|
||||
fn write_drivers(&mut self, shared: &Arc<Shared>, m: &Manifest) {
|
||||
let path = self.app_dir.join("drivers.json");
|
||||
match &m.drivers {
|
||||
Some(t) => {
|
||||
let text = t.to_string();
|
||||
if std::fs::read_to_string(&path).ok().as_deref() == Some(text.as_str()) {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = std::fs::write(&path, &text) {
|
||||
shared.log(&format!("could not write {}: {e}", path.display()));
|
||||
return;
|
||||
}
|
||||
let n = t.get("vendors").and_then(|c| c.as_object()).map(|c| c.len()).unwrap_or(0);
|
||||
shared.event("info", &format!("driver table from the signed manifest: {n} vendor(s), updated {}", t.get("updated").and_then(|u| u.as_str()).unwrap_or("?")));
|
||||
self.drivers_changed = true;
|
||||
}
|
||||
None => {
|
||||
if path.is_file() && std::fs::remove_file(&path).is_ok() {
|
||||
shared.log("the manifest carries no driver table any more; drivers.json removed");
|
||||
self.drivers_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The driver table changed (written or removed), once per change.
|
||||
pub fn take_drivers_change(&mut self) -> bool {
|
||||
std::mem::take(&mut self.drivers_changed)
|
||||
}
|
||||
|
||||
pub fn drivers_table(&self) -> Option<crate::drivers::Table> {
|
||||
let text = std::fs::read_to_string(self.app_dir.join("drivers.json")).ok()?;
|
||||
let v: serde_json::Value = serde_json::from_str(&text).ok()?;
|
||||
crate::drivers::Table::parse(&v).ok()
|
||||
}
|
||||
|
||||
/// The tuning file, once per change (the engine only logs it: the workers read the file at their next prepare).
|
||||
/// ui-ota: the interface entry of the last check, once (None until a check has run)
|
||||
pub fn take_ui_change(&mut self) -> Option<Option<manifest::UiEntry>> {
|
||||
|
|
@ -697,6 +739,7 @@ impl Updater {
|
|||
self.min_supported = m.min_supported_version.clone();
|
||||
self.write_override(shared, &m);
|
||||
self.write_tuning(shared, &m);
|
||||
self.write_drivers(shared, &m);
|
||||
self.ui_change = Some(m.ui.clone());
|
||||
if let Some(e) = entry {
|
||||
if changed {
|
||||
|
|
|
|||
|
|
@ -357,6 +357,18 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
|
|||
}
|
||||
"/api/prove" => shared.set_prove(body.get("on").and_then(|v| v.as_bool()).unwrap_or(false)),
|
||||
"/api/prove/setup" => crate::prover::setup(shared),
|
||||
"/api/drivers/install" => {
|
||||
let vendor = body.get("vendor").and_then(|v| v.as_str()).unwrap_or("").to_ascii_lowercase();
|
||||
if !["nvidia", "amd", "intel"].contains(&vendor.as_str()) {
|
||||
return Err("vendor must be nvidia, amd or intel".into());
|
||||
}
|
||||
shared.send(Cmd::DriverInstall(vendor));
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
"/api/drivers/restart" => {
|
||||
shared.send(Cmd::DriverRestart);
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
"/api/update/check" => {
|
||||
shared.send(Cmd::CheckUpdate);
|
||||
Ok(json!({ "ok": true }))
|
||||
|
|
|
|||
|
|
@ -144,6 +144,10 @@ pub struct CardState {
|
|||
pub mem_cap_mhz: u32, // Ember 2: the memory clock set by the tune (0 = the driver's default)
|
||||
pub amd_ordinal: i64, // the `amd N` ordinal of igneum-gpu-telemetry (-1 = unknown)
|
||||
pub driver: String, // the driver version (nvidia-smi, or the worker's race line)
|
||||
/// driver-check (7 October 2026): the comparable version the OS reports (nvidia-smi's for NVIDIA, Windows'
|
||||
/// DriverVersion for AMD and Intel; empty = none found) and what the row says about it against the manifest's table
|
||||
pub driver_os: String,
|
||||
pub driver_offer: Option<crate::drivers::Offer>,
|
||||
pub program_class: String, // the program class of the race line (loads and wide loads per hash); "" = unknown
|
||||
pub tune_control: bool, // both knobs reach the card (else measure only; sweep_note says why)
|
||||
pub tune_clock_mhz: u32, // the clock cap the last tune chose (0 = unlocked)
|
||||
|
|
@ -542,6 +546,8 @@ pub struct State {
|
|||
pub heat: HeatState,
|
||||
pub dev_fee: DevFeeState,
|
||||
pub update: UpdateState,
|
||||
/// driver-check: the one install in flight (or the last), and the table's stamp
|
||||
pub drivers: crate::drivers::DriverState,
|
||||
pub ui: UiState,
|
||||
pub jobs: JobsState,
|
||||
pub events: Vec<Event>,
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
|
||||
/* GPU rows (first run) */
|
||||
.gpu-row{display:flex;align-items:center;gap:var(--s-4);background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:16px 20px;min-width:0;flex-wrap:wrap}
|
||||
.gpu-row.off{opacity:.72}
|
||||
.gpu-row.off>.badge,.gpu-row.off>.switch,.gpu-row.off .info>:not(.driver){opacity:.72}
|
||||
.badge{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;flex:0 0 44px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)}
|
||||
.badge.apple{color:var(--bone)}
|
||||
.badge.nvidia{color:var(--nvidia)}
|
||||
|
|
@ -264,7 +264,7 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
the tune line under; the details under that */
|
||||
.gpu-list{display:flex;flex-direction:column;gap:var(--s-2)}
|
||||
.gpu-line{border:1px solid var(--line);border-radius:var(--row-r);background:var(--row);min-width:0}
|
||||
.gpu-line.off{opacity:.62}
|
||||
.gpu-line.off .gl-main,.gpu-line.off .gl-tune,.gpu-line.off .gl-details{opacity:.62}
|
||||
.gpu-line.open{border-color:var(--line-2)}
|
||||
.gl-main{display:grid;grid-template-columns:44px minmax(150px,1.2fr) auto auto;align-items:center;gap:var(--s-4);padding:12px 14px}
|
||||
.gpu-line .who{min-width:0;display:flex;flex-direction:column;gap:3px}
|
||||
|
|
@ -292,6 +292,21 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.gl-tune{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;padding:8px 14px 10px 72px;font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2);border-top:1px solid var(--line)}
|
||||
.gl-tune .t{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
||||
.gl-tune .n{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.gl-driver{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;padding:8px 14px 10px 72px;font-size:var(--t-base);color:var(--ink-2);border-top:1px solid var(--line)}
|
||||
.gl-driver .t{flex:1 1 320px;min-width:0;color:var(--bone)}
|
||||
.gl-driver .n{flex-basis:100%;font-size:var(--t-sm);line-height:1.4}
|
||||
.gl-driver.offer .t,.gl-driver.reboot .t{font-weight:600}
|
||||
.gl-driver.error .t{color:var(--ember)}
|
||||
.gl-driver.note{color:var(--ash)}
|
||||
.gl-driver.note .t{color:var(--ink-2);font-weight:400}
|
||||
.gl-driver .bar{flex-basis:100%;height:4px;border-radius:2px;background:var(--line-2);overflow:hidden;display:block}
|
||||
.gl-driver .bar b{display:block;height:100%;background:var(--ember);transition:width .4s}
|
||||
.gpu-row .driver{margin-top:6px;font-size:var(--t-base);color:var(--bone);display:flex;align-items:center;gap:var(--s-2);flex-wrap:wrap}
|
||||
.gpu-row .driver .n{flex-basis:100%;font-size:var(--t-sm);color:var(--ash);line-height:1.4}
|
||||
.gpu-row .driver.error .t{color:var(--ember)}
|
||||
.gpu-row .driver.note{color:var(--ink-2);font-weight:400}
|
||||
.gpu-row .driver .bar{flex-basis:100%;height:4px;border-radius:2px;background:var(--line-2);overflow:hidden;display:block}
|
||||
.gpu-row .driver .bar b{display:block;height:100%;background:var(--ember)}
|
||||
.gl-tune.running{color:var(--molten)}
|
||||
.gl-tune.stopped,.gl-tune.paused{color:var(--ember)}
|
||||
.gl-tune .bar{flex-basis:100%;height:3px;border-radius:2px;background:var(--line);overflow:hidden;display:block}
|
||||
|
|
|
|||
|
|
@ -326,6 +326,29 @@ var View = (function () {
|
|||
return { key: cd.key, name: cd.name, vendor: cd.vendor, kind: cd.kind, kindWord: kindWord(cd.kind), integrated: cd.kind === 'integrated', on: on, removed: removed, unusable: unusable, canToggle: cd.kind !== 'unknown' && !removed && !unusable, word: word, tone: tone, hash: hash, temp: temp, tempTone: tempTone, power: power, cap: cap, sub: sub, meta: meta.join(' · '), title: cardTitle(cd) };
|
||||
}
|
||||
|
||||
// driver-check (7 October 2026): the row's driver strip. {kind: none|fine|offer|note|running|reboot|error, text, button, sub}
|
||||
// offer = a missing or old driver with an Install button (Windows); note = the macOS, Linux or HiveOS sentence, no
|
||||
// button; running = this card's vendor is installing (the progress line); reboot = installed, Restart now;
|
||||
// error = the last install failed, Install again. The install is one at a time, so a second card of the same vendor
|
||||
// reads the same running line.
|
||||
function driverWords(cd, s) {
|
||||
var o = cd.driver_offer, d = (s && s.drivers) || {};
|
||||
if (!o) return { kind: 'none', text: '' };
|
||||
var mine = d.vendor && d.vendor === o.vendor;
|
||||
if (mine && (d.status === 'downloading' || d.status === 'verifying' || d.status === 'installing')) {
|
||||
var pct = d.status === 'downloading' ? Math.round((d.progress || 0) * 100) + '%' : '';
|
||||
return { kind: 'running', text: (d.status === 'downloading' ? 'Downloading the ' : d.status === 'verifying' ? 'Checking the ' : 'Installing the ') + vendorWord(o.vendor) + ' driver ' + d.version + (pct ? ' · ' + pct : ''), sub: d.message || '', progress: d.progress || 0, dry: !!d.dry_run };
|
||||
}
|
||||
if (mine && d.status === 'reboot') return { kind: 'reboot', text: vendorWord(o.vendor) + ' driver ' + d.version + ' installed. Restart Windows to finish.', button: 'restart', sub: 'Mining continues until you restart.' };
|
||||
if (mine && d.status === 'error') return { kind: 'error', text: 'The ' + vendorWord(o.vendor) + ' driver did not install: ' + (d.error || d.message || 'unknown error'), button: o.installable ? 'install' : '', sub: o.hash_source ? 'Checked against ' + o.hash_source : '' };
|
||||
if (o.status === 'fine') return { kind: 'fine', text: o.text };
|
||||
if (o.status === 'none') return { kind: 'note', text: o.text };
|
||||
if (!o.installable) return { kind: 'note', text: o.text };
|
||||
return { kind: 'offer', text: o.text, button: 'install', sub: 'Downloaded from ' + hostOf(o.url) + ', checked against the vendor\u2019s published hash and signature, then installed with one administrator prompt. Mining continues.' };
|
||||
}
|
||||
function vendorWord(v) { return v === 'nvidia' ? 'NVIDIA' : v === 'amd' ? 'AMD' : v === 'intel' ? 'Intel Arc' : v === 'apple' ? 'Apple' : v; }
|
||||
function hostOf(u) { var m = /^https?:\/\/([^\/]+)/.exec(u || ''); return m ? m[1] : 'the vendor'; }
|
||||
|
||||
// "tuning: step 7 of 9 · measuring 220 W" (the live draw; the plan's words when there is no draw yet)
|
||||
function tuneWord(cd) {
|
||||
var step = cd.tune_steps > 0 ? 'step ' + (cd.tune_step || 1) + ' of ' + cd.tune_steps : '';
|
||||
|
|
@ -1056,7 +1079,7 @@ var View = (function () {
|
|||
}
|
||||
function addressAsk(a) { return 'Pay ' + shortHex(a, 6, 4) + ' from the next block? Mining restarts.'; }
|
||||
function trustAsk(on) { return on ? 'Include proofs the node never checked? Devnet only. The node restarts.' : 'Check proofs again? The node restarts.'; }
|
||||
return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, ign: ign, balanceWords: balanceWords, fleetWatts: fleetWatts, fleetPounds: fleetPounds, isMine: isMine, liveNote: liveNote, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, tipAge: tipAge, nodeSource: nodeSource, finalityWords: finalityWords, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk, interfaceWords: interfaceWords, shouldReload: shouldReload, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords, REGIONS: REGIONS, CURRENCIES: CURRENCIES, RESTRICTED_SENTENCE: RESTRICTED_SENTENCE, regionOf: regionOf, currencyOf: currencyOf, setRegion: setRegion, restrictedLine: restrictedLine, priceNote: priceNote, RENT: RENT, rentLine: rentLine, heatLine: heatLine, heatRowWords: heatRowWords };
|
||||
return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, driverWords: driverWords, vendorWord: vendorWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, ign: ign, balanceWords: balanceWords, fleetWatts: fleetWatts, fleetPounds: fleetPounds, isMine: isMine, liveNote: liveNote, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, tipAge: tipAge, nodeSource: nodeSource, finalityWords: finalityWords, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk, interfaceWords: interfaceWords, shouldReload: shouldReload, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords, REGIONS: REGIONS, CURRENCIES: CURRENCIES, RESTRICTED_SENTENCE: RESTRICTED_SENTENCE, regionOf: regionOf, currencyOf: currencyOf, setRegion: setRegion, restrictedLine: restrictedLine, priceNote: priceNote, RENT: RENT, rentLine: rentLine, heatLine: heatLine, heatRowWords: heatRowWords };
|
||||
})();
|
||||
/* ---------- Ember Tune: the card row's tuning line (pure; tune-line.test.mjs loads this block) ----------
|
||||
One line per card from the card state (src/state.rs, src/ember.rs): running (the phase and the step), tuned
|
||||
|
|
@ -1317,6 +1340,14 @@ if (typeof document !== 'undefined') (function () {
|
|||
sendCards('Power cap set. It stays until you choose Default again');
|
||||
}
|
||||
});
|
||||
['cards-list', 's-cards'].forEach(function (id) {
|
||||
var el = $(id); if (!el) return;
|
||||
el.addEventListener('click', function (e) {
|
||||
var b = e.target.closest('button'); if (!b) return;
|
||||
if (b.dataset.driverInstall !== undefined) api('api/drivers/install', { vendor: b.dataset.driverInstall }).then(function (r) { toast(r && r.ok === false ? (r.error || 'could not start the driver install') : 'Driver download started. Windows asks for permission once before the install.'); });
|
||||
else if (b.dataset.driverRestart !== undefined) { if (window.confirm('Restart Windows now to finish the driver install?')) api('api/drivers/restart', {}).then(function () { toast('Restarting in 20 seconds'); }); }
|
||||
});
|
||||
});
|
||||
$('d-cards').addEventListener('click', function (e) {
|
||||
var b = e.target.closest('button'); if (!b) return;
|
||||
var row = b.closest('.gpu-line'), key = row ? row.dataset.key : '';
|
||||
|
|
@ -1326,6 +1357,8 @@ if (typeof document !== 'undefined') (function () {
|
|||
else if (b.dataset.d) { var inp = b.parentNode.querySelector('input'), v = parseInt(inp.value, 10) || 1; inp.value = Math.max(1, Math.min(64, v + parseInt(b.dataset.d, 10))); sendCards('Identities set; that card’s worker restarts'); }
|
||||
else if (b.dataset.pin !== undefined) api('api/sweep/pin', { key: key, pinned: b.dataset.pin === '1' }).then(function () { toast(b.dataset.pin === '1' ? 'Cap pinned' : 'Default: Ember chooses the cap again'); });
|
||||
else if (b.dataset.powerRetry !== undefined) api('api/power/apply', {}).then(function () { toast('Allow the administrator prompt to set the cap'); });
|
||||
else if (b.dataset.driverInstall !== undefined) api('api/drivers/install', { vendor: b.dataset.driverInstall }).then(function (r) { toast(r && r.ok === false ? (r.error || 'could not start the driver install') : 'Driver download started. Windows asks for permission once before the install. Mining continues.'); });
|
||||
else if (b.dataset.driverRestart !== undefined) { if (window.confirm('Restart Windows now to finish the driver install? Mining stops until the app is back.')) api('api/drivers/restart', {}).then(function () { toast('Restarting in 20 seconds'); }); }
|
||||
else if (b.dataset.price !== undefined) { showPage('settings'); $('s-price').focus(); }
|
||||
else if (b.closest('[data-card-goal]') && b.dataset.goal !== undefined) {
|
||||
api('api/tune/goal', { key: key, goal: b.dataset.goal || '' }).then(function (r) { if (r && r.ok) toast(b.dataset.goal ? 'Goal for this card: ' + View.GOALS[b.dataset.goal] : 'Default: this card follows the goal set for all cards'); else toast('A goal for one card needs the next Ember Tune update'); }).catch(function () { toast('A per-card goal needs the Ember Tune update'); });
|
||||
|
|
@ -1920,6 +1953,14 @@ if (typeof document !== 'undefined') (function () {
|
|||
return cd.vendor === 'nvidia' ? 'NV' : cd.vendor === 'amd' ? 'AMD' : cd.vendor === 'intel' ? 'INTEL' : 'GPU';
|
||||
}
|
||||
function vramText(cd) { if (!cd.vram_mb) return ''; var gb = cd.vram_mb / 1024; return (gb >= 10 ? Math.round(gb) : gb.toFixed(gb < 4 ? 1 : 0)) + ' GB' + (cd.kind === 'apple' ? ' unified' : ''); }
|
||||
// driver-check: the first-run row's driver line (the same words as the dashboard strip, the same buttons)
|
||||
function setupDriverHtml(cd) {
|
||||
var dw = View.driverWords(cd, state || {});
|
||||
if (dw.kind === 'none' || dw.kind === 'fine') return '';
|
||||
return '<div class="driver ' + dw.kind + '">' + (dw.kind === 'running' ? '<i class="bar"><b style="width:' + Math.round((dw.progress || 0) * 100) + '%"></b></i>' : '') + '<span class="t">' + esc(dw.text) + '</span>' +
|
||||
(dw.button === 'install' ? ' <button class="btn tiny primary" data-driver-install="' + esc(cd.driver_offer.vendor) + '">Install</button>' : dw.button === 'restart' ? ' <button class="btn tiny primary" data-driver-restart="1">Restart now</button>' : '') +
|
||||
(dw.sub ? '<div class="n">' + esc(dw.sub) + '</div>' : '') + '</div>';
|
||||
}
|
||||
// ---------- the first-run GPU rows: one per card, with a switch ----------
|
||||
function renderCardRows(container, cards) {
|
||||
container.querySelectorAll('.gpu-row').forEach(function (n) { n.remove(); });
|
||||
|
|
@ -1931,7 +1972,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
var meta = (vramText(cd) ? '<span>' + esc(vramText(cd)) + '</span>' : '') + (cd.detail && cd.kind !== 'apple' ? '<span>' + esc(cd.detail) + '</span>' : '') + (cd.detail && cd.kind === 'apple' ? '<span>' + esc(cd.detail.split(',')[0]) + '</span>' : '');
|
||||
d.innerHTML = '<div class="badge ' + esc(cd.vendor) + '">' + badgeHtml(cd) + '</div>' +
|
||||
'<div class="info"><div class="name" title="' + esc(cardTitle(cd)) + '">' + esc(cd.name) + (unusable ? ': ' + esc(cd.message || 'not usable (' + cd.problem + ')') : removed ? ': removed' : '') + ' <span class="kind ' + esc(cd.kind) + '">' + kindWord(cd.kind) + '</span></div><div class="meta">' + meta + '</div>' +
|
||||
(cd.reason ? '<div class="reason">' + esc(cd.reason) + '</div>' : '') + (cd.message && !unusable && !removed ? '<div class="msg">' + esc(cd.message) + '</div>' : '') + '</div>' +
|
||||
(cd.reason ? '<div class="reason">' + esc(cd.reason) + '</div>' : '') + (cd.message && !unusable && !removed ? '<div class="msg">' + esc(cd.message) + '</div>' : '') + setupDriverHtml(cd) + '</div>' +
|
||||
'<label class="switch" title="' + (unusable ? 'no worker can drive this card' : removed ? 'the card was removed' : cd.enabled ? 'mining on this card' : 'not mining on this card') + '"><input type="checkbox" aria-label="Mine on ' + esc(cd.name) + '"' + (cd.enabled && !removed && !unusable ? ' checked' : '') + (cd.kind === 'unknown' || unusable || removed ? ' disabled' : '') + '><span class="track"></span></label>';
|
||||
container.appendChild(d);
|
||||
});
|
||||
|
|
@ -2011,13 +2052,17 @@ if (typeof document !== 'undefined') (function () {
|
|||
var subs = [];
|
||||
if (r.sub) subs.push('<div class="msg' + (r.tone === 'bad' ? '' : ' dim') + '">' + esc(r.sub) + '</div>');
|
||||
if (cells.noReading && r.on) subs.push('<div class="msg dim">' + esc(cells.noReading) + '</div>');
|
||||
var dw = View.driverWords(cd, s);
|
||||
var drv = dw.kind === 'none' || dw.kind === 'fine' ? '' : '<div class="gl-driver ' + dw.kind + '">' + (dw.kind === 'running' ? '<i class="bar"><b style="width:' + Math.round((dw.progress || 0) * 100) + '%"></b></i>' : '') + '<span class="t">' + esc(dw.text) + '</span>' +
|
||||
(dw.button === 'install' ? '<button class="btn small primary" data-driver-install="' + esc(cd.driver_offer.vendor) + '">Install</button>' : dw.button === 'restart' ? '<button class="btn small primary" data-driver-restart="1">Restart now</button>' : '') +
|
||||
(dw.sub ? '<span class="n dim">' + esc(dw.sub) + '</span>' : '') + (dw.dry ? '<span class="n dim">dry run: the installer is not run</span>' : '') + '</div>';
|
||||
var tune = em.kind === 'none' ? '' : '<div class="gl-tune ' + em.kind + '">' + flameHtml(em.fill, em.kind) + '<span class="t">' + esc(em.label) + '</span>' + (em.sub ? '<span class="n dim">' + esc(em.sub) + '</span>' : '') + (em.savingText ? '<span class="n save">' + esc(em.savingText) + '</span>' : '') + (em.heat ? '<span class="n heat">' + esc(em.heat) + '</span>' : '') + (em.kind === 'running' ? '<i class="bar"><b style="width:' + Math.round(em.fill * 100) + '%"></b></i>' : '') + '</div>';
|
||||
return '<div class="gpu-line' + (r.on ? ' on' : ' off') + (r.removed ? ' removed' : '') + (r.unusable ? ' unusable' : '') + '" data-key="' + esc(r.key) + '">' +
|
||||
'<div class="gl-main"><div class="badge ' + esc(r.vendor) + '">' + badgeHtml({ vendor: r.vendor }) + '</div>' +
|
||||
'<div class="who"><div class="name" title="' + esc(r.title) + '">' + esc(r.name) + ' <span class="kind ' + esc(r.kind) + '">' + esc(r.kindWord) + '</span></div>' +
|
||||
'<span class="st ' + r.tone + '"><i></i>' + esc(word) + (r.meta ? ' · ' + esc(r.meta) : '') + '</span>' + subs.join('') + '</div>' +
|
||||
'<div class="nums">' + nums + '</div>' +
|
||||
'<div class="acts">' + tuneBtn + sw + chev + '</div></div>' + tune +
|
||||
'<div class="acts">' + tuneBtn + sw + chev + '</div></div>' + drv + tune +
|
||||
'<div class="gl-details" hidden>' + detailsHtml(cd, s) + '</div></div>';
|
||||
}
|
||||
// the flame mark (the brand mark's two polygons) filled to a fraction: outline when not tuned, filling while tuning, solid when tuned
|
||||
|
|
@ -2076,7 +2121,8 @@ if (typeof document !== 'undefined') (function () {
|
|||
if (tm.kind === 'tuned' || tm.kind === 'measured') h += '<div class="line"><b>' + esc(tm.text) + '</b>' + (tm.note ? ' <span class="dim">' + esc(tm.note) + '</span>' : '') + (em.savingText ? ' <span class="save">' + esc(em.savingText) + '</span>' : '') + '</div>';
|
||||
h += curveHtml(em.points);
|
||||
h += '<div class="ctl"><span class="k">Miners on this card</span><p class="help">How many miners this card runs as. Each one votes and is paid on its own. 8 suits a big card, 2 a small one, 1 a built-in GPU. Changing it restarts the card.</p><div class="ids"><button type="button" data-d="-1" aria-label="fewer identities">−</button><input type="number" min="1" max="64" value="' + (cd.identities || 1) + '" aria-label="Identities on ' + esc(cd.name) + '"><button type="button" data-d="1" aria-label="more identities">+</button></div></div>';
|
||||
var facts = [cd.worker ? 'runs on ' + cd.worker : '', cd.detail || '', cardTitle(cd), cd.driver ? 'driver ' + cd.driver : ''].filter(Boolean);
|
||||
var facts = [cd.worker ? 'runs on ' + cd.worker : '', cd.detail || '', cardTitle(cd), cd.driver_os ? 'driver ' + cd.driver_os : cd.driver ? 'driver ' + cd.driver : ''].filter(Boolean);
|
||||
if (cd.driver_offer && cd.driver_offer.status === 'fine') h += '<p class="help">' + esc(cd.driver_offer.text) + (cd.driver_offer.wanted && cd.driver_offer.wanted !== cd.driver_os ? ' The table offers ' + esc(cd.driver_offer.wanted) + '; the one installed is enough.' : '') + '</p>';
|
||||
if (facts.length) h += '<p class="note mono small">' + esc(facts.join(' · ')) + '</p>';
|
||||
return h;
|
||||
}
|
||||
|
|
@ -2103,7 +2149,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
var tunables = presentCards.filter(function (c) { return View.tunable(c) && c.enabled; });
|
||||
setHidden('btn-tune-all', !(tunables.length > 0) || !!(s.settings && s.settings.tuning_off) || !!View.tuningCard(presentCards));
|
||||
var box = $('d-cards'), busy = box.querySelector('input[type=range]:active') || (box.contains(document.activeElement) && document.activeElement.tagName === 'INPUT');
|
||||
var sig = JSON.stringify([rows, shown.map(function (c) { return [View.tuneWords(c, s, s.now), View.cells(c, price), c.identities, c.power_pct, c.pinned, c.power_applied, Math.round(c.temp_mem || 0), Math.round(c.gclk_mhz || 0), Math.round(c.mclk_mhz || 0), (c.tune_curve || []).length, c.tune_goal || '', c.sweep_watts, c.sweep_mhs, View.cardIgnDay(c, chain)]; }), goalNow()]);
|
||||
var sig = JSON.stringify([rows, s.drivers, shown.map(function (c) { return [View.tuneWords(c, s, s.now), View.cells(c, price), c.driver_offer, c.identities, c.power_pct, c.pinned, c.power_applied, Math.round(c.temp_mem || 0), Math.round(c.gclk_mhz || 0), Math.round(c.mclk_mhz || 0), (c.tune_curve || []).length, c.tune_goal || '', c.sweep_watts, c.sweep_mhs, View.cardIgnDay(c, chain)]; }), goalNow()]);
|
||||
if (sig !== mineRowsSig && !busy) {
|
||||
mineRowsSig = sig;
|
||||
box.innerHTML = rows.length ? shown.map(function (cd, i) { return gpuLineHtml(cd, s, rows[i]); }).join('') : '<div class="empty' + (s.detecting ? '' : ' err') + '">' + (s.detecting ? 'Asking the graphics cards to report in.' : 'No graphics card this app can use was found. The node runs on its own.') + '</div>';
|
||||
|
|
|
|||
|
|
@ -68,6 +68,50 @@ test('an Intel Arc card is a discrete Intel row: INTEL badge, no power reading s
|
|||
assert.equal(tm.text, 'tuning: measure only on Intel Arc once a power reading exists');
|
||||
});
|
||||
|
||||
test('driver-check: the row\'s driver strip per state (offer, running, reboot, error, note, fine)', () => {
|
||||
const offer = { vendor: 'intel', status: 'missing', installed: '', wanted: '32.0.101.9034', min_version: '32.0.101.9034', size: 932631144, url: 'https://downloadmirror.intel.com/929959/gfx_win_101.9034.exe', hash_source: 'https://www.intel.com/content/www/us/en/download/785597/', text: 'Install the Intel Arc driver 32.0.101.9034 (889 MB). No driver was found, so this card cannot mine yet.', installable: true };
|
||||
const arc = card({ key: 'intel:Intel(R) Arc(TM) B580 Graphics', name: 'Intel(R) Arc(TM) B580 Graphics', vendor: 'intel', kind: 'discrete', worker: 'OpenCL', driver_os: '', driver_offer: offer });
|
||||
const idle = { drivers: { status: 'idle', vendor: '', version: '', progress: 0, message: '', error: '', reboot_required: false, dry_run: false } };
|
||||
let w = V.driverWords(arc, idle);
|
||||
assert.equal(w.kind, 'offer');
|
||||
assert.equal(w.button, 'install');
|
||||
assert.equal(w.text, offer.text);
|
||||
assert.ok(w.sub.startsWith('Downloaded from downloadmirror.intel.com, checked against the vendor'), w.sub);
|
||||
// running: the progress line names the vendor, the version and the percent while downloading
|
||||
w = V.driverWords(arc, { drivers: { status: 'downloading', vendor: 'intel', version: '32.0.101.9034', progress: 0.42, message: 'downloading 32.0.101.9034 (889 MB) from downloadmirror.intel.com', dry_run: false } });
|
||||
assert.equal(w.kind, 'running');
|
||||
assert.equal(w.text, 'Downloading the Intel Arc driver 32.0.101.9034 · 42%');
|
||||
w = V.driverWords(arc, { drivers: { status: 'installing', vendor: 'intel', version: '32.0.101.9034', progress: 0.7, message: 'installing INTEL 32.0.101.9034: Windows asks for permission once', dry_run: true } });
|
||||
assert.equal(w.text, 'Installing the Intel Arc driver 32.0.101.9034');
|
||||
assert.equal(w.dry, true);
|
||||
// reboot: the Restart now button, mining continues until the click
|
||||
w = V.driverWords(arc, { drivers: { status: 'reboot', vendor: 'intel', version: '32.0.101.9034', reboot_required: true } });
|
||||
assert.equal(w.kind, 'reboot');
|
||||
assert.equal(w.button, 'restart');
|
||||
assert.equal(w.text, 'Intel Arc driver 32.0.101.9034 installed. Restart Windows to finish.');
|
||||
// error: the reason and Install again
|
||||
w = V.driverWords(arc, { drivers: { status: 'error', vendor: 'intel', version: '32.0.101.9034', error: 'sha256 mismatch: the file is not the one the table names' } });
|
||||
assert.equal(w.kind, 'error');
|
||||
assert.equal(w.button, 'install');
|
||||
assert.ok(w.text.startsWith('The Intel Arc driver did not install: sha256 mismatch'), w.text);
|
||||
// another vendor\'s install does not touch this row
|
||||
w = V.driverWords(arc, { drivers: { status: 'downloading', vendor: 'nvidia', version: '581.57', progress: 0.1 } });
|
||||
assert.equal(w.kind, 'offer');
|
||||
// a note (macOS, Linux, HiveOS): no button
|
||||
const mac = card({ vendor: 'apple', kind: 'apple', driver_offer: { vendor: 'apple', status: 'none', text: 'Apple silicon: no driver step, macOS carries it.', installable: false } });
|
||||
w = V.driverWords(mac, idle);
|
||||
assert.equal(w.kind, 'note');
|
||||
assert.equal(w.button, undefined);
|
||||
const linux = card({ driver_offer: { vendor: 'nvidia', status: 'missing', text: 'Linux: install the package nvidia-driver-570 (the worker needs 570.00 or newer).', installable: false } });
|
||||
assert.equal(V.driverWords(linux, idle).kind, 'note');
|
||||
// fine: nothing on the row (the details panel carries the sentence)
|
||||
const fine = card({ driver_os: '581.57', driver_offer: { vendor: 'nvidia', status: 'fine', text: 'NVIDIA driver 581.57: fine.', installable: false } });
|
||||
assert.equal(V.driverWords(fine, idle).kind, 'fine');
|
||||
// no table: nothing
|
||||
assert.equal(V.driverWords(card({ driver_offer: null }), idle).kind, 'none');
|
||||
assert.equal(V.vendorWord('amd'), 'AMD');
|
||||
});
|
||||
|
||||
test('an integrated GPU is shown as integrated and off, with its reason', () => {
|
||||
const r = V.cardRow(card({ key: 'intel:1:UHD', name: 'Intel UHD Graphics 770', vendor: 'other', kind: 'integrated', enabled: false, state: 'off', hash_now: 0, reason: 'integrated GPU: slow and shares the machine memory', power_w: 0, temp_gpu: 0 }));
|
||||
assert.equal(r.integrated, true);
|
||||
|
|
|
|||
BIN
docs/plans/driver-check-shots/01-cards-offer-light.png
Normal file
|
After Width: | Height: | Size: 421 KiB |
BIN
docs/plans/driver-check-shots/02-cards-offer-dark.png
Normal file
|
After Width: | Height: | Size: 416 KiB |
BIN
docs/plans/driver-check-shots/03-cards-running-light.png
Normal file
|
After Width: | Height: | Size: 408 KiB |
BIN
docs/plans/driver-check-shots/04-cards-running-dark.png
Normal file
|
After Width: | Height: | Size: 402 KiB |
BIN
docs/plans/driver-check-shots/05-cards-reboot-light.png
Normal file
|
After Width: | Height: | Size: 408 KiB |
BIN
docs/plans/driver-check-shots/06-cards-reboot-dark.png
Normal file
|
After Width: | Height: | Size: 402 KiB |
BIN
docs/plans/driver-check-shots/07-cards-error-light.png
Normal file
|
After Width: | Height: | Size: 423 KiB |
BIN
docs/plans/driver-check-shots/08-cards-error-dark.png
Normal file
|
After Width: | Height: | Size: 416 KiB |
BIN
docs/plans/driver-check-shots/09-mac-light.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
BIN
docs/plans/driver-check-shots/10-mac-dark.png
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
docs/plans/driver-check-shots/11-firstrun-light.png
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
docs/plans/driver-check-shots/12-firstrun-dark.png
Normal file
|
After Width: | Height: | Size: 315 KiB |
48
docs/plans/driver-check.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Driver check: the app detects a missing or old driver and installs it on one click (branch driver-check, 7 October 2026)
|
||||
|
||||
the project lead: "can we package the drivers with the miner? for all cards? and the system knows which to install if not present". The answer given: not bundled (size, vendor licences, staleness), but detected and installed on one click. Built off the release-0.3.20 app tree (082f70fb) for 0.3.20's app or the next cut; every cargo build and test on the box.
|
||||
|
||||
## 1. What is built
|
||||
|
||||
| Piece | Where |
|
||||
|---|---|
|
||||
| The per-vendor table, validated in the signer and the app alike (`Table::parse`): min_version (what the worker needs), version, the vendor's own URL, size, sha256, the page the hash was read from, the silent arguments, the exit codes that mean "restart required", the Authenticode subject word, the Linux and HiveOS package | `app/igneum-app/src/drivertable.rs` (pure, shared with `src/bin/ota-sign.rs`), `packaging/ota/drivers.json` (today's table) |
|
||||
| The table rides the manifest: `drivers` object, parsed and validated with the rest, written as is to `<app data>/drivers.json`, re-read on every change; the app never ships a driver | `src/manifest.rs` (`Manifest.drivers`), `src/ota.rs` (`write_drivers`, `take_drivers_change`, `drivers_table`), `packaging/ota/publish-manifest.sh --drivers <file> | --no-drivers` (carried over like tuning) |
|
||||
| Each card's driver version: nvidia-smi's `driver_version` for NVIDIA, Windows' `DriverVersion` (the adapter row) for AMD and Intel; `CardState.driver_os` | `src/detect.rs` (the query, the `Adapter.driver` field, `assemble`) |
|
||||
| The check at first run and every detection, and on every table change: the offer per card (`CardState.driver_offer`: status missing, old, fine or none; the row's sentence; installable on Windows only) | `src/engine.rs` (`refresh_driver_offers`, called from the `Detected` arm and `tick_update`) |
|
||||
| The click: download from the vendor's server (curl with resume into `<app data>/drivers/`), size and sha256 against the table, the Authenticode signature (status Valid, subject carries the vendor), then the installer through ONE elevated prompt (`platform::elevated_command`, the PC 1 driver job's shape), the exit code read (0 installed, a table code = restart required, else an error), "Restart now" on the row; the app never restarts by itself; nothing else pauses (the miners keep mining) | `src/drivers.rs` (`start_install`, `download`, `exit_code_of`, `restart_now`), `src/engine.rs` (`driver_install`, `driver_event`), `src/server.rs` (`POST api/drivers/install {vendor}`, `POST api/drivers/restart`) |
|
||||
| The row: "Install the NVIDIA driver 581.57 (965 MB). Driver 566.36 is older than the 570.00 the worker needs." with the Install button on the dashboard's card row and on the first-run card list; the progress line; "installed, restart Windows to finish" with Restart now; the error with Install again; macOS "no driver step"; Linux and HiveOS the package line, no button | `ui/app.js` (`View.driverWords`, the `gl-driver` strip, `setupDriverHtml`, the click handlers), `ui/app.css` |
|
||||
| Dry run: `IGNEUM_DRIVER_DRY_RUN=1` or `dry_run: true` in the table: the download and every check run, the installer does not (the row says so) | `src/drivers.rs`, `src/engine.rs` |
|
||||
| Tests: version compare, the table's parse and refusals, the offers per tier (missing, old, fine, Apple, Linux, HiveOS), exit codes and Authenticode verdicts, the download against a mocked vendor server (a std TcpListener on 127.0.0.1: the right sha256 passes, a wrong one is refused with the part file gone, the file in place is kept), the manifest's `drivers` validation; the UI's strip per state | `src/drivertable.rs` tests, `src/drivers.rs` `download_tests`, `ui/view.test.mjs` |
|
||||
| The mock's scenarios for the captures: `drivers`, `drivers-running`, `drivers-reboot`, `drivers-error`, `drivers-mac` | `tools/ui-mock/server.mjs` |
|
||||
|
||||
The captures (the installed Mac window's `--snapshot` on the mock, 1280 x 900, light and dark): `docs/plans/driver-check-shots/01` to `12`: the dashboard's Cards page with the offers (01, 02), the download running (03, 04), installed and Restart now (05, 06), the error (07, 08), macOS (09, 10), the first-run card list (11, 12).
|
||||
|
||||
## 2. The table today (`packaging/ota/drivers.json`)
|
||||
|
||||
| Vendor | min_version | On offer | Source | State |
|
||||
|---|---|---|---|---|
|
||||
| Intel | 32.0.101.9034 (the Arc driver the worker's rotate rewrite is proven on) | 32.0.101.9034, 932,631,144 bytes, sha256 72ba7eea... | downloadmirror.intel.com/929959, Intel's page 785597 (SHA512 printed there; the sha256 read on PC 1 on 7 October) | measured, live |
|
||||
| NVIDIA | 570.00 (CUDA 12.8) | 581.57, approximate size, sha256 of zeros | us.download.nvidia.com, the driver page prints the sha256 | placeholder: the app refuses to install it (the download's sha256 never matches) until the hash is read from the page and written here |
|
||||
| AMD | 32.0.32000.0 (Adrenalin 25.x) | 26.9.2, approximate size, sha256 of zeros | drivers.amd.com, the download page prints the sha256 | placeholder, the same |
|
||||
|
||||
Apple: none. The exit codes: Intel 14 and 1014 (the bootstrapper's wrap, measured on PC 1), NVIDIA 1 (approximate: NVIDIA's installer returns 1 for "restart required" in silent mode), AMD 3010 (the MSI code, approximate). A wrong code reads as an error on the row and the install is marked failed, never as a success; the table fixes it without an app cut.
|
||||
|
||||
## 3. Consequences per tier
|
||||
|
||||
| Tier | What the user sees |
|
||||
|---|---|
|
||||
| A home miner with a new card and no driver (Windows) | the first-run card list says "Install the <vendor> driver <version> (<size>). No driver was found, so this card cannot mine yet." with the button; one click, one administrator prompt, the restart line if the installer asks; the other cards mine through it |
|
||||
| An old driver (Windows) | the same row with "Driver X is older than the Y the worker needs"; mining on that card may already fail (the worker's self-test) and the row says why |
|
||||
| macOS | "Apple silicon: no driver step, macOS carries it." in the details; no button |
|
||||
| Linux, HiveOS | "Linux: install the package nvidia-driver-570 (the worker needs 570.00 or newer)." or the HiveOS line; no installer |
|
||||
| A rig | one install at a time (the engine refuses a second while one runs); each card's row carries its own offer |
|
||||
| A pool user | nothing changes |
|
||||
|
||||
Sizes: the installers are 870 MB to 1 GB each; the download lands in `<app data>/drivers/` and stays (a second click reuses it when its sha256 still matches). Time: the download at a home line of 50 Mbit/s is 2.5 to 3 minutes; the install 1 to 4 minutes; the prompt is the only wait on the user.
|
||||
|
||||
## 4. Not done, and what follows
|
||||
|
||||
- The NVIDIA and AMD rows' hashes: read from the vendors' pages and written into `drivers.json`, then published with `publish-manifest.sh --drivers packaging/ota/drivers.json`. Until then those two rows are refused by the app's own check.
|
||||
- The Linux install path stays a line (the brief).
|
||||
- A PC 1 dry run of the Windows path (the installer not run, the download and the checks live) is one `IGNEUM_DRIVER_DRY_RUN=1` engine start away; not run today (PC 1 is the project lead's desk and mines).
|
||||
60
packaging/ota/drivers.json
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"updated": "2026-10-07",
|
||||
"note": "The per-vendor driver table (branch driver-check). The app installs a driver on one click from the vendor's own server, checked against the sha256 and the Authenticode signature; it never ships one. The Intel row is measured (PC 1, 7 October 2026: the file's sha256 after the SHA512 Intel prints matched). The NVIDIA and AMD rows carry the release version read from the vendor's page on 7 October 2026 with their sizes approximate and a placeholder sha256 of zeros, which the app refuses to install (the download's sha256 never matches): they go live when the file's sha256 is read from the vendor's page and written here. A row's min_version is what the worker needs (NVIDIA: CUDA 12.8 = driver 570 or newer; AMD: Adrenalin 25.x = 32.0.32000 or newer; Intel: the Arc driver with the rotate fold the worker rewrites, 32.0.101.9034 or newer).",
|
||||
"vendors": {
|
||||
"nvidia": {
|
||||
"min_version": "570.00",
|
||||
"version": "581.57",
|
||||
"url": "https://us.download.nvidia.com/Windows/581.57/581.57-desktop-win10-win11-64bit-international-dch-whql.exe",
|
||||
"size": 1012345678,
|
||||
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"hash_source": "https://www.nvidia.com/en-us/drivers/ (the download page prints the sha256 under the file; approximate until read)",
|
||||
"args": [
|
||||
"-s",
|
||||
"-noreboot"
|
||||
],
|
||||
"reboot_codes": [
|
||||
1
|
||||
],
|
||||
"signer": "NVIDIA",
|
||||
"linux_package": "nvidia-driver-570",
|
||||
"hive_package": "nvidia-driver-update 570"
|
||||
},
|
||||
"amd": {
|
||||
"min_version": "32.0.32000.0",
|
||||
"version": "26.9.2",
|
||||
"url": "https://drivers.amd.com/drivers/whql-amd-software-adrenalin-edition-26.9.2-win10-win11-sep2026.exe",
|
||||
"size": 912345678,
|
||||
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"hash_source": "https://www.amd.com/en/support/download/drivers.html (the download page prints the sha256; approximate until read)",
|
||||
"args": [
|
||||
"-install",
|
||||
"-silent"
|
||||
],
|
||||
"reboot_codes": [
|
||||
3010
|
||||
],
|
||||
"signer": "Advanced Micro Devices",
|
||||
"linux_package": "amdgpu-install --usecase=opencl",
|
||||
"hive_package": "amd-ocl-install 26.9"
|
||||
},
|
||||
"intel": {
|
||||
"min_version": "32.0.101.9034",
|
||||
"version": "32.0.101.9034",
|
||||
"url": "https://downloadmirror.intel.com/929959/gfx_win_101.9034.exe",
|
||||
"size": 932631144,
|
||||
"sha256": "72ba7eea08a0cc18650603976ff9433dfdf84d23d4cbbee662a4df9f2856ae98",
|
||||
"hash_source": "https://www.intel.com/content/www/us/en/download/785597/ (SHA512 3fc6f9b4...3cf3f0 on the page; the sha256 is of the same file, read on PC 1 on 7 October 2026)",
|
||||
"args": [
|
||||
"-s"
|
||||
],
|
||||
"reboot_codes": [
|
||||
14,
|
||||
1014
|
||||
],
|
||||
"signer": "Intel",
|
||||
"linux_package": "intel-opencl-icd",
|
||||
"hive_package": "intel-opencl-icd (apt)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,9 @@
|
|||
# [--public] also publish into dl/public/ (no token: the site's download
|
||||
# links, packaging/ota/publish-public.sh --app, plus --wallet
|
||||
# when the wallet manifest is in the folder); the same deploy
|
||||
# [--drivers drivers.json | --no-drivers] the per-vendor driver table (packaging/ota/drivers.json; branch
|
||||
# driver-check, 7 October 2026: the app installs a driver on one click
|
||||
# from the vendor's own server, never ships one); carried over
|
||||
# [--tuning tuning.json | --no-tuning] the fleet's per-card kernel tuning (tools/tuning.mjs writes it;
|
||||
# docs/design/miner-tuning.md); carried over from the current
|
||||
# manifest when not given, as is consensus.override
|
||||
|
|
@ -45,7 +48,7 @@ TOKEN_FILE="$HOME/.config/igneum/dl-token"
|
|||
SIGNER="$ROOT/app/igneum-app/target/release/igneum-ota-sign"
|
||||
|
||||
VERSION="" MAC="" WIN="" NOTES="" ACTIVATION="" DEADLINE="" MIN_SUPPORTED="" CHANNEL="devnet" BASE="" DEST="" DEPLOY=0 VERIFY_ONLY=0 TRIES=12
|
||||
OVERRIDE="" TUNING_FILE="" NO_TUNING=0 PUBLIC=0 UI_FILE="" NO_UI=0
|
||||
OVERRIDE="" TUNING_FILE="" NO_TUNING=0 PUBLIC=0 UI_FILE="" NO_UI=0 DRIVERS_FILE="" NO_DRIVERS=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--version) VERSION="$2"; shift 2 ;;
|
||||
|
|
@ -61,6 +64,8 @@ while [ $# -gt 0 ]; do
|
|||
--channel) CHANNEL="$2"; shift 2 ;;
|
||||
--tuning) TUNING_FILE="$2"; shift 2 ;;
|
||||
--no-tuning) NO_TUNING=1; shift ;;
|
||||
--drivers) DRIVERS_FILE="$2"; shift 2 ;;
|
||||
--no-drivers) NO_DRIVERS=1; shift ;;
|
||||
--ui) UI_FILE="$2"; shift 2 ;;
|
||||
--no-ui) NO_UI=1; shift ;;
|
||||
--base-url) BASE="$2"; shift 2 ;;
|
||||
|
|
@ -199,6 +204,17 @@ elif [ "$NO_TUNING" = 0 ] && [ -f "$OLD" ]; then
|
|||
[ -n "$TUNING" ] && echo "tuning: carried over from the current manifest ($(python3 -c 'import json,sys; print(len(json.loads(sys.argv[1])["cards"]))' "$TUNING") card model(s))"
|
||||
fi
|
||||
|
||||
# drivers: the per-vendor driver table (packaging/ota/drivers.json), given here, else carried over; --no-drivers withdraws it.
|
||||
# The signer validates it the way the app does (igneum-ota-sign shares src/drivertable.rs), so a bad row never publishes.
|
||||
DRIVERS=""
|
||||
if [ -n "$DRIVERS_FILE" ]; then
|
||||
[ -f "$DRIVERS_FILE" ] || { echo "missing: $DRIVERS_FILE" >&2; exit 1; }
|
||||
DRIVERS="$(python3 -c 'import json,sys; t=json.load(open(sys.argv[1])); assert isinstance(t.get("vendors"), dict) and t["vendors"], "drivers needs a vendors object"; print(json.dumps(t, sort_keys=True, separators=(",",":")))' "$DRIVERS_FILE")"
|
||||
elif [ "$NO_DRIVERS" = 0 ] && [ -f "$OLD" ]; then
|
||||
DRIVERS="$(python3 -c 'import json,sys; t=json.load(open(sys.argv[1])).get("drivers"); print(json.dumps(t, sort_keys=True, separators=(",",":")) if isinstance(t, dict) and isinstance(t.get("vendors"), dict) else "")' "$OLD" 2>/dev/null || true)"
|
||||
[ -n "$DRIVERS" ] && echo "drivers: carried over from the current manifest ($(python3 -c 'import json,sys; print(len(json.loads(sys.argv[1])["vendors"]))' "$DRIVERS") vendor(s))"
|
||||
fi
|
||||
|
||||
# ui: the interface channel, given here (ui.json from tools/ui-ota/publish.mjs, its own signature checked with the
|
||||
# signer and its bundle present in the folder), else carried over; --no-ui withdraws it
|
||||
UI=""
|
||||
|
|
@ -239,9 +255,9 @@ if [ -n "$ACTIVATION" ]; then
|
|||
echo "note: the live DAA could not be read from /api/live; the activation height $ACTIVATION is not checked against it" >&2
|
||||
fi
|
||||
fi
|
||||
python3 - "$NEW" "$VERSION" "$CHANNEL" "$NOTES" "$MIN_SUPPORTED" "$ACTIVATION" "$DEADLINE" "$MAC_ENTRY" "$WIN_ENTRY" "${OVERRIDE:-}" "${TUNING:-}" "${UI:-}" <<'PY'
|
||||
python3 - "$NEW" "$VERSION" "$CHANNEL" "$NOTES" "$MIN_SUPPORTED" "$ACTIVATION" "$DEADLINE" "$MAC_ENTRY" "$WIN_ENTRY" "${OVERRIDE:-}" "${TUNING:-}" "${UI:-}" "${DRIVERS:-}" <<'PY'
|
||||
import json, sys, datetime
|
||||
out, version, channel, notes, min_supported, activation, deadline, mac, win, override, tuning, ui = sys.argv[1:13]
|
||||
out, version, channel, notes, min_supported, activation, deadline, mac, win, override, tuning, ui, drivers = sys.argv[1:14]
|
||||
override = json.loads(override) if override else None
|
||||
if override is not None and (not isinstance(override, dict) or not override): raise SystemExit("--override must be a non-empty JSON object")
|
||||
def entry(s):
|
||||
|
|
@ -261,6 +277,8 @@ if tuning:
|
|||
m["tuning"] = json.loads(tuning)
|
||||
if ui:
|
||||
m["ui"] = json.loads(ui)
|
||||
if drivers:
|
||||
m["drivers"] = json.loads(drivers)
|
||||
open(out, "w").write(json.dumps(m, sort_keys=True, separators=(",", ":"), ensure_ascii=False))
|
||||
PY
|
||||
"$SIGNER" sign "$KEY" "$NEW" > "$NEW.sig"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@
|
|||
// firstwait miner-ui-5: mining, no block yet (the Poisson count-up), the ladder's first rung
|
||||
// firstblock miner-ui-5: the first block a minute ago (the block card), the key under the dust line
|
||||
// ladder miner-ui-5: every rung walked (vote, signature, full window, rank 3, streak, a paid shard)
|
||||
// drivers driver-check: the 5090 fine, the 9070 XT and the Arc with an Install offer (also drivers-running,
|
||||
// drivers-reboot, drivers-error, drivers-mac)
|
||||
// GET /api/ladder answers the chain facts in src/chainfacts.rs's shape (the devnet's params, this key ranked);
|
||||
// POST /api/card accepts the block card's PNG and answers a path.
|
||||
// heat Ember Heat (mission item 7): a 5090 and a 4070 in heat mode holding 20 °C (heating, 60% duty), the
|
||||
|
|
@ -206,6 +208,23 @@ function stateNow() {
|
|||
{ ...recState.mining.cards[0], key: 'intel:Intel(R) Arc(TM) B580 Graphics', name: 'Intel(R) Arc(TM) B580 Graphics', code: 'Intel(R) Arc(TM) B580 Graphics', platform: 'Intel(R) OpenCL Graphics (OpenCL 3.0 ), driver 32.0.101.9034', kind: 'discrete', vendor: 'intel', worker: 'OpenCL', detail: '160 compute units', vram_mb: 12208, device: '1', bus: '05:00.0', identities: 8, ids: ['c0ffee01', 'c0ffee02'], enabled: true, state: 'mining', hash_now: 15.2, hash_avg: 15.0, accepted: 3, rejected: 0, power_default_w: 0, power_pct: 0, power_applied: false, power_w: 0, temp_gpu: 0, temp_mem: 0, eff_mhw: 0, sweep_supported: false, sweep_state: 'unsupported', sweep_note: 'not available: Intel Arc exposes no power cap or reading through OpenCL (the driver\'s IGCL counters come next)', tune_line: '', tune_source: '', tune_control: false, driver: '32.0.101.9034' }];
|
||||
s.mining.hash_total = 142.9; s.display_name = 'pc-1';
|
||||
}
|
||||
if (scenario === 'drivers' || scenario === 'drivers-running' || scenario === 'drivers-reboot' || scenario === 'drivers-error' || scenario === 'drivers-mac') {
|
||||
// driver-check (7 October 2026): PC 1 after the Arc went in, with the manifest's table: the 5090 fine, the
|
||||
// 9070 XT's driver older than the worker needs, the Arc on Windows' inbox driver, the install's states
|
||||
var tbl = 'https://www.intel.com/content/www/us/en/download/785597/';
|
||||
var nvOffer = { vendor: 'nvidia', status: 'fine', installed: '581.57', wanted: '581.57', min_version: '570.00', size: 1012345678, url: 'https://us.download.nvidia.com/Windows/581.57/581.57-desktop-win10-win11-64bit-international-dch-whql.exe', hash_source: 'https://www.nvidia.com/en-us/drivers/', text: 'NVIDIA driver 581.57: fine.', installable: false };
|
||||
var amdOffer = { vendor: 'amd', status: 'old', installed: '32.0.21042.62', wanted: '26.9.2', min_version: '32.0.32000.0', size: 912345678, url: 'https://drivers.amd.com/drivers/whql-amd-software-adrenalin-edition-26.9.2-win10-win11-sep2026.exe', hash_source: 'https://www.amd.com/en/support/download/drivers.html', text: 'Install the AMD driver 26.9.2 (870 MB). Driver 32.0.21042.62 is older than the 32.0.32000.0 the worker needs.', installable: true };
|
||||
var arcOffer = { vendor: 'intel', status: 'old', installed: '32.0.101.6733', wanted: '32.0.101.9034', min_version: '32.0.101.9034', size: 932631144, url: 'https://downloadmirror.intel.com/929959/gfx_win_101.9034.exe', hash_source: tbl, text: 'Install the Intel Arc driver 32.0.101.9034 (889 MB). Driver 32.0.101.6733 is older than the 32.0.101.9034 the worker needs.', installable: true };
|
||||
s.mining.cards = [{ ...recState.mining.cards[0], key: 'nvidia:NVIDIA GeForce RTX 5090', name: 'NVIDIA GeForce RTX 5090', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '21760 cores', vram_mb: 32768, device: '0', identities: 2, ids: ['ea53bd41', '7b8ef6fd'], power_default_w: 575, power_pct: 80, power_applied: true, power_limit_w: 460, power_w: 291, temp_gpu: 63, temp_mem: 72, hash_now: 122.4, hash_avg: 121.1, accepted: 102, driver: '581.57', driver_os: '581.57', driver_offer: nvOffer, sweep_supported: true, sweep_state: 'idle', sweep_note: '' },
|
||||
{ ...recState.mining.cards[0], key: 'amd:gfx1201', name: 'AMD Radeon RX 9070 XT', code: 'gfx1201', kind: 'discrete', vendor: 'amd', worker: 'OpenCL', detail: '32 compute units', vram_mb: 16304, device: '1', identities: 2, ids: ['a405c3e6', 'e15fe94b'], enabled: true, state: 'mining', hash_now: 18.9, hash_avg: 17.8, accepted: 20, power_w: 198, temp_gpu: 61, driver: '3683.0 (PAL,LC)', driver_os: '32.0.21042.62', driver_offer: amdOffer, sweep_supported: true, sweep_state: 'idle', sweep_note: '' },
|
||||
{ ...recState.mining.cards[0], key: 'intel:Intel(R) Arc(TM) B580 Graphics', name: 'Intel(R) Arc(TM) B580 Graphics', code: 'Intel(R) Arc(TM) B580 Graphics', kind: 'discrete', vendor: 'intel', worker: 'OpenCL', detail: '160 compute units', vram_mb: 11930, device: '2', identities: 8, ids: [], enabled: false, state: 'off', hash_now: 0, hash_avg: 0, accepted: 0, power_w: 0, temp_gpu: 0, reason: 'the driver is older than the worker needs', driver: '', driver_os: '32.0.101.6733', driver_offer: arcOffer, sweep_supported: false, sweep_state: 'unsupported', sweep_note: 'not available: Intel Arc exposes no power cap or reading through OpenCL (the driver\'s IGCL counters come next)' }];
|
||||
s.mining.hash_total = 141.3; s.display_name = 'pc-1';
|
||||
s.drivers = { status: 'idle', vendor: '', version: '', progress: 0, message: '', error: '', reboot_required: false, dry_run: false, started_at: 0, finished_at: 0, table_updated: '2026-10-07', platform: 'windows' };
|
||||
if (scenario === 'drivers-running') s.drivers = { ...s.drivers, status: 'downloading', vendor: 'intel', version: '32.0.101.9034', progress: 0.43 + ((Date.now() / 1000) % 60) / 110, message: 'downloading 32.0.101.9034 (889 MB) from downloadmirror.intel.com', started_at: t - 40 };
|
||||
if (scenario === 'drivers-reboot') s.drivers = { ...s.drivers, status: 'reboot', vendor: 'intel', version: '32.0.101.9034', progress: 1, message: 'INTEL driver 32.0.101.9034 installed. Restart Windows to finish.', reboot_required: true, started_at: t - 300, finished_at: t - 20 };
|
||||
if (scenario === 'drivers-error') s.drivers = { ...s.drivers, status: 'error', vendor: 'amd', version: '26.9.2', progress: 1, message: 'download: sha256 mismatch: the file is not the one the table names (page https://www.amd.com/en/support/download/drivers.html)', error: 'download: sha256 mismatch: the file is not the one the table names (page https://www.amd.com/en/support/download/drivers.html)', started_at: t - 120, finished_at: t - 5 };
|
||||
if (scenario === 'drivers-mac') { s.mining.cards = [{ ...recState.mining.cards[0], driver_offer: { vendor: 'apple', status: 'none', installed: '', wanted: '', min_version: '', size: 0, url: '', hash_source: '', text: 'Apple silicon: no driver step, macOS carries it.', installable: false } }]; s.mining.hash_total = recState.mining.hash_total; s.display_name = recState.display_name; s.drivers.platform = 'macos'; }
|
||||
}
|
||||
if (scenario === 'nvidia') {
|
||||
s.mining.cards = [{ ...recState.mining.cards[0], key: 'nvidia:0:NVIDIA GeForce RTX 5090', name: 'NVIDIA GeForce RTX 5090', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '21760 cores', vram_mb: 32768, device: '0', identities: 8, ids: ['8fafda27', '1b2c3d4e', '5f6a7b8c'], power_default_w: 575, power_pct: 80, power_applied: true, power_limit_w: 460, power_w: 441, temp_gpu: 71, temp_mem: 92, telemetry_at: t, hash_now: 412.5, hash_avg: 405.2 },
|
||||
{ ...recState.mining.cards[0], key: 'nvidia:1:NVIDIA GeForce RTX 4070', name: 'NVIDIA GeForce RTX 4070', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '5888 cores', vram_mb: 12288, device: '1', identities: 2, ids: ['9a8b7c6d'], power_default_w: 200, power_pct: 80, power_applied: false, power_note: 'nvidia-smi: wanted 160 W, card reports 200 W', power_limit_w: 200, power_w: 188, temp_gpu: 64, temp_mem: 0, telemetry_at: t, hash_now: 98.1, hash_avg: 97.4, state: 'restarting', restart_in_s: 4 },
|
||||
|
|
|
|||