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 {