the freenet layer: the app supervises its own peer, so a new release no longer quietly ends it

This commit is contained in:
KeelrLeek committed 2026-09-03 11:12:41 -05:00
1 parent 733e3b57a8
commit 07227a90a7
1 file changed
+58 -2
+58 -2
View File
@@ -138,6 +138,11 @@ pub struct NodeInfo {
const TELEMETRY_ON: &str = "telemetry-enabled = true"; const TELEMETRY_ON: &str = "telemetry-enabled = true";
const TELEMETRY_OFF: &str = "telemetry-enabled = false"; 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.
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 { struct NodeDirs {
base: PathBuf, base: PathBuf,
@@ -226,6 +231,57 @@ 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.
///
/// 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<Child, String> {
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<NodeInfo, String> { fn node_info(app: &tauri::AppHandle, state: &AppState) -> Result<NodeInfo, String> {
let dirs = node_dirs(app)?; let dirs = node_dirs(app)?;
let bin = find_binary(app); let bin = find_binary(app);
@@ -272,7 +328,7 @@ 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_node(&bin, &dirs, &first_log)?; let mut first = spawn_supervised(&bin, &dirs, &first_log)?;
let deadline = Instant::now() + FIRST_BOOT_WAIT; let deadline = Instant::now() + FIRST_BOOT_WAIT;
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() {
@@ -291,7 +347,7 @@ 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_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()); log::info!("freenet node started (pid {}) from {}", child.id(), bin.display());
Ok(child) Ok(child)
}) })