From 8321d093e847c4fcfcfba337656ff499e3924fbf Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 1 May 2026 14:59:11 -0400 Subject: [PATCH] fix(install): auto-clean stuck OTHER-variant bitcoin container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If bitcoin-core was installed but never started (e.g. port 8332 already bound by bitcoin-knots), the container sticks in `created` state forever. The old conflict check refused EVERY future bitcoin install — including re-install of the running variant — leaving no UI path to recovery. Now the check distinguishes states: - missing → no conflict, continue - running → real conflict, refuse install - created/exited/configured/... → stuck; auto-remove and continue Volumes are untouched; only the dead container record goes away. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/api/rpc/package/install.rs | 76 ++++++++++++------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index 7e750282..50cc7726 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -1799,40 +1799,64 @@ async fn check_bitcoin_implementation_conflict(package_id: &str) -> Result<()> { _ => return Ok(()), }; - let output = tokio::process::Command::new("podman") - .args([ - "ps", - "-a", - "--format", - "{{.Names}}", - "--filter", - &format!("name=^{}$", other), - ]) + // Three cases for the OTHER variant: + // - missing → no conflict, continue + // - running → real conflict, refuse install + // - any other state (created/exited/configured/...) → stuck from a + // prior failed install. Auto-remove so reinstall is reachable + // without a manual `podman rm`. This is what unblocks the .198 + // "bitcoin-core stuck in created, port 8332 held by bitcoin-knots" + // deadlock that no UI path could exit. + let inspect = tokio::process::Command::new("podman") + .args(["inspect", other, "--format", "{{.State.Status}}"]) .output() .await - .context("Failed to check existing Bitcoin node containers")?; - - if String::from_utf8_lossy(&output.stdout).trim().is_empty() { + .context("Failed to inspect conflicting Bitcoin container")?; + if !inspect.status.success() { return Ok(()); } + let state = String::from_utf8_lossy(&inspect.stdout).trim().to_string(); - let current = match other { + if state == "running" { + let current = pretty_bitcoin_name(other); + let requested = pretty_bitcoin_name(package_id); + return Err(anyhow::anyhow!( + "{} is currently running. Stop and uninstall {} before installing {}; both implementations use the same Bitcoin data directory and ports.", + current, current, requested + )); + } + + info!( + "Removing stuck {} container (state={}) before installing {}", + other, state, package_id + ); + install_log(&format!( + "INSTALL UNSTUCK: removing {} (state={}) before installing {}", + other, state, package_id + )) + .await; + let rm = tokio::process::Command::new("podman") + .args(["rm", "-f", other]) + .output() + .await + .context("Failed to remove stuck Bitcoin container")?; + if !rm.status.success() { + let stderr = String::from_utf8_lossy(&rm.stderr); + return Err(anyhow::anyhow!( + "Failed to remove stuck {} container: {}", + other, + stderr.trim() + )); + } + Ok(()) +} + +fn pretty_bitcoin_name(id: &str) -> &'static str { + match id { "bitcoin-core" => "Bitcoin Core", "bitcoin-knots" => "Bitcoin Knots", _ => "another Bitcoin node", - }; - let requested = match package_id { - "bitcoin-core" => "Bitcoin Core", - "bitcoin-knots" => "Bitcoin Knots", - _ => "the requested Bitcoin node", - }; - - Err(anyhow::anyhow!( - "{} is already installed. Stop and uninstall {} before installing {}; both implementations use the same Bitcoin data directory and ports.", - current, - current, - requested - )) + } } fn orchestrator_install_app_id(package_id: &str) -> &str {