diff --git a/src-tauri/src/freenet.rs b/src-tauri/src/freenet.rs index aca223c..1aa0e9f 100644 --- a/src-tauri/src/freenet.rs +++ b/src-tauri/src/freenet.rs @@ -138,6 +138,11 @@ pub struct NodeInfo { const TELEMETRY_ON: &str = "telemetry-enabled = true"; const TELEMETRY_OFF: &str = "telemetry-enabled = false"; const FIRST_BOOT_WAIT: Duration = Duration::from_secs(20); +/// What the node returns when it has spotted a newer release and wants one. +const EXIT_UPDATE_NEEDED: i32 = 42; +/// How long to watch a fresh child for that exit. The check runs at startup, +/// before any network work, so this only has to outlast process spawn. +const UPDATE_EXIT_WAIT: Duration = Duration::from_secs(2); struct NodeDirs { base: PathBuf, @@ -226,6 +231,57 @@ fn spawn_node(bin: &Path, dirs: &NodeDirs, log: &Path) -> Result .map_err(|e| format!("start the peer: {e}")) } +/// Spawn the peer and be its supervisor. +/// +/// A node that spots a newer release exits with code 42 and waits for someone +/// to apply it; Freenet ships that someone as a system service. We cannot use +/// one: `freenet service install` takes no `--config-dir`/`--data-dir`, so its +/// node would run on the default folders — a different peer with none of this +/// app's identity — and fight ours for port 7509. The app owns the node, so +/// the app is the supervisor. +/// +/// Without this, an update simply ends the layer: 0.2.133 landed on +/// 2026-09-03 and the pinned 0.2.132 exited at every launch, leaving the node +/// permanently absent with only a line in node.log to say why. One update, one +/// restart, no recursion — if the fresh binary asks again we let it be, rather +/// than loop. +fn spawn_supervised(bin: &Path, dirs: &NodeDirs, log: &Path) -> Result { + let mut child = spawn_node(bin, dirs, log)?; + let deadline = Instant::now() + UPDATE_EXIT_WAIT; + while Instant::now() < deadline { + match child.try_wait() { + Ok(Some(status)) if status.code() == Some(EXIT_UPDATE_NEEDED) => { + log::warn!("freenet node: exit 42 — a newer release is out; applying it"); + apply_update(bin)?; + let child = spawn_node(bin, dirs, log)?; + log::info!("freenet node: updated and restarted (pid {})", child.id()); + return Ok(child); + } + // Any other early exit is the caller's problem to report, and a + // live child is the normal case. + Ok(Some(_)) | Err(_) => break, + Ok(None) => std::thread::sleep(Duration::from_millis(100)), + } + } + Ok(child) +} + +/// Run the node's own updater. Blocking — this whole path is already on a +/// blocking task, and the download takes as long as it takes. +fn apply_update(bin: &Path) -> Result<(), String> { + let out = quiet(Command::new(bin)) + .arg("update") + .output() + .map_err(|e| format!("could not run freenet update: {e}"))?; + if !out.status.success() { + return Err(format!( + "freenet update failed: {}", + String::from_utf8_lossy(&out.stderr).trim() + )); + } + Ok(()) +} + fn node_info(app: &tauri::AppHandle, state: &AppState) -> Result { let dirs = node_dirs(app)?; let bin = find_binary(app); @@ -272,7 +328,7 @@ async fn start_node_inner(app: &tauri::AppHandle) -> Result { let config = dirs.config.join("config.toml"); if !config.is_file() { let first_log = dirs.base.join("first-boot.log"); - let mut first = spawn_node(&bin, &dirs, &first_log)?; + let mut first = spawn_supervised(&bin, &dirs, &first_log)?; let deadline = Instant::now() + FIRST_BOOT_WAIT; while !config.is_file() && Instant::now() < deadline { if let Ok(Some(status)) = first.try_wait() { @@ -291,7 +347,7 @@ async fn start_node_inner(app: &tauri::AppHandle) -> Result { log::info!("freenet node: first boot wrote {}", config.display()); } pin_telemetry_off(&config)?; - let child = spawn_node(&bin, &dirs, &dirs.base.join("node.log"))?; + let child = spawn_supervised(&bin, &dirs, &dirs.base.join("node.log"))?; log::info!("freenet node started (pid {}) from {}", child.id(), bin.display()); Ok(child) })