From fafdce62d781f8abca3749bd77a16f67cbf1fd4c Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Fri, 16 Jan 2026 02:20:12 +0200 Subject: [PATCH] feat: fix orphaned OpenCode processes cleanup on restart or exit --- CHANGELOG.md | 3 ++ packages/desktop/src-tauri/Cargo.lock | 2 +- .../desktop/src-tauri/src/opencode_manager.rs | 29 ++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06dfd70a..d1059693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- Desktop: fixed orphaned OpenCode processes not being cleaned up on restart or exit. + + ## [1.5.0] - 2026-01-16 - UI: added a new Files tab to browse workspace files directly from the interface. diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index fcc6d151..17d1522f 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2977,7 +2977,7 @@ dependencies = [ [[package]] name = "openchamber-desktop" -version = "1.4.9" +version = "1.5.0" dependencies = [ "anyhow", "axum", diff --git a/packages/desktop/src-tauri/src/opencode_manager.rs b/packages/desktop/src-tauri/src/opencode_manager.rs index 9261a164..08ed6ac3 100644 --- a/packages/desktop/src-tauri/src/opencode_manager.rs +++ b/packages/desktop/src-tauri/src/opencode_manager.rs @@ -451,13 +451,20 @@ impl OpenCodeManager { } async fn graceful_stop(&self) -> Result<()> { + let port_to_kill = self.current_port(); + let mut guard = self.child.lock().await; let Some(mut child) = guard.take() else { + // No child, but still kill by port in case of orphaned processes + drop(guard); + kill_process_on_port(port_to_kill); return Ok(()); }; if child.try_wait()?.is_some() { - // Already exited + // Already exited, but still clean up by port + drop(guard); + kill_process_on_port(port_to_kill); return Ok(()); } @@ -482,6 +489,8 @@ impl OpenCodeManager { match timeout(Duration::from_secs(3), child.wait()).await { Ok(_) => { info!("[desktop:opencode] exited gracefully"); + drop(guard); + kill_process_on_port(port_to_kill); return Ok(()); } Err(_) => { @@ -501,10 +510,28 @@ impl OpenCodeManager { } } + drop(guard); + kill_process_on_port(port_to_kill); + Ok(()) } } +fn kill_process_on_port(port: Option) { + let Some(port) = port else { return }; + + // Kill any process listening on our port to clean up orphaned children. + // The opencode CLI is a Node wrapper that spawns the actual binary as a child. + // Killing the wrapper doesn't kill the child, so we kill by port. + #[cfg(unix)] + { + use std::process::Command; + let _ = Command::new("sh") + .args(["-c", &format!("lsof -ti:{} | xargs kill -9 2>/dev/null || true", port)]) + .output(); + } +} + /// Check if CLI binary exists (can be called dynamically for polling) pub fn check_cli_exists() -> bool { if std::env::var("OPENCHAMBER_DISABLE_CLI").is_ok() {