Merge driver-check f03b0e74 into release-0.3.22 (main: a 0.3.22 app item; the eGPU driver-install hold and warning)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 17:32:40 +00:00
commit 11aeaf1d68
10 changed files with 119 additions and 4 deletions

View file

@ -244,9 +244,36 @@ pub fn authenticode_verdict(text: &str, signer: &str) -> Result<String, String>
Ok(subject)
}
/// The cards whose worker stops before a vendor's driver installer starts: that vendor's cards the app reads as
/// external (the eGPU kind: a USB4 or Thunderbolt router in the device's parent chain). PC 2, 7 October 2026, 16:26Z:
/// the Intel installer's display reset took the machine down while the app's own worker mined on the Arc in the Razer
/// Core X V2; the machine needed a power cycle. The other cards keep mining through the install. Each item is
/// (key, vendor, kind, enabled); a card already off has nothing to stop.
pub fn install_hold_keys<'a>(cards: impl IntoIterator<Item = (&'a str, &'a str, &'a str, bool)>, vendor: &str) -> Vec<String> {
cards.into_iter().filter(|(_, v, kind, enabled)| *v == vendor && *kind == "external" && *enabled).map(|(key, _, _, _)| key.to_string()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_install_on_an_egpu_card_stops_that_cards_worker_first_and_leaves_the_other_cards_mining() {
// PC 2, 7 October 2026: the Arc B580 in the Razer Core X V2 was mining when the Intel installer's display
// reset took the machine down. The rule: the vendor's external cards stop, nothing else does.
let cards = [
("nvidia:0:NVIDIA GeForce RTX 5090", "nvidia", "discrete", true),
("nvidia:1:NVIDIA GeForce RTX 5060 Ti", "nvidia", "external", true),
("intel:Intel(R) Arc(TM) B580 Graphics", "intel", "external", true),
("intel::Intel UHD 770", "other", "integrated", false),
];
assert_eq!(install_hold_keys(cards, "intel"), vec!["intel:Intel(R) Arc(TM) B580 Graphics".to_string()], "the Arc in the enclosure stops; the NVIDIA cards and the iGPU keep mining");
assert_eq!(install_hold_keys(cards, "nvidia"), vec!["nvidia:1:NVIDIA GeForce RTX 5060 Ti".to_string()], "the 5060 Ti in the enclosure stops; the 5090 inside the case keeps mining");
assert!(install_hold_keys(cards, "amd").is_empty(), "no AMD card: nothing stops");
// an external card already off has nothing to stop; a discrete card of the vendor is not held
let off = [("intel:Intel(R) Arc(TM) B580 Graphics", "intel", "external", false), ("intel:Intel(R) Arc(TM) A770 Graphics", "intel", "discrete", true)];
assert!(install_hold_keys(off, "intel").is_empty());
}
const TABLE: &str = r#"{"updated":"2026-10-07","vendors":{
"nvidia":{"min_version":"570.00","version":"617.42","url":"https://us.download.nvidia.com/Windows/617.42/617.42-desktop-win10-win11-64bit-international-dch-whql.exe","size":990853168,"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"},

View file

@ -700,6 +700,8 @@ pub struct Engine {
/// 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 vendor's cards held off for a driver install, put back as they were when it ends
driver_held: Vec<CardChoice>,
/// the interface over the air (src/uiota.rs)
uiota: crate::uiota::UiOta,
jobs: crate::jobrun::Jobs,
@ -869,6 +871,7 @@ impl Engine {
upload_thread: None,
drivers_table: ota.drivers_table(),
driver_busy: false,
driver_held: Vec::new(),
ota,
uiota,
jobs,
@ -1066,6 +1069,8 @@ impl Engine {
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();
// a card the driver install took away is back: its worker returns
self.driver_release_held(true);
}
Cmd::ApplyCards(choices) => self.apply_cards(choices),
Cmd::ProveHold(on) => self.prove_hold(on),
@ -1711,10 +1716,47 @@ impl Engine {
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 { "" }));
// PC 2, 7 October 2026, 16:26Z: the Intel installer's display reset took the machine down while the app's own
// worker mined on the Arc in the Razer Core X V2 (a power cycle). The vendor's cards the app reads as external
// (drivertable::install_hold_keys) stop before the installer starts; the other cards keep mining; each held card
// comes back as it was when the installer ends, or when the card itself returns if the install took it away.
let (off, restore): (Vec<CardChoice>, Vec<CardChoice>) = {
let st = self.st();
let keys = crate::drivertable::install_hold_keys(st.mining.cards.iter().map(|c| (c.key.as_str(), c.vendor.as_str(), c.kind.as_str(), c.enabled)), vendor);
let live: Vec<crate::jobrun::LiveCard> = st.mining.cards.iter().map(|c| (c.key.clone(), c.enabled, c.identities, c.power_pct, c.present())).collect();
crate::jobrun::cards_off_choices(&keys, &live)
};
if !off.is_empty() {
self.shared.event("info", &format!("driver install: the worker on {} stops for the install (a card in an external enclosure; the display reset can take the machine for a minute); the other cards keep mining", off.iter().map(|c| c.key.as_str()).collect::<Vec<_>>().join(", ")));
self.apply_cards(off);
}
self.driver_held = restore;
let dir = self.shared.runtime.app_dir.join("drivers");
crate::drivers::start_install(&self.shared, e, dir, dry);
}
/// The cards held off for a driver install go back as they were: the ones present now, at once; a card the install
/// took away (Windows re-enumerates an eGPU through the reset) waits for its return (`returned` = the call from a
/// detection) and goes back then. A hold that nothing returns stays until the next install replaces it.
fn driver_release_held(&mut self, returned: bool) {
if self.driver_held.is_empty() || (returned && self.driver_busy) {
return;
}
let held = std::mem::take(&mut self.driver_held);
let (now, later): (Vec<CardChoice>, Vec<CardChoice>) = {
let st = self.st();
held.into_iter().partition(|ch| st.mining.cards.iter().any(|c| c.key == ch.key && c.present()))
};
if !now.is_empty() {
self.shared.event("info", &format!("driver install: the worker on {} is back{}", now.iter().map(|c| c.key.as_str()).collect::<Vec<_>>().join(", "), if returned { " (the card returned)" } else { "" }));
self.apply_cards(now);
}
if !later.is_empty() && !returned {
self.shared.event("info", &format!("driver install: {} not listed after the install; its worker comes back when the card does", later.iter().map(|c| c.key.as_str()).collect::<Vec<_>>().join(", ")));
}
self.driver_held = later;
}
fn driver_event(&mut self, ev: crate::drivers::Event) {
match ev {
crate::drivers::Event::Progress(p, text) => {
@ -1725,6 +1767,7 @@ impl Engine {
}
crate::drivers::Event::Installed(r) => {
self.driver_busy = false;
self.driver_release_held(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),

View file

@ -412,17 +412,22 @@ var View = (function () {
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;
var mine = d.vendor && d.vendor === o.vendor, ext = cd.kind === 'external';
// PC 2, 7 October 2026: a card in an external enclosure (the eGPU kind) has its worker stopped for the install, and the
// installer's display reset can take the machine; the row says both before the click and while it runs
var extRun = ext ? '. This card\u2019s worker is stopped until the installer ends; the display reset can take the machine for a minute.' : '';
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 };
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 || '') + extRun, progress: d.progress || 0, dry: !!d.dry_run, external: ext };
}
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.' };
var sub = 'Downloaded from ' + hostOf(o.url) + ', checked against the vendor\u2019s published hash and signature, then installed with one administrator prompt. ';
sub += ext ? 'This card\u2019s worker stops for the install and comes back after it; the other cards keep mining. The display reset can take the machine for a minute and may need a restart: save your work first.' : 'Mining continues.';
return { kind: 'offer', text: o.text, button: 'install', sub: sub, external: ext };
}
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'; }

View file

@ -112,6 +112,27 @@ test('driver-check: the row\'s driver strip per state (offer, running, reboot, e
assert.equal(V.vendorWord('amd'), 'AMD');
});
test('driver-check: an install offer on an eGPU card warns that its worker stops and the display reset can take the machine (PC 2, 7 October 2026)', () => {
const offer = { 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: 'https://www.intel.com/content/www/us/en/download/785597/', text: 'Install the Intel Arc driver 32.0.101.9034, 889 MB (the worker needs 32.0.101.9034 or newer; 32.0.101.6733 is installed)', installable: true };
const idle = { drivers: { status: 'idle', vendor: '', version: '', progress: 0, message: '', error: '', reboot_required: false, dry_run: false } };
const egpu = card({ key: 'intel:Intel(R) Arc(TM) B580 Graphics', name: 'Intel(R) Arc(TM) B580 Graphics', vendor: 'intel', kind: 'external', worker: 'OpenCL', enabled: true, state: 'mining', driver_offer: offer });
let w = V.driverWords(egpu, idle);
assert.equal(w.kind, 'offer');
assert.equal(w.external, true);
assert.ok(w.sub.includes('This card\u2019s worker stops for the install and comes back after it; the other cards keep mining.'), w.sub);
assert.ok(w.sub.includes('The display reset can take the machine for a minute and may need a restart'), w.sub);
// the same card inside the case: no stop, the plain sentence
const inside = card({ ...egpu, kind: 'discrete' });
w = V.driverWords(inside, idle);
assert.equal(w.external, false);
assert.ok(w.sub.endsWith('Mining continues.'), w.sub);
assert.ok(!w.sub.includes('display reset'));
// while it installs on the eGPU card, the running line says the worker is stopped
w = V.driverWords(egpu, { 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: false } });
assert.equal(w.kind, 'running');
assert.ok(w.sub.includes('This card\u2019s worker is stopped until the installer ends'), w.sub);
});
test('a card behind a USB4 or Thunderbolt router is an eGPU on the Cards page (PC 2, 7 October 2026)', () => {
const r = V.cardRow(card({ key: 'nvidia:1:NVIDIA GeForce RTX 5060 Ti', name: 'NVIDIA GeForce RTX 5060 Ti', vendor: 'nvidia', kind: 'external', enabled: true, state: 'mining', hash_now: 40, power_w: 120, temp_gpu: 55 }));
assert.equal(r.kindWord, 'eGPU');

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

View file

@ -41,6 +41,16 @@ Apple: none. The exit codes: Intel 14 and 1014 (the bootstrapper's wrap, measure
Sizes: the installers are 889 MB to 1.0 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.
## 3b. A card in an external enclosure (7 October 2026, after PC 2)
PC 2 at 16:26Z: the Intel installer started with the app's own worker mining on the Arc B580 in the Razer Core X V2. The installer's display reset took the machine down at 16:27:14Z (every log stops together; a power cycle). The rule, on this branch:
- A driver install on a card the app reads as `external` (the eGPU kind: a USB4 or Thunderbolt router in the device's parent chain, update-return-21b) stops that card's worker before the installer starts (`drivertable::install_hold_keys`, through the cards path with the restore choice kept, the `--cards-off` shape). The vendor's cards inside the case and every other vendor's card keep mining.
- The row says so before the click ("This card's worker stops for the install and comes back after it; the other cards keep mining. The display reset can take the machine for a minute and may need a restart: save your work first.") and while it runs ("This card's worker is stopped until the installer ends; the display reset can take the machine for a minute.").
- When the installer ends the held card goes back as it was (enabled, identities, cap). A card the install took away (Windows re-enumerates an eGPU through the reset) comes back when the card does: the detection that lists it again restores it (`driver_release_held(true)`). The app never restarts the machine.
Per tier: a solo miner with one eGPU loses that card's rate for the install (3 to 8 minutes on a home line, the download dominating) and nothing else; a rig with the card inside the case sees no change; macOS and Linux have no install step. Tests: the known-failed rule test first (an install on an eGPU card with the worker still running held nothing: `an_install_on_an_egpu_card_stops_that_cards_worker_first_and_leaves_the_other_cards_mining`, red on build-2 at 17:1xZ, then green), the view test for the two sentences. Captures 13 to 16: the eGPU offer (13, 14) and the install running with the card's worker off (15, 16), mock scenarios `drivers-egpu` and `drivers-egpu-running`.
## 4. Not done, and what follows
- The table's publish: `publish-manifest.sh --drivers packaging/ota/drivers.json` with the 0.3.21 manifest (the shipper's step); every row is measured.

View file

@ -218,7 +218,7 @@ 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') {
if (scenario === 'drivers' || scenario === 'drivers-running' || scenario === 'drivers-reboot' || scenario === 'drivers-error' || scenario === 'drivers-mac' || scenario === 'drivers-egpu' || scenario === 'drivers-egpu-running') {
// 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/';
@ -233,6 +233,15 @@ function stateNow() {
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-egpu' || scenario === 'drivers-egpu-running') {
// PC 2, 7 October 2026: the Arc B580 in the Razer Core X V2 (the eGPU kind), the 5060 Ti inside the case; the offer
// on the eGPU row warns that its worker stops and the display reset can take the machine; running: the worker off
var arcRow = s.mining.cards.find(c => c.vendor === 'intel');
s.mining.cards = s.mining.cards.filter(c => c.vendor === 'intel' || c.vendor === 'nvidia').map(c => c.vendor === 'intel' ? { ...c, kind: 'external', enabled: true, state: 'mining', hash_now: 11.0, hash_avg: 10.98, power_w: 118, temp_gpu: 61, identities: 2, ids: ['7c2e', 'a91f'] } : { ...c, key: 'nvidia:0:NVIDIA GeForce RTX 5060 Ti', name: 'NVIDIA GeForce RTX 5060 Ti', detail: '4608 cores', vram_mb: 16384, hash_now: 40.2, hash_avg: 40.1, power_w: 152, power_default_w: 180, power_limit_w: 152 });
s.display_name = 'pc-2'; s.mining.hash_total = 51.2;
if (scenario === 'drivers-egpu-running') { s.mining.cards = s.mining.cards.map(c => c.vendor === 'intel' ? { ...c, enabled: false, state: 'off', hash_now: 0, power_w: 0 } : c); s.mining.hash_total = 40.2; s.drivers = { ...s.drivers, status: 'installing', vendor: 'intel', version: '32.0.101.9034', progress: 0.72, message: 'installing INTEL 32.0.101.9034: Windows asks for permission once', reboot_required: false, started_at: t - 95, finished_at: 0 }; }
void arcRow;
}
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 === 'euro') {