fix(install): auto-clean stuck OTHER-variant bitcoin container

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) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-05-01 14:59:11 -04:00
parent d5c1253a7e
commit 8321d093e8

View File

@ -1799,40 +1799,64 @@ async fn check_bitcoin_implementation_conflict(package_id: &str) -> Result<()> {
_ => return Ok(()), _ => return Ok(()),
}; };
let output = tokio::process::Command::new("podman") // Three cases for the OTHER variant:
.args([ // - missing → no conflict, continue
"ps", // - running → real conflict, refuse install
"-a", // - any other state (created/exited/configured/...) → stuck from a
"--format", // prior failed install. Auto-remove so reinstall is reachable
"{{.Names}}", // without a manual `podman rm`. This is what unblocks the .198
"--filter", // "bitcoin-core stuck in created, port 8332 held by bitcoin-knots"
&format!("name=^{}$", other), // deadlock that no UI path could exit.
]) let inspect = tokio::process::Command::new("podman")
.args(["inspect", other, "--format", "{{.State.Status}}"])
.output() .output()
.await .await
.context("Failed to check existing Bitcoin node containers")?; .context("Failed to inspect conflicting Bitcoin container")?;
if !inspect.status.success() {
if String::from_utf8_lossy(&output.stdout).trim().is_empty() {
return Ok(()); 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-core" => "Bitcoin Core",
"bitcoin-knots" => "Bitcoin Knots", "bitcoin-knots" => "Bitcoin Knots",
_ => "another Bitcoin node", _ => "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 { fn orchestrator_install_app_id(package_id: &str) -> &str {