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
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.
This commit is contained in:
1 parent
f67a0a30b4
commit
594a33a302
1 file changed
+70
-33
+70
-33
@@ -140,9 +140,12 @@ const TELEMETRY_OFF: &str = "telemetry-enabled = false";
|
|||||||
const FIRST_BOOT_WAIT: Duration = Duration::from_secs(20);
|
const FIRST_BOOT_WAIT: Duration = Duration::from_secs(20);
|
||||||
/// What the node returns when it has spotted a newer release and wants one.
|
/// What the node returns when it has spotted a newer release and wants one.
|
||||||
const EXIT_UPDATE_NEEDED: i32 = 42;
|
const EXIT_UPDATE_NEEDED: i32 = 42;
|
||||||
/// How long to watch a fresh child for that exit. The check runs at startup,
|
/// How long to keep watching a fresh peer for that exit. The check is a
|
||||||
/// before any network work, so this only has to outlast process spawn.
|
/// network call the node makes at startup, and it takes as long as the
|
||||||
const UPDATE_EXIT_WAIT: Duration = Duration::from_secs(2);
|
/// 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 {
|
struct NodeDirs {
|
||||||
base: PathBuf,
|
base: PathBuf,
|
||||||
@@ -236,7 +239,7 @@ fn spawn_node(bin: &Path, dirs: &NodeDirs, log: &Path) -> Result<Child, String>
|
|||||||
.map_err(|e| format!("start the peer: {e}"))
|
.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
|
/// 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
|
/// 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<Child, String>
|
|||||||
/// permanently absent with only a line in node.log to say why. One update, one
|
/// 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
|
/// restart, no recursion — if the fresh binary asks again we let it be, rather
|
||||||
/// than loop.
|
/// than loop.
|
||||||
fn spawn_supervised(bin: &Path, dirs: &NodeDirs, log: &Path) -> Result<Child, String> {
|
///
|
||||||
let mut child = spawn_node(bin, dirs, log)?;
|
/// Watches the peer that `start_node_inner` just put in `freenet_node`, from
|
||||||
let deadline = Instant::now() + UPDATE_EXIT_WAIT;
|
/// its own thread, polling under the lock rather than owning the child — the
|
||||||
while Instant::now() < deadline {
|
/// child belongs to the state, and the Stop button takes it. An update is
|
||||||
match child.try_wait() {
|
/// applied and the peer restarted while the lock is held, so nothing reads
|
||||||
Ok(Some(status)) if status.code() == Some(EXIT_UPDATE_NEEDED) => {
|
/// the dead child as "stopped" and starts a second peer in the meantime. Any
|
||||||
log::warn!("freenet node: exit 42 — a newer release is out; applying it");
|
/// other exit is logged and the slot emptied; an emptied slot ends the watch.
|
||||||
apply_update(bin)?;
|
fn watch_for_update(app: tauri::AppHandle, bin: PathBuf, dirs: NodeDirs, log: PathBuf) {
|
||||||
let child = spawn_node(bin, dirs, log)?;
|
std::thread::spawn(move || {
|
||||||
log::info!("freenet node: updated and restarted (pid {})", child.id());
|
let deadline = Instant::now() + UPDATE_EXIT_WATCH;
|
||||||
return Ok(child);
|
while Instant::now() < deadline {
|
||||||
|
std::thread::sleep(Duration::from_millis(500));
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
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
|
/// 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
|
/// Start the peer under the app's folder (the first-boot recipe when its
|
||||||
/// config does not exist yet). Returns the running child.
|
/// config does not exist yet), put it in `freenet_node`, and leave the
|
||||||
async fn start_node_inner(app: &tauri::AppHandle) -> Result<Child, String> {
|
/// supervisor watching it.
|
||||||
|
async fn start_node_inner(app: &tauri::AppHandle) -> Result<(), String> {
|
||||||
let handle = app.clone();
|
let handle = app.clone();
|
||||||
tauri::async_runtime::spawn_blocking(move || -> Result<Child, String> {
|
tauri::async_runtime::spawn_blocking(move || -> Result<(), String> {
|
||||||
let bin = find_binary(&handle)
|
let bin = find_binary(&handle)
|
||||||
.ok_or("no Freenet peer found on this machine — install it (cargo install freenet) and try again")?;
|
.ok_or("no Freenet peer found on this machine — install it (cargo install freenet) and try again")?;
|
||||||
let dirs = node_dirs(&handle)?;
|
let dirs = node_dirs(&handle)?;
|
||||||
@@ -333,10 +357,22 @@ async fn start_node_inner(app: &tauri::AppHandle) -> Result<Child, String> {
|
|||||||
let config = dirs.config.join("config.toml");
|
let config = dirs.config.join("config.toml");
|
||||||
if !config.is_file() {
|
if !config.is_file() {
|
||||||
let first_log = dirs.base.join("first-boot.log");
|
let first_log = dirs.base.join("first-boot.log");
|
||||||
let mut first = spawn_supervised(&bin, &dirs, &first_log)?;
|
let mut first = spawn_node(&bin, &dirs, &first_log)?;
|
||||||
let deadline = Instant::now() + FIRST_BOOT_WAIT;
|
let mut deadline = Instant::now() + FIRST_BOOT_WAIT;
|
||||||
|
let mut updated = false;
|
||||||
while !config.is_file() && Instant::now() < deadline {
|
while !config.is_file() && Instant::now() < deadline {
|
||||||
if let Ok(Some(status)) = first.try_wait() {
|
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!(
|
return Err(format!(
|
||||||
"the peer exited during its first boot ({status}) — see {}",
|
"the peer exited during its first boot ({status}) — see {}",
|
||||||
first_log.display()
|
first_log.display()
|
||||||
@@ -352,9 +388,12 @@ async fn start_node_inner(app: &tauri::AppHandle) -> Result<Child, String> {
|
|||||||
log::info!("freenet node: first boot wrote {}", config.display());
|
log::info!("freenet node: first boot wrote {}", config.display());
|
||||||
}
|
}
|
||||||
pin_telemetry_off(&config)?;
|
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());
|
log::info!("freenet node started (pid {}) from {}", child.id(), bin.display());
|
||||||
Ok(child)
|
*handle.state::<AppState>().freenet_node.lock().unwrap() = Some(child);
|
||||||
|
watch_for_update(handle.clone(), bin, dirs, log);
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?
|
.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) {
|
if !matches!(state.freenet.detect().await, freenet_adapter::AdapterStatus::NodeAbsent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match start_node_inner(&app).await {
|
if let Err(e) = start_node_inner(&app).await {
|
||||||
Ok(child) => *state.freenet_node.lock().unwrap() = Some(child),
|
log::warn!("freenet node: could not start the peer: {e}");
|
||||||
Err(e) => 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 {
|
if node_info(&app, &state)?.started_by_app {
|
||||||
return node_info(&app, &state);
|
return node_info(&app, &state);
|
||||||
}
|
}
|
||||||
let child = start_node_inner(&app).await?;
|
start_node_inner(&app).await?;
|
||||||
*state.freenet_node.lock().unwrap() = Some(child);
|
|
||||||
node_info(&app, &state)
|
node_info(&app, &state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user