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(()),
};
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 {