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]
- 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.
+1 -1
View File
@@ -2977,7 +2977,7 @@ dependencies = [
[[package]]
name = "openchamber-desktop"
version = "1.4.9"
version = "1.5.0"
dependencies = [
"anyhow",
"axum",
@@ -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<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)
pub fn check_cli_exists() -> bool {
if std::env::var("OPENCHAMBER_DISABLE_CLI").is_ok() {