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) }