diff --git a/core/archipelago/src/container/quadlet.rs b/core/archipelago/src/container/quadlet.rs index 2e76cf80..9250e841 100644 --- a/core/archipelago/src/container/quadlet.rs +++ b/core/archipelago/src/container/quadlet.rs @@ -581,11 +581,12 @@ pub async fn write_if_changed(unit: &QuadletUnit, dir: &Path) -> Result { /// Reload the user systemd manager. Required after any quadlet write /// or removal so systemd picks up the generated `.service` translation. pub async fn daemon_reload_user() -> Result<()> { - let status = Command::new("systemctl") - .args(["--user", "daemon-reload"]) - .status() + // Bounded: a wedged user manager (e.g. a unit stuck "deactivating" while + // podman hangs) could otherwise block daemon-reload indefinitely and freeze + // any caller — notably uninstall teardown. + let status = systemctl_user_status(&["daemon-reload"], Duration::from_secs(30)) .await - .context("spawn systemctl --user daemon-reload")?; + .context("systemctl --user daemon-reload")?; if !status.success() { return Err(anyhow!("systemctl --user daemon-reload exited {status}")); } @@ -787,11 +788,19 @@ fn directive_values(unit_body: &str, prefix: &str) -> Vec { /// that systemd no longer knows about. pub async fn disable_remove(unit_name: &str, dir: &Path) -> Result<()> { let svc = format!("{unit_name}.service"); - // Stop first; ignore failure (unit may already be down). - let _ = Command::new("systemctl") - .args(["--user", "stop", &svc]) - .status() - .await; + // Stop first; ignore failure (unit may already be down). BOUNDED — on + // rootless podman a generated unit can wedge in "deactivating" while + // `podman rm -f` hangs underneath it, and an unbounded `systemctl stop` + // would block the entire uninstall forever: the progress bar freezes and + // the package entry is stranded in `Removing` (a ghost in My Apps that also + // blocks reinstall). If the graceful stop times out, escalate to + // SIGKILL + reset-failed so teardown always proceeds. + if systemctl_user_status(&["stop", &svc], QUADLET_STOP_TIMEOUT) + .await + .is_err() + { + let _ = kill_and_reset_service(&svc).await; + } let path = dir.join(format!("{unit_name}.container")); if fs::try_exists(&path).await.unwrap_or(false) { match fs::remove_file(&path).await { @@ -802,10 +811,15 @@ pub async fn disable_remove(unit_name: &str, dir: &Path) -> Result<()> { } daemon_reload_user().await.ok(); // Defensive: kill the actual container too, in case quadlet left it. - let _ = Command::new("podman") - .args(["rm", "-f", unit_name]) - .status() - .await; + // Bounded so a hung podman store can't re-introduce the stall this function + // exists to avoid. + let _ = tokio::time::timeout( + QUADLET_STOP_TIMEOUT, + Command::new("podman") + .args(["rm", "-f", unit_name]) + .status(), + ) + .await; Ok(()) }