igneum/app/igneum-app/src/main.rs
igneum-labs d00772bf20 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 3f0ef65786)
2026-10-07 13:09:46 +00:00

180 lines
7.4 KiB
Rust

//! Igneum Miner engine. Supervises igneumd, igneum-miner and the GPU workers and serves the dashboard on
//! 127.0.0.1:<random port>/t/<token>/. The window host (macOS: the Swift app, Windows: the WebView2 host) starts it with
//! --wrapper, reads `URL ...` and `STATE {...}` lines from its stdout and writes `quit` on its stdin. Without a host
//! (--open) the dashboard opens in the default browser.
//!
//! igneum-app [--wrapper | --open | --no-open | --launch | --sweep] [--print-url]
//! --launch is what the Start Menu entry runs on Windows: it hands over to "Igneum Miner.exe" (the window host) when
//! that is installed next to the engine, else runs the engine with the dashboard in the default browser.
//! --sweep runs the efficiency sweep (src/sweep.rs) on every supported card as soon as it mines, prints the SWEEP
//! table on stdout, leaves the chosen caps in force and quits; no browser opens (the PC measurement job).
//!
//! Environment (tests): IGNEUM_APP_NETWORK devnet|simnet, IGNEUM_APP_RPC_PORT, IGNEUM_APP_P2P_PORT, IGNEUM_APP_PEERS
//! (comma list, empty for none), IGNEUM_APP_UNSYNCED=1, IGNEUM_APP_DATA, IGNEUM_APP_LOGS, IGNEUM_APP_BIN, IGNEUM_APP_NODE_DIR.
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
mod config;
mod detect;
mod engine;
mod hotplug;
mod keys;
mod platform;
mod procs;
mod server;
mod state;
mod manifest;
mod ota;
mod uiota;
mod update;
mod jobs;
mod jobrun;
mod jobbuild;
mod prover;
mod provedefault;
mod provingdir;
mod segments;
mod verifier;
mod wslhost;
mod sweep;
mod ember;
mod heat;
mod powertask;
mod watchdog;
mod live;
mod extnode;
mod merge;
mod ladder;
mod chainfacts;
mod card;
mod drivertable;
mod drivers;
use std::io::{BufRead, Write};
use std::sync::mpsc::channel;
use std::sync::Arc;
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let wrapper = args.iter().any(|a| a == "--wrapper");
let sweep = args.iter().any(|a| a == "--sweep");
if sweep {
// the runtime reads it (config::Runtime::from_env); the engine starts at once and quits after the sweep
std::env::set_var("IGNEUM_APP_SWEEP", "1");
}
let no_open = wrapper || sweep || args.iter().any(|a| a == "--no-open");
if args.iter().any(|a| a == "--version" || a == "-V") {
println!("igneum-app {}", engine::VERSION);
return;
}
if args.iter().any(|a| a == "--power-helper") {
// the scheduled task's action (src/powertask.rs): elevated, runs only digit-argument nvidia-smi commands
// from <app data>/app/sweep/cmd.txt, exits on quit, remove or 20 idle minutes
let dir = powertask::helper_dir();
std::process::exit(powertask::run_helper(&dir));
}
if args.iter().any(|a| a == "--launch") {
if let Some(dir) = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())) {
let host = dir.join("Igneum Miner.exe");
if host.exists() {
let mut c = std::process::Command::new(&host);
c.current_dir(&dir);
crate::platform::quiet(&mut c); // no console of our own for the window host (it is a GUI program; the flag only governs a console)
if c.spawn().is_ok() {
return;
}
}
}
// no window host: the engine runs on its own and the dashboard opens in the default browser
}
platform::clear_quarantine();
let runtime = config::Runtime::from_env();
let _ = std::fs::create_dir_all(&runtime.app_dir);
let _ = std::fs::create_dir_all(&runtime.log_dir);
platform::lock_permissions(&runtime.app_dir, true);
let bins = match detect::find_bins() {
Ok(b) => b,
Err(e) => {
eprintln!("{e}");
if wrapper {
println!("FATAL {e}");
}
std::process::exit(1);
}
};
let mut candidates = vec![bins.dir.join("igneum-app.json")];
if let Ok(exe) = std::env::current_exe() {
if let Some(d) = exe.parent() {
candidates.push(d.join("igneum-app.json"));
if let Some(c) = d.parent() {
candidates.push(c.join("Resources").join("igneum-app.json"));
}
}
}
let packaged = config::Packaged::load(&candidates).with_env_overrides();
let settings = config::Settings::load(&runtime.app_dir.join("settings.json"));
// a measurement engine runs with the installed app's choices and its own switches (config.rs for_measurement)
let settings = if sweep { settings.for_measurement() } else { settings };
// the per-launch token: 32 hex characters from the OS
let mut raw = [0u8; 16];
getrandom::getrandom(&mut raw).expect("os randomness");
let token = keys::hex(&raw);
let stamp_file = runtime.log_dir.join(format!("app-{}.log", stamp_now()));
let engine_log = std::fs::File::create(&stamp_file).ok();
let (tx, rx) = channel();
let shared = Arc::new(engine::Shared::new(token.clone(), runtime, packaged, settings, tx, engine_log, stamp_file.clone()));
let port = match server::start(shared.clone()) {
Ok(p) => p,
Err(e) => {
eprintln!("cannot listen on 127.0.0.1: {e}");
std::process::exit(1);
}
};
shared.set_port(port);
let url = format!("http://127.0.0.1:{port}/t/{token}/");
// the URL file lets a second launch or a support script find the window; user-only permissions
let url_file = shared.runtime.app_dir.join("app.url");
let _ = std::fs::write(&url_file, &url);
platform::lock_permissions(&url_file, false);
// the token is never logged (the logs are uploaded); app.url, 0600, is the one place it is written
shared.log(&format!("dashboard listening on 127.0.0.1:{port} (the URL with its token is in app.url; log {})", stamp_file.display()));
if wrapper || args.iter().any(|a| a == "--print-url") {
println!("URL {url}");
let _ = std::io::stdout().flush();
}
if !no_open {
platform::open_url(&url);
}
// the host's commands on stdin; stdin closing in wrapper mode means the host is gone
{
let shared = shared.clone();
std::thread::spawn(move || {
let stdin = std::io::stdin();
for line in stdin.lock().lines() {
let Ok(l) = line else { break };
let t = l.trim();
match t {
"quit" => shared.send(engine::Cmd::Quit("the window host (quit on stdin: the tray menu or the installer)")),
"pause" => shared.send(engine::Cmd::Pause),
"resume" => shared.send(engine::Cmd::Resume),
// the window host saw WM_DEVICECHANGE (a card plugged in or out): enumerate now, not at the next minute
"detect" => shared.send(engine::Cmd::Detect),
"elevated ok" => shared.send(engine::Cmd::ElevatedDone(Ok(()))),
_ if t.starts_with("elevated fail") => shared.send(engine::Cmd::ElevatedDone(Err(t.trim_start_matches("elevated fail").trim_start_matches(':').trim().to_string()))),
_ => {}
}
}
if wrapper {
shared.send(engine::Cmd::Quit("the window host went away (stdin closed)"));
}
});
}
engine::Engine::new(shared, bins, rx, wrapper).run();
}
fn stamp_now() -> String {
let t = platform::unix_now();
format!("{}-{:02}{:02}{:02}", t / 86400, t % 86400 / 3600, t % 3600 / 60, t % 60)
}