feat: fix orphaned OpenCode processes cleanup on restart or exit
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
Generated
+1
-1
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user