From 594a33a3021e82df098b28317c5b22bba4bc1de4 Mon Sep 17 00:00:00 2001 From: KeelrLeek <156356974+KeelrLeek@users.noreply.github.com> Date: Sun, 6 Sep 2026 08:17:55 -0500 Subject: [PATCH] freenet node: the update exit is watched from a thread for as long as the check takes, not for two seconds inside the start call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The supervisor that applies the node's update-wanted exit (code 42) watched a fresh child for two seconds and then let go. The exit is the result of a network check the node makes at startup, and it takes as long as the network takes: on a fresh install today a pinned 0.2.132 exited several seconds after launch, after the supervisor had already returned, and the layer read "node absent" with only node.log saying why — the exact failure the supervisor was written against, back again. `start_node_inner` now puts the child in `freenet_node` itself and leaves `watch_for_update` polling it from its own thread for 90 s, under the lock rather than owning the child — the child belongs to the state, and the Stop button takes it. An update is applied and the peer restarted while the lock is held, so nothing reads the dead child as "stopped" and starts a second peer meanwhile; any other exit is logged and the slot emptied; an emptied slot ends the watch. The first boot is the one launch the watcher cannot cover (its child never reaches the state), so that loop handles code 42 itself, once, with its clock restarted. Seen live on this machine: the other-exit branch caught a spawn that died on a busy port (code 43) within half a second and cleared the slot; a peer killed by hand and started again from the app was connected two seconds later. The exit-42 branch itself is not reproducible here any more — the binary is on the latest release — and stands on the same code path. --- src-tauri/src/freenet.rs | 103 ++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/src-tauri/src/freenet.rs b/src-tauri/src/freenet.rs index ce29a21..9e1e1f4 100644 --- a/src-tauri/src/freenet.rs +++ b/src-tauri/src/freenet.rs @@ -140,9 +140,12 @@ 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); +/// How long to keep watching a fresh peer for that exit. The check is a +/// network call the node makes at startup, and it takes as long as the +/// network takes: on 2026-09-06 a fresh 0.2.132 took more than the two +/// seconds this used to wait, exited after the supervisor had already gone, +/// and the layer read "node absent" with only node.log saying why. +const UPDATE_EXIT_WATCH: Duration = Duration::from_secs(90); struct NodeDirs { base: PathBuf, @@ -236,7 +239,7 @@ 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. +/// The peer's 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 @@ -250,25 +253,45 @@ fn spawn_node(bin: &Path, dirs: &NodeDirs, log: &Path) -> Result /// 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); +/// +/// Watches the peer that `start_node_inner` just put in `freenet_node`, from +/// its own thread, polling under the lock rather than owning the child — the +/// child belongs to the state, and the Stop button takes it. An update is +/// applied and the peer restarted while the lock is held, so nothing reads +/// the dead child as "stopped" and starts a second peer in the meantime. Any +/// other exit is logged and the slot emptied; an emptied slot ends the watch. +fn watch_for_update(app: tauri::AppHandle, bin: PathBuf, dirs: NodeDirs, log: PathBuf) { + std::thread::spawn(move || { + let deadline = Instant::now() + UPDATE_EXIT_WATCH; + while Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(500)); + let state = app.state::(); + let mut guard = state.freenet_node.lock().unwrap(); + match guard.as_mut().map(|c| c.try_wait()) { + Some(Ok(None)) => continue, + Some(Ok(Some(status))) if status.code() == Some(EXIT_UPDATE_NEEDED) => { + log::warn!("freenet node: exit 42 — a newer release is out; applying it"); + match apply_update(&bin).and_then(|()| spawn_node(&bin, &dirs, &log)) { + Ok(child) => { + log::info!("freenet node: updated and restarted (pid {})", child.id()); + *guard = Some(child); + } + Err(e) => { + log::warn!("freenet node: could not apply the update: {e}"); + *guard = None; + } + } + return; + } + Some(Ok(Some(status))) => { + log::warn!("freenet node: the peer exited ({status}) — see {}", log.display()); + *guard = None; + return; + } + Some(Err(_)) | None => return, } - // 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 @@ -321,10 +344,11 @@ pub fn freenet_node_info( } /// Start the peer under the app's folder (the first-boot recipe when its -/// config does not exist yet). Returns the running child. -async fn start_node_inner(app: &tauri::AppHandle) -> Result { +/// config does not exist yet), put it in `freenet_node`, and leave the +/// supervisor watching it. +async fn start_node_inner(app: &tauri::AppHandle) -> Result<(), String> { let handle = app.clone(); - tauri::async_runtime::spawn_blocking(move || -> Result { + tauri::async_runtime::spawn_blocking(move || -> Result<(), String> { let bin = find_binary(&handle) .ok_or("no Freenet peer found on this machine — install it (cargo install freenet) and try again")?; let dirs = node_dirs(&handle)?; @@ -333,10 +357,22 @@ 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_supervised(&bin, &dirs, &first_log)?; - let deadline = Instant::now() + FIRST_BOOT_WAIT; + let mut first = spawn_node(&bin, &dirs, &first_log)?; + let mut deadline = Instant::now() + FIRST_BOOT_WAIT; + let mut updated = false; while !config.is_file() && Instant::now() < deadline { if let Ok(Some(status)) = first.try_wait() { + // The first boot is the one launch the watcher cannot + // cover — its child is never in the state — so the update + // exit is handled here, once, with the clock restarted. + if status.code() == Some(EXIT_UPDATE_NEEDED) && !updated { + log::warn!("freenet node: exit 42 on first boot — a newer release is out; applying it"); + apply_update(&bin)?; + updated = true; + first = spawn_node(&bin, &dirs, &first_log)?; + deadline = Instant::now() + FIRST_BOOT_WAIT; + continue; + } return Err(format!( "the peer exited during its first boot ({status}) — see {}", first_log.display() @@ -352,9 +388,12 @@ 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_supervised(&bin, &dirs, &dirs.base.join("node.log"))?; + let log = dirs.base.join("node.log"); + let child = spawn_node(&bin, &dirs, &log)?; log::info!("freenet node started (pid {}) from {}", child.id(), bin.display()); - Ok(child) + *handle.state::().freenet_node.lock().unwrap() = Some(child); + watch_for_update(handle.clone(), bin, dirs, log); + Ok(()) }) .await .map_err(|e| e.to_string())? @@ -381,9 +420,8 @@ pub async fn ensure_node(app: tauri::AppHandle) { if !matches!(state.freenet.detect().await, freenet_adapter::AdapterStatus::NodeAbsent) { return; } - match start_node_inner(&app).await { - Ok(child) => *state.freenet_node.lock().unwrap() = Some(child), - Err(e) => log::warn!("freenet node: could not start the peer: {e}"), + if let Err(e) = start_node_inner(&app).await { + log::warn!("freenet node: could not start the peer: {e}"); } } @@ -395,8 +433,7 @@ pub async fn freenet_node_start( if node_info(&app, &state)?.started_by_app { return node_info(&app, &state); } - let child = start_node_inner(&app).await?; - *state.freenet_node.lock().unwrap() = Some(child); + start_node_inner(&app).await?; node_info(&app, &state) }