From 007d43ee1a8d3f37b7e6b566decbfce9605b0d88 Mon Sep 17 00:00:00 2001 From: npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz> Date: Sat, 1 Aug 2026 13:24:27 -0400 Subject: [PATCH] fix: restart exited desktop relay sidecar Signed-off-by: npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz> --- desktop/src-tauri/src/local_relay.rs | 42 +++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/desktop/src-tauri/src/local_relay.rs b/desktop/src-tauri/src/local_relay.rs index f81ae0bd6..8dd8bcff0 100644 --- a/desktop/src-tauri/src/local_relay.rs +++ b/desktop/src-tauri/src/local_relay.rs @@ -64,6 +64,13 @@ impl LocalRelayRuntime { &self.url } + fn is_running(&mut self) -> Result { + self.child + .try_wait() + .map(|status| status.is_none()) + .map_err(|error| format!("inspect local relay: {error}")) + } + fn stop(&mut self) { // The relay handles SIGTERM with a WebSocket drain. It is our child, so // waiting here prevents a restart from inheriting a stale listener. @@ -161,19 +168,27 @@ pub(crate) fn ensure_started( requested_port: Option, ) -> Result { let mut runtime = runtime.lock().map_err(|error| error.to_string())?; - if let Some(running) = runtime.as_ref() { + if let Some(running) = runtime.as_mut() { let same_port = requested_port.is_none_or(|port| running.url == local_relay_url(port)); let same_owner = running.owner_pubkey == keys.public_key().to_hex(); - if same_port && same_owner { + let is_running = running.is_running()?; + if same_port && same_owner && is_running { return Ok(running.url().to_string()); } if !same_owner { // An onboarding identity import changed the owner. Its local relay // must use a separate identity-scoped nest, not retain the prior - // identity's private sidecar. + // identity's private sidecar. `try_wait` above already reaped an + // exited child, so never signal its potentially recycled PID. if let Some(mut previous) = runtime.take() { - previous.stop(); + if is_running { + previous.stop(); + } } + } else if !is_running { + // `try_wait` reaped the crashed child. Drop its stale handle so the + // replacement below receives a fresh loopback port and child. + runtime.take(); } else { return Err("a different local relay port is already running".to_string()); } @@ -287,6 +302,7 @@ mod tests { collections::HashMap, net::{IpAddr, Ipv4Addr, SocketAddr}, path::{Path, PathBuf}, + process::{Command, Stdio}, }; #[test] @@ -306,6 +322,24 @@ mod tests { assert!(!is_local_relay_url("wss://127.0.0.1:4317")); } + #[test] + fn exited_relay_is_not_running() { + let child = Command::new(std::env::current_exe().expect("test executable")) + .arg("--help") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("start test child"); + let mut runtime = super::LocalRelayRuntime { + child, + url: "ws://127.0.0.1:4317".to_string(), + owner_pubkey: "owner".to_string(), + }; + + runtime.child.wait().expect("wait for test child"); + assert!(!runtime.is_running().expect("inspect exited child")); + } + #[test] fn relay_service_key_is_durable_and_distinct_from_owner_identity() { let directory = tempfile::tempdir().expect("temp data dir");