From 365f3d7d188b674e1b516443bb5ff18bb753fdf6 Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 27 Jul 2026 07:29:54 +0100 Subject: [PATCH] fix(install): mesh app-port relay no longer kills the daemon on install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v6 app-port relay preemptively bound [::]: for ALL catalog ports, even apps not installed. Installing such an app (grafana:3000, photoprism, uptime-kuma, jellyfin — framework-pt 2026-07-27) then hit 'address already in use' from the relay, which triggered cleanup_stale_pasta_port -> -> killed archipelago itself (it held the port) mid-install. The daemon crash-looped and the half-created apps were rolled back and vanished. Two fixes: - relay only bridges a port that a running app already answers on over IPv4 (probe 127.0.0.1:port first) — an uninstalled app's port is never held, so its install sees a free port and never triggers the cleanup. - cleanup_stale_pasta_port excludes our own PID from both the ss-based kill and the fuser kill, so freeing a port can never terminate the daemon even when the relay legitimately holds it (reinstall case). Co-Authored-By: Claude Fable 5 --- .../src/api/rpc/package/install.rs | 19 +++++++++++---- core/archipelago/src/server.rs | 24 +++++++++++++++++-- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index 984e0132..589b0b09 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -2324,17 +2324,28 @@ async fn cleanup_start_conflict(package_id: &str, stderr: &str) -> bool { } async fn cleanup_stale_pasta_port(port: &str) { + // NEVER kill our own process. The daemon holds catalog app ports over + // IPv6 (the mesh app-port relay), so a blunt `fuser -k /tcp` would + // terminate archipelago itself mid-install — installs failed and apps + // vanished on framework-pt 2026-07-27. Kill every listener on the port + // EXCEPT our PID (and our process group), leaving the relay/daemon alive. + let self_pid = std::process::id(); let kill_listener = format!( - "ss -ltnp 'sport = :{}' 2>/dev/null | sed -n 's/.*pid=\\([0-9]*\\).*/\\1/p' | xargs -r kill 2>/dev/null || true", - port + "ss -ltnp 'sport = :{port}' 2>/dev/null | sed -n 's/.*pid=\\([0-9]*\\).*/\\1/p' | \ + while read p; do [ \"$p\" = \"{self_pid}\" ] || kill \"$p\" 2>/dev/null; done || true", ); let _ = tokio::process::Command::new("sh") .args(["-c", &kill_listener]) .output() .await; - let _ = tokio::process::Command::new("sudo") - .args(["fuser", "-k", &format!("{}/tcp", port)]) + // sudo fuser -k, but exclude our own PID: fuser prints the PIDs holding + // the port; kill each except self. (`fuser -k` has no exclusion flag.) + let fuser_kill = format!( + "for p in $(sudo fuser {port}/tcp 2>/dev/null); do [ \"$p\" = \"{self_pid}\" ] || sudo kill \"$p\" 2>/dev/null; done || true", + ); + let _ = tokio::process::Command::new("sh") + .args(["-c", &fuser_kill]) .output() .await; diff --git a/core/archipelago/src/server.rs b/core/archipelago/src/server.rs index 17806321..1d733fe1 100644 --- a/core/archipelago/src/server.rs +++ b/core/archipelago/src/server.rs @@ -1103,12 +1103,32 @@ async fn app_port_v6_relay_loop(mut shutdown_rx: tokio::sync::watch::Receiver/tcp`) then + // kills THIS daemon, which holds the port — the exact + // cause of installs failing + apps vanishing on + // framework-pt 2026-07-27. No v4 listener → skip; the + // next rescan picks it up once the app is up. + let v4_up = tokio::time::timeout( + std::time::Duration::from_millis(300), + tokio::net::TcpStream::connect(("127.0.0.1", port)), + ) + .await + .ok() + .and_then(|r| r.ok()) + .is_some(); + if !v4_up { + continue; + } let addr = SocketAddr::new( std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED), port, ); - // EADDRINUSE = the app itself (or a prior relay) already - // answers on v6 — exactly when we must stay out of the way. + // EADDRINUSE = the app itself already answers on v6 — + // exactly when we must stay out of the way. let Ok(listener) = bind_v6_only(addr) else { continue }; bridged.insert(port); debug!("v6 relay bridging [::]:{port} -> 127.0.0.1:{port}");