feat: fix orphaned OpenCode processes cleanup on restart or exit

This commit is contained in:
Bohdan Triapitsyn
2026-01-16 02:20:12 +02:00
parent ba49a291bd
commit fafdce62d7
3 changed files with 32 additions and 2 deletions
+3
View File
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
## [Unreleased] ## [Unreleased]
- Desktop: fixed orphaned OpenCode processes not being cleaned up on restart or exit.
## [1.5.0] - 2026-01-16 ## [1.5.0] - 2026-01-16
- UI: added a new Files tab to browse workspace files directly from the interface. - UI: added a new Files tab to browse workspace files directly from the interface.
+1 -1
View File
@@ -2977,7 +2977,7 @@ dependencies = [
[[package]] [[package]]
name = "openchamber-desktop" name = "openchamber-desktop"
version = "1.4.9" version = "1.5.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -451,13 +451,20 @@ impl OpenCodeManager {
} }
async fn graceful_stop(&self) -> Result<()> { async fn graceful_stop(&self) -> Result<()> {
let port_to_kill = self.current_port();
let mut guard = self.child.lock().await; let mut guard = self.child.lock().await;
let Some(mut child) = guard.take() else { 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(()); return Ok(());
}; };
if child.try_wait()?.is_some() { 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(()); return Ok(());
} }
@@ -482,6 +489,8 @@ impl OpenCodeManager {
match timeout(Duration::from_secs(3), child.wait()).await { match timeout(Duration::from_secs(3), child.wait()).await {
Ok(_) => { Ok(_) => {
info!("[desktop:opencode] exited gracefully"); info!("[desktop:opencode] exited gracefully");
drop(guard);
kill_process_on_port(port_to_kill);
return Ok(()); return Ok(());
} }
Err(_) => { Err(_) => {
@@ -501,10 +510,28 @@ impl OpenCodeManager {
} }
} }
drop(guard);
kill_process_on_port(port_to_kill);
Ok(()) Ok(())
} }
} }
fn kill_process_on_port(port: Option<u16>) {
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) /// Check if CLI binary exists (can be called dynamically for polling)
pub fn check_cli_exists() -> bool { pub fn check_cli_exists() -> bool {
if std::env::var("OPENCHAMBER_DISABLE_CLI").is_ok() { if std::env::var("OPENCHAMBER_DISABLE_CLI").is_ok() {