diff --git a/app/igneum-app/src/drivertable.rs b/app/igneum-app/src/drivertable.rs index 37045a9ee..ca9b27b38 100644 --- a/app/igneum-app/src/drivertable.rs +++ b/app/igneum-app/src/drivertable.rs @@ -244,9 +244,36 @@ pub fn authenticode_verdict(text: &str, signer: &str) -> Result 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, vendor: &str) -> Vec { + 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"}, diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index 4d4d0878b..4bc1249ce 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -700,6 +700,8 @@ pub struct Engine { /// driver-check: the manifest's table as last read from /drivers.json (None = none published) drivers_table: Option, driver_busy: bool, + /// the vendor's cards held off for a driver install, put back as they were when it ends + driver_held: Vec, /// the interface over the air (src/uiota.rs) uiota: crate::uiota::UiOta, jobs: crate::jobrun::Jobs, @@ -867,6 +869,7 @@ impl Engine { upload_thread: None, drivers_table: ota.drivers_table(), driver_busy: false, + driver_held: Vec::new(), ota, uiota, jobs, @@ -1063,6 +1066,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), @@ -1708,10 +1713,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, Vec) = { + 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 = 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::>().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, Vec) = { + 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::>().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::>().join(", "))); + } + self.driver_held = later; + } + fn driver_event(&mut self, ev: crate::drivers::Event) { match ev { crate::drivers::Event::Progress(p, text) => { @@ -1722,6 +1764,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), diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index b0eb6bbe8..d2e928d5f 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -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'; } diff --git a/app/igneum-app/ui/view.test.mjs b/app/igneum-app/ui/view.test.mjs index df71c2040..d3b9aab46 100644 --- a/app/igneum-app/ui/view.test.mjs +++ b/app/igneum-app/ui/view.test.mjs @@ -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'); diff --git a/docs/plans/driver-check-shots/13-cards-egpu-offer-light.png b/docs/plans/driver-check-shots/13-cards-egpu-offer-light.png new file mode 100644 index 000000000..60e3acf76 Binary files /dev/null and b/docs/plans/driver-check-shots/13-cards-egpu-offer-light.png differ diff --git a/docs/plans/driver-check-shots/14-cards-egpu-offer-dark.png b/docs/plans/driver-check-shots/14-cards-egpu-offer-dark.png new file mode 100644 index 000000000..c565a9a67 Binary files /dev/null and b/docs/plans/driver-check-shots/14-cards-egpu-offer-dark.png differ diff --git a/docs/plans/driver-check-shots/15-cards-egpu-running-light.png b/docs/plans/driver-check-shots/15-cards-egpu-running-light.png new file mode 100644 index 000000000..69fe36558 Binary files /dev/null and b/docs/plans/driver-check-shots/15-cards-egpu-running-light.png differ diff --git a/docs/plans/driver-check-shots/16-cards-egpu-running-dark.png b/docs/plans/driver-check-shots/16-cards-egpu-running-dark.png new file mode 100644 index 000000000..5f72e3878 Binary files /dev/null and b/docs/plans/driver-check-shots/16-cards-egpu-running-dark.png differ diff --git a/docs/plans/driver-check.md b/docs/plans/driver-check.md index 55bb50ccb..b622baac8 100644 --- a/docs/plans/driver-check.md +++ b/docs/plans/driver-check.md @@ -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 `/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. diff --git a/tools/ui-mock/server.mjs b/tools/ui-mock/server.mjs index 5b2b16cf8..342c2e56f 100644 --- a/tools/ui-mock/server.mjs +++ b/tools/ui-mock/server.mjs @@ -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') {