Igneum Miner 0.3.3: an unattended Windows miner is never stranded by an update (4 Oct 15:40 incident: both PCs stopped at the installer's UAC prompt). Per-user installer (PrivilegesRequired=lowest, %LOCALAPPDATA%\Programs, migration from Program Files offered only when someone is at the keyboard, firewall rule asked once on first run and mining without it); the Windows helper runs the installer FIRST with the engine still mining and only the installer's own stop step ends it, a declined or unanswered prompt leaves the machine mining with "OTA: waiting for administrator approval" / "administrator approval not given; waits for the next time someone is at this PC" in the log and the intake, retry on Install now, next start or 6 h; machines take turns (apply only in the minute = machine id8 mod 60) and hold while /api/live shows over 30% of identities gone in 10 minutes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
b338db3fac
commit
dc33031c3a
12 changed files with 388 additions and 68 deletions
2
app/igneum-app/Cargo.lock
generated
2
app/igneum-app/Cargo.lock
generated
|
|
@ -219,7 +219,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "igneum-app"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"ed25519-dalek",
|
||||
"getrandom",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "igneum-app"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
edition = "2021"
|
||||
description = "Igneum Miner engine: supervises the node, the miner and the GPU workers, and serves the dashboard on 127.0.0.1"
|
||||
license = "MIT"
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
1 ICON "igneum.ico"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,3,2,0
|
||||
PRODUCTVERSION 0,3,2,0
|
||||
FILEVERSION 0,3,3,0
|
||||
PRODUCTVERSION 0,3,3,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
|
|
@ -20,12 +20,12 @@ BEGIN
|
|||
BEGIN
|
||||
VALUE "CompanyName", "Igneum"
|
||||
VALUE "FileDescription", "Igneum Miner engine"
|
||||
VALUE "FileVersion", "0.3.2"
|
||||
VALUE "FileVersion", "0.3.3"
|
||||
VALUE "InternalName", "igneum-app"
|
||||
VALUE "LegalCopyright", "Igneum contributors"
|
||||
VALUE "OriginalFilename", "igneum-app.exe"
|
||||
VALUE "ProductName", "Igneum Miner"
|
||||
VALUE "ProductVersion", "0.3.2"
|
||||
VALUE "ProductVersion", "0.3.3"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
|
|
|||
|
|
@ -1506,7 +1506,7 @@ impl Engine {
|
|||
}
|
||||
let v = self.ota.version();
|
||||
match self.ota.launch_apply(&self.shared, self.host_pid()) {
|
||||
Ok(()) => {
|
||||
Ok(crate::ota::Launch::QuitNow) => {
|
||||
{
|
||||
let mut st = self.st();
|
||||
st.update.applying = true;
|
||||
|
|
@ -1517,6 +1517,16 @@ impl Engine {
|
|||
self.shared.event("info", &format!("installing Igneum Miner {v}: the miners stop, then the node, then the app opens again"));
|
||||
self.quitting = true;
|
||||
}
|
||||
Ok(crate::ota::Launch::InstallerRunning) => {
|
||||
// Windows: the installer stops this engine itself (api/quit) once it may run; until then we mine
|
||||
{
|
||||
let mut st = self.st();
|
||||
st.update.applying = true;
|
||||
st.update.status = "applying".into();
|
||||
st.update.wait = "the installer is starting; if Windows asks for permission the miners keep running until it is given".into();
|
||||
}
|
||||
self.shared.event("info", &format!("installing Igneum Miner {v}: the installer runs first; the miners keep running until it is allowed to, then it stops them, then the node, and the app opens again"));
|
||||
}
|
||||
Err(e) => {
|
||||
self.shared.event("error", &format!("the update could not start: {e}"));
|
||||
let mut st = self.st();
|
||||
|
|
|
|||
|
|
@ -293,6 +293,18 @@ pub struct Moment {
|
|||
pub ready_for_s: u64,
|
||||
/// A consensus activation is close, or this version is below min_supported_version: now beats later.
|
||||
pub urgent: bool,
|
||||
/// This minute is the machine's own slot (slot_minute): machines take turns, two never restart together.
|
||||
pub slot_ok: bool,
|
||||
/// How much of the network's identity count (/api/live, last 10 minutes) is gone right now, in percent.
|
||||
pub network_drop_pct: f64,
|
||||
}
|
||||
|
||||
/// The percentage of identities the network may lose in 10 minutes before updates hold (we are the devnet).
|
||||
pub const NETWORK_DROP_HOLD_PCT: f64 = 30.0;
|
||||
|
||||
/// The minute of the hour in which this machine applies updates: the first 8 hex of the machine id modulo 60.
|
||||
pub fn slot_minute(id8: &str) -> u64 {
|
||||
u64::from_str_radix(id8.trim(), 16).unwrap_or(0) % 60
|
||||
}
|
||||
|
||||
/// Ok when the update may be applied now; Err carries the reason to wait, in the words the dashboard shows.
|
||||
|
|
@ -300,6 +312,12 @@ pub fn safe_to_apply(m: &Moment) -> Result<(), String> {
|
|||
if m.urgent {
|
||||
return Ok(());
|
||||
}
|
||||
if m.network_drop_pct > NETWORK_DROP_HOLD_PCT {
|
||||
return Err(format!("the network lost {:.0}% of its identities in the last 10 minutes; holding the update", m.network_drop_pct));
|
||||
}
|
||||
if !m.slot_ok {
|
||||
return Err("waiting for this machine's own minute of the hour (machines take turns)".into());
|
||||
}
|
||||
if m.ready_for_s >= SAFE_MOMENT_PATIENCE_S {
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -453,7 +471,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn safe_moments() {
|
||||
let base = Moment { node_synced: true, boundary_eta_s: Some(1800), miner_busy: false, ready_for_s: 60, urgent: false };
|
||||
let base = Moment { node_synced: true, boundary_eta_s: Some(1800), miner_busy: false, ready_for_s: 60, urgent: false, slot_ok: true, network_drop_pct: 0.0 };
|
||||
assert!(safe_to_apply(&base).is_ok());
|
||||
assert_eq!(safe_to_apply(&Moment { node_synced: false, ..base.clone() }).unwrap_err(), "waiting for the node to sync");
|
||||
assert!(safe_to_apply(&Moment { boundary_eta_s: Some(120), ..base.clone() }).unwrap_err().contains("boundary in 120 s"));
|
||||
|
|
@ -466,6 +484,24 @@ mod tests {
|
|||
// patience: an unsynced node for 6 h applies anyway
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, ready_for_s: SAFE_MOMENT_PATIENCE_S, ..base.clone() }).is_ok());
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, ready_for_s: SAFE_MOMENT_PATIENCE_S - 1, ..base.clone() }).is_err());
|
||||
// the machine's slot: outside it nothing applies, not even with patience; urgent ignores it
|
||||
assert!(safe_to_apply(&Moment { slot_ok: false, ..base.clone() }).unwrap_err().contains("own minute"));
|
||||
assert!(safe_to_apply(&Moment { slot_ok: false, ready_for_s: SAFE_MOMENT_PATIENCE_S, ..base.clone() }).is_err());
|
||||
assert!(safe_to_apply(&Moment { slot_ok: false, urgent: true, ..base.clone() }).is_ok());
|
||||
// the network guard: over 30% of identities gone in 10 minutes holds the update (we are the devnet)
|
||||
assert!(safe_to_apply(&Moment { network_drop_pct: 30.0, ..base.clone() }).is_ok());
|
||||
assert!(safe_to_apply(&Moment { network_drop_pct: 30.1, ..base.clone() }).unwrap_err().contains("lost 30%"));
|
||||
assert!(safe_to_apply(&Moment { network_drop_pct: 80.0, ready_for_s: SAFE_MOMENT_PATIENCE_S, ..base.clone() }).is_err());
|
||||
assert!(safe_to_apply(&Moment { network_drop_pct: 80.0, urgent: true, ..base.clone() }).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slots() {
|
||||
assert_eq!(slot_minute("1ccfe586"), 58); // PC 2
|
||||
assert_eq!(slot_minute("00000000"), 0);
|
||||
assert_eq!(slot_minute("0000003c"), 0);
|
||||
assert_eq!(slot_minute("0000003b"), 59);
|
||||
assert_eq!(slot_minute("zz"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -14,8 +14,11 @@
|
|||
//! -> apply: the engine stops the miners, then the node, writes update-pending.json, starts a detached helper
|
||||
//! (ota-apply.sh / ota-apply.ps1 in the app data folder) and exits. macOS: the helper waits for the window to
|
||||
//! quit, moves the old bundle to "Igneum Miner.app.previous", the new one in, and opens it. Windows: the helper
|
||||
//! runs the Inno installer /VERYSILENT (an administrator prompt appears; the installer stops what is left,
|
||||
//! replaces the files and relaunches the app with /IGNOTA=1).
|
||||
//! runs the per-user Inno installer /VERYSILENT FIRST, with the engine still mining; the installer stops the
|
||||
//! engine itself (api/quit), replaces the files and relaunches the app with /IGNOTA=1. An administrator prompt
|
||||
//! (an install still in Program Files) that nobody answers leaves the machine mining: the update is deferred.
|
||||
//! Machines take turns: each applies only in its own minute of the hour (machine id modulo 60), and never while
|
||||
//! the network lost over 30% of its identities in 10 minutes (/api/live).
|
||||
//! -> rollback: the helper restores the previous bundle when the new app does not start twice; the new engine
|
||||
//! counts its own starts in update-pending.json and, on the third start without 90 healthy seconds, restores
|
||||
//! the previous version (macOS: the .previous bundle; Windows: the previous installer kept in updates/).
|
||||
|
|
@ -42,6 +45,8 @@ pub enum Event {
|
|||
Downloaded(Result<PathBuf, String>),
|
||||
/// macOS: the new bundle staged next to the running one. Windows: the installer path again.
|
||||
Staged(Result<PathBuf, String>),
|
||||
/// The network's identity count from /api/live (state.miners_10m); None when the site did not answer.
|
||||
Live(Option<u64>),
|
||||
}
|
||||
|
||||
/// What the engine must do now.
|
||||
|
|
@ -49,6 +54,16 @@ pub enum Action {
|
|||
Apply,
|
||||
}
|
||||
|
||||
/// What launch_apply started.
|
||||
#[allow(dead_code)] // one variant per platform
|
||||
pub enum Launch {
|
||||
/// macOS: the helper waits for this engine to exit; the engine leaves through its quit path now.
|
||||
QuitNow,
|
||||
/// Windows: the installer runs first, while the engine keeps mining; the installer stops the engine itself
|
||||
/// (api/quit) once it is allowed to run. The engine stays up and watches update-result.json for a deferral.
|
||||
InstallerRunning,
|
||||
}
|
||||
|
||||
pub struct Ctx {
|
||||
pub node_synced: bool,
|
||||
pub boundary_eta_s: Option<i64>,
|
||||
|
|
@ -94,6 +109,17 @@ pub struct Updater {
|
|||
/// the consensus override file written from the manifest, when it changed since the last take
|
||||
override_changed: Option<PathBuf>,
|
||||
override_daa: u64,
|
||||
/// Windows: the installer was started and the engine is still up (it stops us when it may run)
|
||||
apply_launched: Option<Instant>,
|
||||
/// the administrator prompt was not answered: no automatic retry before this (Install now still works)
|
||||
deferred_until: Option<Instant>,
|
||||
/// this machine's minute of the hour for applying (manifest::slot_minute of the machine id)
|
||||
slot: u64,
|
||||
/// identity counts from /api/live over the last 10 minutes, sampled while an update is ready
|
||||
live_samples: Vec<(Instant, u64)>,
|
||||
live_next: Instant,
|
||||
live_busy: bool,
|
||||
live_api: String,
|
||||
}
|
||||
|
||||
impl Updater {
|
||||
|
|
@ -131,7 +157,23 @@ impl Updater {
|
|||
staged_digest: String::new(),
|
||||
override_changed: None,
|
||||
override_daa: 0,
|
||||
apply_launched: None,
|
||||
deferred_until: None,
|
||||
slot: manifest::slot_minute(&shared.runtime.id8()),
|
||||
live_samples: Vec::new(),
|
||||
live_next: now,
|
||||
live_busy: false,
|
||||
live_api: env("IGNEUM_APP_LIVE_API").unwrap_or_else(|| live_api_from(&shared.packaged.live_page)),
|
||||
};
|
||||
shared.log(&format!("update slot: minute {} of every hour (machine id {})", u.slot, shared.runtime.id8()));
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// the login entry follows the install folder (a per-user install replaces one in Program Files)
|
||||
if crate::platform::start_at_login_is_on() {
|
||||
let _ = crate::platform::set_start_at_login(true);
|
||||
}
|
||||
firewall_first_run(shared);
|
||||
}
|
||||
u.failed_versions = std::fs::read_to_string(u.failed_path()).ok().and_then(|t| serde_json::from_str::<Vec<String>>(&t).ok()).unwrap_or_default();
|
||||
// the cached manifest: the rollback floor and the consensus override are known before the first check
|
||||
if let Ok(text) = std::fs::read_to_string(u.dir.join("manifest.json")) {
|
||||
|
|
@ -226,8 +268,13 @@ impl Updater {
|
|||
let ok = v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false);
|
||||
let err = v.get("error").and_then(|x| x.as_str()).unwrap_or("").to_string();
|
||||
let rolled_back = v.get("rolled_back").and_then(|x| x.as_bool()).unwrap_or(false);
|
||||
let deferred = v.get("deferred").and_then(|x| x.as_bool()).unwrap_or(false);
|
||||
let ver = v.get("version").and_then(|x| x.as_str()).unwrap_or("").to_string();
|
||||
if !ok {
|
||||
if !ok && deferred {
|
||||
// the installer never ran (nobody answered the administrator prompt): not a failure, it tries again
|
||||
shared.log(&format!("OTA: the update to {ver} was deferred before this start ({err}); it tries again in this machine's slot"));
|
||||
let _ = std::fs::remove_file(self.pending_path());
|
||||
} else if !ok {
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
st.update.error = err.clone();
|
||||
st.update.status = "error".into();
|
||||
|
|
@ -292,7 +339,7 @@ impl Updater {
|
|||
u.downloaded = self.file.is_some();
|
||||
u.ready = self.staged.is_some();
|
||||
u.file = self.file.as_ref().map(|p| p.display().to_string()).unwrap_or_default();
|
||||
if u.status != "applying" && u.status != "manual" && u.status != "error" {
|
||||
if u.status != "applying" && u.status != "manual" && u.status != "error" && u.status != "deferred" {
|
||||
u.status = if self.staged.is_some() {
|
||||
"ready".into()
|
||||
} else if self.file.is_some() {
|
||||
|
|
@ -391,8 +438,47 @@ impl Updater {
|
|||
return None;
|
||||
}
|
||||
self.last_safe_check = now;
|
||||
// Windows: the installer was started with the engine still mining; it stops us when it may run. Until then
|
||||
// watch for the helper's verdict (an unanswered administrator prompt), never the other way round.
|
||||
if let Some(t) = self.apply_launched {
|
||||
match self.read_result() {
|
||||
Some((false, err, _)) => {
|
||||
let _ = std::fs::remove_file(self.result_path());
|
||||
self.defer(shared, &err);
|
||||
}
|
||||
Some((true, ..)) => {} // the installer is in: it stops this engine any moment now
|
||||
None if now.duration_since(t) >= Duration::from_secs(15 * 60) => self.defer(shared, "no answer from the installer in 15 minutes"),
|
||||
None => {}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
if let Some(u) = self.deferred_until {
|
||||
if now < u && !self.install_asked {
|
||||
return None;
|
||||
}
|
||||
self.deferred_until = None;
|
||||
shared.state.lock().unwrap().update.status = "ready".into();
|
||||
}
|
||||
// the network guard: /api/live every 60 s while an update waits
|
||||
if !self.live_api.is_empty() && !self.live_busy && now >= self.live_next {
|
||||
self.live_next = now + Duration::from_secs(60);
|
||||
self.live_busy = true;
|
||||
let url = self.live_api.clone();
|
||||
let shared2 = shared.clone();
|
||||
std::thread::spawn(move || shared2.send(Cmd::Ota(Event::Live(fetch_live_identities(&url)))));
|
||||
}
|
||||
self.live_samples.retain(|(t, _)| now.duration_since(*t) <= Duration::from_secs(600));
|
||||
let network_drop_pct = {
|
||||
let max = self.live_samples.iter().map(|(_, n)| *n).max().unwrap_or(0);
|
||||
match self.live_samples.last() {
|
||||
Some((_, cur)) if max > 0 && self.live_samples.len() >= 2 => (max.saturating_sub(*cur)) as f64 * 100.0 / max as f64,
|
||||
_ => 0.0,
|
||||
}
|
||||
};
|
||||
let minute = (crate::platform::unix_now() / 60) % 60;
|
||||
let slot_ok = minute == self.slot || std::env::var("IGNEUM_APP_UPDATE_NO_SLOT").map(|v| v == "1").unwrap_or(false);
|
||||
let ready_for = self.ready_since.map(|t| now.duration_since(t).as_secs()).unwrap_or(0);
|
||||
let moment = Moment { node_synced: ctx.node_synced, boundary_eta_s: ctx.boundary_eta_s, miner_busy: ctx.miner_busy, ready_for_s: ready_for, urgent: urgent || self.install_asked };
|
||||
let moment = Moment { node_synced: ctx.node_synced, boundary_eta_s: ctx.boundary_eta_s, miner_busy: ctx.miner_busy, ready_for_s: ready_for, urgent: urgent || self.install_asked, slot_ok, network_drop_pct };
|
||||
if !self.auto && !urgent && !self.install_asked {
|
||||
shared.state.lock().unwrap().update.wait = "waiting for Install now (automatic updates are off)".into();
|
||||
return None;
|
||||
|
|
@ -405,12 +491,40 @@ impl Updater {
|
|||
match manifest::safe_to_apply(&moment) {
|
||||
Ok(()) => Some(Action::Apply),
|
||||
Err(why) => {
|
||||
let why = if why.contains("own minute") { format!("{why}: at :{:02} past the hour", self.slot) } else { why };
|
||||
shared.state.lock().unwrap().update.wait = why;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The helper's verdict file: (ok, error, deferred).
|
||||
fn read_result(&self) -> Option<(bool, String, bool)> {
|
||||
let v: Value = serde_json::from_str(&std::fs::read_to_string(self.result_path()).ok()?).ok()?;
|
||||
Some((
|
||||
v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false),
|
||||
v.get("error").and_then(|x| x.as_str()).unwrap_or("").to_string(),
|
||||
v.get("deferred").and_then(|x| x.as_bool()).unwrap_or(false),
|
||||
))
|
||||
}
|
||||
|
||||
/// Windows: the installer could not run (the administrator prompt was declined, timed out, or nobody was there).
|
||||
/// The engine never stopped, so mining goes on; the update waits for Install now, the next start, or 6 hours.
|
||||
fn defer(&mut self, shared: &Arc<Shared>, err: &str) {
|
||||
self.apply_launched = None;
|
||||
self.install_asked = false;
|
||||
self.deferred_until = Some(Instant::now() + Duration::from_secs(6 * 3600));
|
||||
let _ = std::fs::remove_file(self.pending_path());
|
||||
let v = self.version();
|
||||
{
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
st.update.applying = false;
|
||||
st.update.status = "deferred".into();
|
||||
st.update.wait = "waits for the next time someone is at this PC (Windows asks for permission); mining continues".into();
|
||||
}
|
||||
shared.event("info", &format!("OTA: administrator approval not given for Igneum Miner {v} ({err}); the update waits for the next time someone is at this PC; mining continues"));
|
||||
}
|
||||
|
||||
fn urgent(&self, ctx: &Ctx) -> bool {
|
||||
let Some(m) = &self.manifest else { return false };
|
||||
if self.entry.is_none() {
|
||||
|
|
@ -531,6 +645,13 @@ impl Updater {
|
|||
self.start_stage(shared, p);
|
||||
}
|
||||
},
|
||||
Event::Live(n) => {
|
||||
self.live_busy = false;
|
||||
if let Some(n) = n {
|
||||
self.live_samples.push((Instant::now(), n));
|
||||
}
|
||||
return;
|
||||
}
|
||||
Event::Staged(r) => match r {
|
||||
Err(e) if e.starts_with("manual:") => {
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
|
|
@ -601,7 +722,11 @@ impl Updater {
|
|||
/// Install now: a ready update applies at once; a downloaded one as soon as it is staged; else a check runs.
|
||||
pub fn install_now(&mut self, shared: &Arc<Shared>) {
|
||||
self.install_asked = true;
|
||||
self.deferred_until = None;
|
||||
self.last_safe_check = Instant::now() - Duration::from_secs(10);
|
||||
if self.apply_launched.is_some() {
|
||||
return; // the installer is already up (its prompt may be waiting on the screen)
|
||||
}
|
||||
if self.staged.is_some() {
|
||||
shared.state.lock().unwrap().update.wait = "installing now".into();
|
||||
return;
|
||||
|
|
@ -649,9 +774,11 @@ impl Updater {
|
|||
if std::fs::write(&p, vars.join("\n") + "\n").is_ok() { p.display().to_string() } else { String::new() }
|
||||
}
|
||||
|
||||
/// Writes update-pending.json and the helper, starts the helper detached. The engine exits right after.
|
||||
/// `host_pid` is the window host when the engine runs under one.
|
||||
pub fn launch_apply(&mut self, shared: &Arc<Shared>, host_pid: u32) -> Result<(), String> {
|
||||
/// Writes update-pending.json and the helper, starts the helper detached. macOS: the engine exits right after
|
||||
/// (Launch::QuitNow). Windows: the installer runs first while the engine keeps mining and stops the engine itself
|
||||
/// once it may run (Launch::InstallerRunning); an unanswered administrator prompt never strands the machine
|
||||
/// (4 October 2026: two unattended PCs sat stopped at a prompt for an hour). `host_pid` is the window host.
|
||||
pub fn launch_apply(&mut self, shared: &Arc<Shared>, host_pid: u32) -> Result<Launch, String> {
|
||||
let staged = self.staged.clone().ok_or("no update is ready")?;
|
||||
let to = self.version();
|
||||
// R4.3.5: what is about to be swapped in is re-verified now, not only when it was downloaded
|
||||
|
|
@ -687,7 +814,7 @@ impl Updater {
|
|||
let args = ["apply".to_string(), std::process::id().to_string(), host_pid.to_string(), app.display().to_string(), staged.display().to_string(), to.clone(), result.display().to_string(), env_file, self.staged_digest.clone()];
|
||||
shared.log(&format!("update: starting the helper: bash {} {}", script.display(), args.join(" ")));
|
||||
spawn_detached(Command::new(crate::platform::tool("nohup")).arg(crate::platform::tool("bash")).arg(&script).args(&args))?;
|
||||
Ok(())
|
||||
Ok(Launch::QuitNow)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
|
|
@ -699,9 +826,14 @@ impl Updater {
|
|||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-File"]).arg(&script).args([
|
||||
"-Mode", "apply", "-EnginePid", &std::process::id().to_string(), "-Installer", &staged.display().to_string(), "-Version", &to, "-Result", &result.display().to_string(), "-InstallDir", &install_dir.display().to_string(), "-Sha256", &entry.sha256,
|
||||
]);
|
||||
shared.log(&format!("update: starting the helper: {} (an administrator prompt follows)", script.display()));
|
||||
let per_user = !under_program_files(&install_dir);
|
||||
shared.log(&format!("update: starting the installer first, the miners keep running: {} ({})", script.display(), if per_user { "per-user install, no administrator prompt" } else { "install in Program Files: Windows asks for administrator approval" }));
|
||||
if !per_user {
|
||||
shared.log(&format!("OTA: waiting for administrator approval for Igneum Miner {to}; mining continues until it is given"));
|
||||
}
|
||||
spawn_detached(&mut c)?;
|
||||
Ok(())
|
||||
self.apply_launched = Some(Instant::now());
|
||||
Ok(Launch::InstallerRunning)
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
{
|
||||
|
|
@ -759,6 +891,69 @@ impl Updater {
|
|||
|
||||
// ---- the threads ---------------------------------------------------------------------------------------------------
|
||||
|
||||
/// https://igneum.network/live -> https://igneum.network/api/live; "" when the build carries no live page.
|
||||
fn live_api_from(live_page: &str) -> String {
|
||||
let Some(rest) = live_page.strip_prefix("https://") else { return String::new() };
|
||||
let host = rest.split('/').next().unwrap_or("");
|
||||
if host.is_empty() { String::new() } else { format!("https://{host}/api/live") }
|
||||
}
|
||||
|
||||
/// The network's identity count over the last 10 minutes from /api/live (state.miners_10m).
|
||||
fn fetch_live_identities(url: &str) -> Option<u64> {
|
||||
let out = crate::detect::run_timeout(Command::new(crate::platform::tool("curl")).args(["-fsS", "--max-time", "10", url]), None, Duration::from_secs(12))?;
|
||||
let v: Value = serde_json::from_str(out.trim()).ok()?;
|
||||
v.get("state")?.get("miners_10m")?.as_u64()
|
||||
}
|
||||
|
||||
/// Windows: an install under Program Files was made by the administrator installer (0.3.2 and earlier).
|
||||
#[cfg(windows)]
|
||||
fn under_program_files(dir: &Path) -> bool {
|
||||
let d = dir.to_string_lossy().to_ascii_lowercase();
|
||||
["ProgramFiles", "ProgramFiles(x86)", "ProgramW6432"].iter().filter_map(|k| std::env::var(k).ok()).any(|pf| !pf.is_empty() && d.starts_with(&pf.to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
/// Windows, per-user installs: the inbound firewall rule for igneumd.exe needs administrator approval once. Asked on
|
||||
/// the first run only, in a thread; declined or unanswered, the node still dials out and mines (other nodes cannot
|
||||
/// dial in), and it is never asked again. The administrator installer of 0.3.2 and earlier added the rule itself.
|
||||
#[cfg(windows)]
|
||||
fn firewall_first_run(shared: &Arc<Shared>) {
|
||||
let flag = shared.runtime.app_dir.join("firewall-rule.json");
|
||||
if flag.exists() {
|
||||
return;
|
||||
}
|
||||
let Some(install_dir) = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())) else { return };
|
||||
if under_program_files(&install_dir) {
|
||||
let _ = std::fs::write(&flag, json!({ "source": "installer", "at": crate::platform::unix_now() }).to_string());
|
||||
return;
|
||||
}
|
||||
let node = install_dir.join("igneumd.exe");
|
||||
if !node.is_file() {
|
||||
return;
|
||||
}
|
||||
let script = shared.runtime.app_dir.join("firewall-rule.ps1");
|
||||
let text = format!(
|
||||
"$rule = 'advfirewall firewall add rule name=\"Igneum Miner node\" dir=in action=allow enable=yes profile=private,domain protocol=TCP program=\"{}\"'\n$p = Start-Process -FilePath netsh.exe -ArgumentList $rule -Verb RunAs -Wait -PassThru -WindowStyle Hidden\nexit $p.ExitCode\n",
|
||||
node.display()
|
||||
);
|
||||
if std::fs::write(&script, text).is_err() {
|
||||
return;
|
||||
}
|
||||
let shared = shared.clone();
|
||||
std::thread::spawn(move || {
|
||||
shared.log("firewall: asking once for administrator approval of the inbound rule for igneumd.exe (mining does not wait for it)");
|
||||
let mut c = Command::new(crate::platform::tool("powershell"));
|
||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-File"]).arg(&script);
|
||||
crate::platform::quiet(&mut c);
|
||||
let ok = c.status().map(|s| s.success()).unwrap_or(false);
|
||||
let _ = std::fs::write(&flag, json!({ "source": "first-run", "ok": ok, "at": crate::platform::unix_now() }).to_string());
|
||||
if ok {
|
||||
shared.log("firewall: inbound rule added for igneumd.exe");
|
||||
} else {
|
||||
shared.log("firewall: no inbound rule (administrator approval not given); the node dials out and mines without it, other nodes cannot dial in; not asked again");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn file_name(url: &str) -> String {
|
||||
let name = url.rsplit('/').next().unwrap_or("update").split('?').next().unwrap_or("update");
|
||||
let clean: String = name.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '.' || *c == '-' || *c == '_').collect();
|
||||
|
|
@ -1001,48 +1196,56 @@ esac
|
|||
|
||||
#[cfg(windows)]
|
||||
const WIN_HELPER: &str = r#"# Igneum Miner update helper, written by the engine (src/ota.rs). Not for running by hand.
|
||||
# powershell -File ota-apply.ps1 -Mode apply|rollback -EnginePid <pid> -Installer <setup exe> -Version <v> -Result <json> -InstallDir <folder>
|
||||
# apply: waits for the engine (it exits right after starting this), runs the installer /VERYSILENT with /IGNOTA=1 as
|
||||
# administrator (one UAC prompt; the installer stops what is left, replaces the files and relaunches the app), writes
|
||||
# <json>. If the installer does not run (prompt declined, error), the old app is started again.
|
||||
# rollback: the same with the previous version's installer.
|
||||
# powershell -File ota-apply.ps1 -Mode apply|rollback -EnginePid <pid> -Installer <setup exe> -Version <v> -Result <json> -InstallDir <folder> -Sha256 <hex>
|
||||
# The installer runs FIRST, while the engine keeps mining (4 October 2026: two unattended PCs sat stopped at an
|
||||
# administrator prompt nobody could click). A per-user installer (0.3.3 and later, PrivilegesRequired=lowest) needs no
|
||||
# prompt; an older administrator installer raises one through ShellExecute. Only when the installer actually runs does
|
||||
# its PrepareToInstall step stop the engine (api/quit: miners first, then the node), replace the files and relaunch
|
||||
# the app (/IGNOTA=1). A declined, timed-out or unanswered prompt leaves the engine running: the result says
|
||||
# deferred:true and the engine shows "waits for the next time someone is at this PC". The old app is relaunched only
|
||||
# when the engine is gone and the install did not happen.
|
||||
param([string]$Mode, [int]$EnginePid, [string]$Installer, [string]$Version, [string]$Result, [string]$InstallDir, [string]$Sha256 = '')
|
||||
$log = Join-Path (Split-Path -Parent $Result) 'ota-apply.log'
|
||||
function Log([string]$t) { Add-Content -Path $log -Value ("{0} {1}" -f (Get-Date -Format s), $t) }
|
||||
function Done([bool]$ok, [string]$err, [bool]$rb) {
|
||||
$o = @{ ok = $ok; version = $Version; error = $err; rolled_back = $rb; at = [int][double](Get-Date -UFormat %s) }
|
||||
function Done([bool]$ok, [string]$err, [bool]$rb, [bool]$deferred) {
|
||||
$o = @{ ok = $ok; version = $Version; error = $err; rolled_back = $rb; deferred = $deferred; at = [int][double](Get-Date -UFormat %s) }
|
||||
($o | ConvertTo-Json -Compress) | Set-Content -Path $Result -Encoding ASCII
|
||||
}
|
||||
function EngineAlive() { return [bool](Get-Process -Id $EnginePid -ErrorAction SilentlyContinue) }
|
||||
function Relaunch() {
|
||||
if (EngineAlive) { return }
|
||||
$exe = Join-Path $InstallDir 'igneum-app.exe'
|
||||
if (Test-Path $exe) { Start-Process -FilePath $exe -ArgumentList '--launch' -WorkingDirectory $InstallDir | Out-Null }
|
||||
if (Test-Path $exe) { Log 'engine gone and nothing installed: starting the old app again'; Start-Process -FilePath $exe -ArgumentList '--launch' -WorkingDirectory $InstallDir | Out-Null }
|
||||
}
|
||||
Log "$Mode : engine $EnginePid installer '$Installer' version $Version"
|
||||
$deadline = (Get-Date).AddSeconds(120)
|
||||
while ((Get-Date) -lt $deadline -and (Get-Process -Id $EnginePid -ErrorAction SilentlyContinue)) { Start-Sleep -Milliseconds 500 }
|
||||
if (Get-Process -Id $EnginePid -ErrorAction SilentlyContinue) { Log 'engine still running; ending it'; Stop-Process -Id $EnginePid -Force -ErrorAction SilentlyContinue }
|
||||
if (-not (Test-Path $Installer)) { Log 'installer missing'; Done $false 'the downloaded installer is missing' $false; Relaunch; exit 1 }
|
||||
# the installer is hashed again right before it runs as administrator (R4.3.5)
|
||||
if (-not $Sha256) { Log 'no sha256 given'; Done $false 'no sha256 for the installer; not run' $false; Relaunch; exit 1 }
|
||||
Log "$Mode : engine $EnginePid installer '$Installer' version $Version (the engine keeps mining until the installer runs)"
|
||||
if (-not (Test-Path $Installer)) { Log 'installer missing'; Done $false 'the downloaded installer is missing' $false $false; exit 1 }
|
||||
# the installer is hashed again right before it runs (R4.3.5)
|
||||
if (-not $Sha256) { Log 'no sha256 given'; Done $false 'no sha256 for the installer; not run' $false $false; exit 1 }
|
||||
$have = (Get-FileHash -Path $Installer -Algorithm SHA256).Hash.ToLower()
|
||||
if ($have -ne $Sha256.ToLower()) { Log "sha256 mismatch: $have"; Remove-Item -Path $Installer -Force -ErrorAction SilentlyContinue; Done $false 'the installer changed since it was verified; not run' $false; Relaunch; exit 1 }
|
||||
if ($have -ne $Sha256.ToLower()) { Log "sha256 mismatch: $have"; Remove-Item -Path $Installer -Force -ErrorAction SilentlyContinue; Done $false 'the installer changed since it was verified; not run' $false $false; exit 1 }
|
||||
Log 'installer sha256 verified'
|
||||
$setupLog = Join-Path (Split-Path -Parent $Result) 'ota-setup.log'
|
||||
$setupArgs = @('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/CLOSEAPPLICATIONS', '/IGNOTA=1', ('/LOG="' + $setupLog + '"'))
|
||||
try {
|
||||
$args = @('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/CLOSEAPPLICATIONS', '/IGNOTA=1', ('/LOG="' + $setupLog + '"'))
|
||||
$p = Start-Process -FilePath $Installer -ArgumentList $args -Verb RunAs -Wait -PassThru
|
||||
# no -Verb RunAs: a per-user installer just runs; an administrator installer makes Windows ask, and a declined or
|
||||
# timed-out prompt comes back here as an exception with the engine still mining
|
||||
$p = Start-Process -FilePath $Installer -ArgumentList $setupArgs -Wait -PassThru
|
||||
if ($p.ExitCode -eq 0) {
|
||||
if ($Mode -eq 'rollback') { Done $false "Igneum Miner $Version did not stay up twice; the previous version was reinstalled" $true }
|
||||
else { Done $true '' $false }
|
||||
Log "installer exit 0"
|
||||
if ($Mode -eq 'rollback') { Done $false "Igneum Miner $Version did not stay up twice; the previous version was reinstalled" $true $false }
|
||||
else { Done $true '' $false $false }
|
||||
Log 'installer exit 0'
|
||||
exit 0
|
||||
}
|
||||
Log ("installer exit " + $p.ExitCode)
|
||||
Done $false ("the installer exited with code " + $p.ExitCode + " (see ota-setup.log)") $false
|
||||
Done $false ("the installer exited with code " + $p.ExitCode + " (see ota-setup.log)") $false $false
|
||||
Relaunch
|
||||
exit 1
|
||||
} catch {
|
||||
Log ("installer did not run: " + $_.Exception.Message)
|
||||
Done $false ("Windows did not let the installer run: " + $_.Exception.Message) $false
|
||||
$msg = $_.Exception.Message
|
||||
Log ("installer did not run: " + $msg)
|
||||
Log 'OTA: waiting for administrator approval; the engine keeps mining; the update waits for the next time someone is at this PC'
|
||||
Done $false ("waiting for administrator approval (" + $msg + ")") $false $true
|
||||
Relaunch
|
||||
exit 1
|
||||
}
|
||||
Relaunch
|
||||
exit 1
|
||||
"#;
|
||||
|
|
|
|||
|
|
@ -434,7 +434,8 @@
|
|||
case 'downloading': return { text: 'Downloading ' + v + (u.size ? ' (' + Math.round(u.size / 1e6) + ' MB)' : '') + ': ' + Math.round((u.progress || 0) * 100) + '%', prog: true };
|
||||
case 'staging': return { text: v + ' downloaded and verified. Preparing it.' };
|
||||
case 'ready': return { text: v + ' is ready. ' + (u.wait ? cap(u.wait) + '.' : 'It installs at the next safe moment.'), install: true };
|
||||
case 'applying': return { text: 'Installing ' + v + ': the miners stop, then the node, then the app opens again.' };
|
||||
case 'applying': return { text: 'Installing ' + v + ': ' + (u.wait ? u.wait + '.' : 'the miners stop, then the node, then the app opens again.') };
|
||||
case 'deferred': return { text: v + ' is downloaded. Windows asked for permission and nobody answered; it installs the next time someone is at this PC. Mining continues.', install: true };
|
||||
case 'manual': return { text: v + ' is downloaded. ' + cap(u.wait || 'open the download and drag the app over the old one.'), open: true };
|
||||
case 'error': return { text: 'Update: ' + (u.error || 'failed') + '.', install: !!(u.ready || u.downloaded) };
|
||||
default: return null;
|
||||
|
|
@ -472,6 +473,7 @@
|
|||
if (kind === 'downloading') { u.status = 'downloading'; u.downloaded = false; u.ready = false; u.progress = 0.43; }
|
||||
if (kind === 'waiting') { u.wait = 'hourly program boundary in 97 s; installing after it'; }
|
||||
if (kind === 'applying') { u.status = 'applying'; u.applying = true; }
|
||||
if (kind === 'deferred') { u.status = 'deferred'; u.wait = 'waits for the next time someone is at this PC (Windows asks for permission); mining continues'; }
|
||||
if (kind === 'urgent') { u.urgent = true; u.activation_height = 120000; u.urgent_text = 'Consensus upgrade at height 120000 (difficulty v2): the node is 1,240 blocks away. Installing 0.3.1 now.'; }
|
||||
if (kind === 'manual') { u.status = 'manual'; u.ready = false; u.wait = '/Applications is not writable; open the downloaded disk image and drag the app over the old one'; }
|
||||
if (kind === 'error') { u.status = 'error'; u.error = 'sha256 mismatch: the file is not what the manifest signed'; u.downloaded = false; u.ready = false; }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
// packaging/windows/Igneum-Miner.iss when the app version moves. Include guards, not #pragma once: rc.exe reads it too.
|
||||
#ifndef IGNEUM_HOST_VERSION_H
|
||||
#define IGNEUM_HOST_VERSION_H
|
||||
#define IGNEUM_HOST_VERSION_STR "0.3.2"
|
||||
#define IGNEUM_HOST_VERSION_RC 0,3,2,0
|
||||
#define IGNEUM_HOST_VERSION_STR "0.3.3"
|
||||
#define IGNEUM_HOST_VERSION_RC 0,3,3,0
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -68,21 +68,44 @@ running bundle (same volume: the swap is two renames), run its engine with `--ve
|
|||
version. When the folder is not writable the state is `manual`: the banner says so and offers "Open the download".
|
||||
Windows: the installer is the staged file.
|
||||
|
||||
Safe moment (`manifest::safe_to_apply`): node synced, no hourly program boundary within 180 s (`program.eta_s`),
|
||||
no worker starting. Urgent (fork within 1,800 blocks, or unsupported version, or Install now) skips the wait. A
|
||||
ready update that found no safe moment for 6 hours applies anyway (an unsynced node mines nothing).
|
||||
Safe moment (`manifest::safe_to_apply`): the network has not lost over 30% of its identities in 10 minutes, this
|
||||
minute is the machine's slot, node synced, no hourly program boundary within 180 s (`program.eta_s`), no worker
|
||||
starting. Urgent (fork within 1,800 blocks, or unsupported version, or Install now) skips the wait. A ready update
|
||||
that found no safe moment for 6 hours applies anyway in its slot (an unsynced node mines nothing).
|
||||
|
||||
Apply. The engine writes `update-pending.json` (from, to, starts), starts the helper detached and leaves through its
|
||||
normal quit path: miners first (8 s grace), then the node (30 s), the last log upload, `EXIT` for the window.
|
||||
- macOS helper `ota-apply.sh`: waits for the engine, asks the window (`network.igneum.miner`) to quit, moves the
|
||||
bundle to `Igneum Miner.app.previous`, the staged one in, strips quarantine, `open -n`. If the new engine is not
|
||||
running after 30 s it opens once more; if that fails too it puts `.previous` back and reports.
|
||||
- Windows helper `ota-apply.ps1`: waits for the engine, runs `Igneum-Miner-Setup-<v>.exe /VERYSILENT
|
||||
/SUPPRESSMSGBOXES /NORESTART /CLOSEAPPLICATIONS /IGNOTA=1 /LOG=...` as administrator (ONE UAC prompt: the
|
||||
installer is `PrivilegesRequired=admin` because of Program Files and the firewall rule). The installer's
|
||||
`PrepareToInstall` runs `stop-igneum.ps1` (ends the window and anything left), replaces the files, and the
|
||||
`[Run]` entry on `/IGNOTA=1` relaunches `igneum-app.exe --launch` as the signed-in user. A declined prompt or a
|
||||
non-zero exit relaunches the old app and reports the error in the banner (Install now retries).
|
||||
- Windows helper `ota-apply.ps1` (0.3.3, after the 4 October incident below): runs `Igneum-Miner-Setup-<v>.exe
|
||||
/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /CLOSEAPPLICATIONS /IGNOTA=1 /LOG=...` FIRST, with the engine still
|
||||
mining. The installer is per-user since 0.3.3 (`PrivilegesRequired=lowest`,
|
||||
`{localappdata}\Programs\Igneum Miner`), so nothing asks for an administrator; its `PrepareToInstall` runs
|
||||
`stop-igneum.ps1` (api/quit: miners first, then the node, then the window), replaces the files, and the `[Run]`
|
||||
entry on `/IGNOTA=1` relaunches `igneum-app.exe --launch`. An older install in Program Files (0.3.2 and before)
|
||||
still raises one UAC prompt through ShellExecute: declined, timed out or unanswered, the helper writes
|
||||
`deferred:true`, the engine keeps mining, logs `OTA: waiting for administrator approval` / `OTA: administrator
|
||||
approval not given ...; mining continues`, shows "waits for the next time someone is at this PC" and retries on
|
||||
Install now, at the next start, or after 6 hours. The engine is never stopped before the installer is running.
|
||||
- Machines take turns: an update applies only in the machine's own minute of the hour (`manifest::slot_minute`:
|
||||
the first 8 hex of the machine id modulo 60; PC 2 = :58), and never while `/api/live` shows the network lost
|
||||
over 30% of its identities (`state.miners_10m`) in the last 10 minutes. Urgent (fork close, unsupported) and
|
||||
Install now skip both.
|
||||
- Per-user install, migration: the data (`%LOCALAPPDATA%\igneum`: chain, wallet, settings) is the same folder for
|
||||
both installs, nothing is copied. A hand-run installer offers to remove the Program Files copy (one
|
||||
administrator prompt for its uninstaller); a silent OTA install leaves it and says nothing. The login entry
|
||||
(HKCU Run) is rewritten to the new path on first start. The inbound firewall rule for `igneumd.exe` is requested
|
||||
once on the first run of a per-user install (one prompt, in a thread; declined or unanswered = the node dials out
|
||||
and mines without it, never asked again).
|
||||
|
||||
Field incident, 4 October 2026 15:40 BST: 0.3.2 published; both Windows PCs (0.3.1, nobody at either keyboard)
|
||||
reached apply, stopped the miners and the node, then sat at the installer's UAC prompt. The chain fell to one
|
||||
laptop. The 0.3.1 helper waits for the engine to exit before it runs the installer, so a prompt nobody answers
|
||||
strands the machine; 0.3.3 inverts the order (installer first, engine stops only when the installer runs) and the
|
||||
per-user installer removes the prompt altogether. The 0.3.2 engines still carry the old helper: their 0.3.3 update
|
||||
raises the prompt once more (with 0.3.2's R4.3.6 rule the version is then marked failed, not retried); click Yes
|
||||
once, or run the 0.3.3 installer by hand.
|
||||
|
||||
Rollback. The helper writes `update-result.json`; the new engine reads it on start and reports "updated to X from
|
||||
Y" or the error. The new engine counts its starts in `update-pending.json` and deletes the file after 90 healthy
|
||||
|
|
|
|||
|
|
@ -3,6 +3,18 @@
|
|||
The Windows apply path could not be run from the Mac. It was reviewed against `packaging/windows/Igneum-Miner.iss`,
|
||||
`stop-igneum.ps1` and `app/windows/host.cpp`; these steps run it for real. Allow 20 minutes.
|
||||
|
||||
## 0.3.3 (after the 4 October incident): what changed and what to check first
|
||||
|
||||
The installer is per-user (`%LOCALAPPDATA%\Programs\Igneum Miner`, no administrator prompt) and the updater runs
|
||||
the installer BEFORE it stops anything. On a PC still on 0.3.2 (installed in Program Files) the 0.3.2 engine's old
|
||||
helper stops the miners and raises the prompt once more for the 0.3.3 installer: click Yes when it appears, or run
|
||||
`Igneum-Miner-Setup-0.3.3.exe` by hand (it stops the old app, installs per user, offers to remove the Program Files
|
||||
copy with one administrator prompt, starts the new app). After that no update ever asks again.
|
||||
|
||||
Check on the first 0.3.3 start: Settings shows 0.3.3; the log has `update slot: minute 58 of every hour` (PC 2) and
|
||||
`firewall: asking once for administrator approval ...` (answer Yes once, or ignore: mining does not wait);
|
||||
`%LOCALAPPDATA%\igneum\app\firewall-rule.json` exists afterwards. The Start Menu entry opens the new copy.
|
||||
|
||||
## What is untested on Windows
|
||||
|
||||
- `ota-apply.ps1` end to end: the wait for the engine, `Start-Process -Verb RunAs` of the installer, the UAC prompt,
|
||||
|
|
@ -11,6 +23,13 @@ The Windows apply path could not be run from the Mac. It was reviewed against `p
|
|||
Manager cannot close it; `PrepareToInstall` (`stop-igneum.ps1`, `Stop-Process -Force` on "Igneum Miner") is what
|
||||
ends it. Watch for an installer dialog about files in use.
|
||||
- The rollback: the previous installer is kept in `updates/` only from the second OTA on; a first update has none.
|
||||
- The deferral path end to end: `Start-Process` without `-Verb RunAs` on an administrator installer, the exception
|
||||
on a declined or timed-out prompt, `deferred:true` in `update-result.json`, the engine's "OTA: administrator
|
||||
approval not given" line and the banner, the 6-hour retry. (Only reachable while an install is still in Program
|
||||
Files; a per-user install never prompts.)
|
||||
- The per-user installer over a Program Files install: the stop script from the old folder, the "remove the older
|
||||
copy?" question, the HKCU Run entry rewritten, two Start Menu entries until the old copy goes.
|
||||
- The first-run firewall prompt and `firewall-rule.json`.
|
||||
- `powershell` 5.1 parsing of the helper (`tools/ci/windows/check-ps51.ps1` cannot see it: it is a string in
|
||||
`src/ota.rs`). The helper avoids `"$x: y"` and uses nothing newer than 5.1.
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
#define AppName "Igneum Miner"
|
||||
#define Publisher "Igneum"
|
||||
#define Url "https://igneum.network"
|
||||
#define FirewallRule "Igneum Miner node"
|
||||
|
||||
[Setup]
|
||||
AppId={{A4C1F0E2-6B8D-4E7A-9F31-2C5D8E7B9A01}
|
||||
|
|
@ -30,7 +29,7 @@ VersionInfoVersion={#AppVersion}.0
|
|||
VersionInfoCompany={#Publisher}
|
||||
VersionInfoProductName={#AppName}
|
||||
VersionInfoDescription={#AppName} Setup
|
||||
DefaultDirName={autopf}\{#AppName}
|
||||
DefaultDirName={localappdata}\Programs\{#AppName}
|
||||
DefaultGroupName={#AppName}
|
||||
DisableProgramGroupPage=yes
|
||||
LicenseFile=LICENSE.txt
|
||||
|
|
@ -46,7 +45,9 @@ Compression=lzma2/max
|
|||
SolidCompression=yes
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
PrivilegesRequired=admin
|
||||
; Per user since 0.3.3 (4 October 2026): no administrator prompt, so the app's own updater can install unattended
|
||||
; (two PCs sat stopped at a UAC prompt for an hour). The firewall rule moved to the app's first run (src/ota.rs).
|
||||
PrivilegesRequired=lowest
|
||||
MinVersion=10.0
|
||||
CloseApplications=yes
|
||||
RestartApplications=no
|
||||
|
|
@ -60,7 +61,6 @@ english.FinishedHeadingLabel=Igneum Miner is installed
|
|||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
|
||||
Name: "firewall"; Description: "Let other Igneum nodes connect to this PC (Windows Firewall rule for igneumd.exe on private networks)"; GroupDescription: "Network:"
|
||||
|
||||
;; No [Dirs] entry: {app} stays read-only for users (R4.3.3). The engine writes under %LOCALAPPDATA%\igneum, and
|
||||
;; the fallback worker build copies the sources there first.
|
||||
|
|
@ -68,6 +68,7 @@ Name: "firewall"; Description: "Let other Igneum nodes connect to this PC (Windo
|
|||
[Files]
|
||||
Source: "{#Payload}\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion; Excludes: "*.log,*.seeds,*.DS_Store,packs\*,build\*,dist\*"
|
||||
Source: "{#ArtDir}\igneum.ico"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#Payload}\stop-igneum.ps1"; Flags: dontcopy
|
||||
Source: "wrappers\Stop Igneum Miner.cmd"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
|
|
@ -80,8 +81,7 @@ Name: "{group}\Uninstall Igneum Miner"; Filename: "{uninstallexe}"; IconFilename
|
|||
Name: "{autodesktop}\Igneum Miner"; Filename: "{app}\igneum-app.exe"; Parameters: "--launch"; WorkingDir: "{app}"; IconFilename: "{app}\igneum.ico"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "netsh.exe"; Parameters: "advfirewall firewall delete rule name=""{#FirewallRule}"""; Flags: runhidden; Tasks: firewall
|
||||
Filename: "netsh.exe"; Parameters: "advfirewall firewall add rule name=""{#FirewallRule}"" dir=in action=allow enable=yes profile=private,domain protocol=TCP program=""{app}\igneumd.exe"""; Flags: runhidden; Tasks: firewall; StatusMsg: "Adding the firewall rule for the node"
|
||||
; The inbound firewall rule needs an administrator and is asked for once by the app on its first run (declined = the node dials out and mines without it).
|
||||
; Started as the signed-in user, not as administrator (the data lands in that user's %LOCALAPPDATA%).
|
||||
Filename: "{app}\igneum-app.exe"; Parameters: "--launch"; Description: "Start Igneum Miner now"; Flags: postinstall nowait skipifsilent runasoriginaluser
|
||||
; The over-the-air updater (packaging/ota, src/ota.rs) runs this installer /VERYSILENT /IGNOTA=1 and the app must come back by itself.
|
||||
|
|
@ -89,7 +89,6 @@ Filename: "{app}\igneum-app.exe"; Parameters: "--launch"; Flags: nowait runasori
|
|||
|
||||
[UninstallRun]
|
||||
Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\stop-igneum.ps1"""; Flags: runhidden waituntilterminated; RunOnceId: "StopIgneum"
|
||||
Filename: "netsh.exe"; Parameters: "advfirewall firewall delete rule name=""{#FirewallRule}"""; Flags: runhidden; RunOnceId: "FirewallRule"
|
||||
|
||||
[UninstallDelete]
|
||||
; The GPU workers built on this PC and the WebView2 cache are not in the install log; remove them with the folder.
|
||||
|
|
@ -102,16 +101,41 @@ begin
|
|||
Result := ExpandConstant('{param:IGNOTA|0}') = '1';
|
||||
end;
|
||||
|
||||
// Stops a running copy before the files are replaced (an upgrade over a running miner).
|
||||
// The 0.3.2-and-earlier install in Program Files (administrator), when this per-user install lands on top of it.
|
||||
function OldAdminInstallDir: String;
|
||||
begin
|
||||
Result := ExpandConstant('{commonpf}\Igneum Miner');
|
||||
if not FileExists(Result + '\igneum-app.exe') then
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
// Stops a running copy before the files are replaced (an upgrade over a running miner): the old copy's own
|
||||
// stop-igneum.ps1 when one is installed (Program Files or here), else ours from the payload. Then, when someone is at
|
||||
// the keyboard, offers to remove the Program Files copy (its uninstaller needs one administrator prompt; the data in
|
||||
// %LOCALAPPDATA%\igneum is the same for both, nothing to copy). A silent (over-the-air) install never asks.
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
var
|
||||
StopScript: String;
|
||||
StopScript, OldDir: String;
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := '';
|
||||
OldDir := OldAdminInstallDir;
|
||||
StopScript := ExpandConstant('{app}\stop-igneum.ps1');
|
||||
if (not FileExists(StopScript)) and (OldDir <> '') then
|
||||
StopScript := OldDir + '\stop-igneum.ps1';
|
||||
if not FileExists(StopScript) then
|
||||
StopScript := ExpandConstant('{tmp}\stop-igneum.ps1');
|
||||
if not FileExists(StopScript) then
|
||||
ExtractTemporaryFile('stop-igneum.ps1');
|
||||
if FileExists(StopScript) then
|
||||
Exec('powershell.exe', '-NoProfile -ExecutionPolicy Bypass -File "' + StopScript + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
if (OldDir <> '') and (not WizardSilent) and FileExists(OldDir + '\unins000.exe') then
|
||||
begin
|
||||
if MsgBox('Igneum Miner now installs in your user folder, so updates need no administrator prompt.' + #13#10#13#10 +
|
||||
'An older copy is still in ' + OldDir + '. Remove it now? (one administrator prompt; your chain data, address and settings stay)',
|
||||
mbConfirmation, MB_YESNO) = IDYES then
|
||||
ShellExec('runas', OldDir + '\unins000.exe', '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CurPageChanged(CurPageID: Integer);
|
||||
|
|
|
|||
|
|
@ -63,8 +63,11 @@ prints the deploy command, or deploys with `--deploy`.
|
|||
|
||||
The installed app updates itself: `packaging/ota/README.md`. `fetch-ci-artifacts.sh` adds the installer it copies to
|
||||
the signed manifest (`igneum-app-latest.json`), `--deploy` ships both, and every app downloads the installer within
|
||||
the hour and runs it `/VERYSILENT /IGNOTA=1` at a safe moment (one UAC prompt; `Igneum-Miner.iss` has
|
||||
`CloseApplications=yes` and a `[Run]` relaunch for that flag). PC 2 steps: `packaging/ota/TEST.md`. The console
|
||||
the hour and runs it `/VERYSILENT /IGNOTA=1` in its own minute of the hour (`Igneum-Miner.iss` has
|
||||
`CloseApplications=yes` and a `[Run]` relaunch for that flag). Since 0.3.3 the installer is per user
|
||||
(`PrivilegesRequired=lowest`, `%LOCALAPPDATA%\Programs\Igneum Miner`): no administrator prompt, ever, for an
|
||||
update (4 October 2026: two unattended PCs sat at a UAC prompt for an hour); the inbound firewall rule is asked for
|
||||
once by the app on its first run. PC 2 steps: `packaging/ota/TEST.md`. The console
|
||||
launcher packages (`proto-cuda/windows-app`, `igneum-windows-v4.zip`) are not auto-updated, by design: they are the
|
||||
engineering path and are replaced by hand.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue