From c74e0b89f7f472e9c88da0341f9a9b30acb76c41 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Sun, 25 Jan 2026 11:01:50 +0000 Subject: [PATCH] fix(shutdown): stop instances before app exit Prevent desktop wrappers from SIGKILLing the CLI during shutdown, which could orphan OpenCode workspace processes. Shut down workspaces earlier/in parallel and increase the quit grace period. --- .../electron/main/process-manager.ts | 3 +- packages/server/src/index.ts | 40 ++++++++++++------- packages/server/src/workspaces/manager.ts | 23 ++++++++--- .../tauri-app/src-tauri/src/cli_manager.rs | 4 +- packages/tauri-app/src-tauri/src/main.rs | 23 ++++++----- 5 files changed, 59 insertions(+), 34 deletions(-) diff --git a/packages/electron-app/electron/main/process-manager.ts b/packages/electron-app/electron/main/process-manager.ts index 20134f98..a614676a 100644 --- a/packages/electron-app/electron/main/process-manager.ts +++ b/packages/electron-app/electron/main/process-manager.ts @@ -178,7 +178,7 @@ export class CliProcessManager extends EventEmitter { return new Promise((resolve) => { const killTimeout = setTimeout(() => { child.kill("SIGKILL") - }, 4000) + }, 30000) child.on("exit", () => { clearTimeout(killTimeout) @@ -376,4 +376,3 @@ export class CliProcessManager extends EventEmitter { throw new Error("Unable to locate CodeNomad CLI build (dist/bin.js). Run npm run build --workspace @neuralnomads/codenomad.") } } - diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ac0c34e0..389b6198 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -286,21 +286,33 @@ async function main() { return } shuttingDown = true - logger.info("Received shutdown signal, closing server") - try { - await server.stop() - logger.info("HTTP server stopped") - } catch (error) { - logger.error({ err: error }, "Failed to stop HTTP server") - } + logger.info("Received shutdown signal, stopping workspaces and server") - try { - instanceEventBridge.shutdown() - await workspaceManager.shutdown() - logger.info("Workspace manager shutdown complete") - } catch (error) { - logger.error({ err: error }, "Workspace manager shutdown failed") - } + const shutdownWorkspaces = (async () => { + try { + instanceEventBridge.shutdown() + } catch (error) { + logger.warn({ err: error }, "Instance event bridge shutdown failed") + } + + try { + await workspaceManager.shutdown() + logger.info("Workspace manager shutdown complete") + } catch (error) { + logger.error({ err: error }, "Workspace manager shutdown failed") + } + })() + + const shutdownHttp = (async () => { + try { + await server.stop() + logger.info("HTTP server stopped") + } catch (error) { + logger.error({ err: error }, "Failed to stop HTTP server") + } + })() + + await Promise.allSettled([shutdownWorkspaces, shutdownHttp]) // no-op: remote UI manifest replaces GitHub release monitor diff --git a/packages/server/src/workspaces/manager.ts b/packages/server/src/workspaces/manager.ts index 1b1d863f..623667b7 100644 --- a/packages/server/src/workspaces/manager.ts +++ b/packages/server/src/workspaces/manager.ts @@ -187,16 +187,27 @@ export class WorkspaceManager { async shutdown() { this.options.logger.info("Shutting down all workspaces") + + const stopTasks: Array> = [] + for (const [id, workspace] of this.workspaces) { - if (workspace.pid) { - this.options.logger.info({ workspaceId: id }, "Stopping workspace during shutdown") - await this.runtime.stop(id).catch((error) => { - this.options.logger.error({ workspaceId: id, err: error }, "Failed to stop workspace during shutdown") - }) - } else { + if (!workspace.pid) { this.options.logger.debug({ workspaceId: id }, "Workspace already stopped") + continue } + + this.options.logger.info({ workspaceId: id }, "Stopping workspace during shutdown") + stopTasks.push( + this.runtime.stop(id).catch((error) => { + this.options.logger.error({ workspaceId: id, err: error }, "Failed to stop workspace during shutdown") + }), + ) } + + if (stopTasks.length > 0) { + await Promise.allSettled(stopTasks) + } + this.workspaces.clear() this.opencodeAuth.clear() this.options.logger.info("All workspaces cleared") diff --git a/packages/tauri-app/src-tauri/src/cli_manager.rs b/packages/tauri-app/src-tauri/src/cli_manager.rs index 14475910..f0ea6938 100644 --- a/packages/tauri-app/src-tauri/src/cli_manager.rs +++ b/packages/tauri-app/src-tauri/src/cli_manager.rs @@ -34,6 +34,8 @@ fn workspace_root() -> Option { const SESSION_COOKIE_NAME: &str = "codenomad_session"; +const CLI_STOP_GRACE_SECS: u64 = 30; + fn navigate_main(app: &AppHandle, url: &str) { if let Some(win) = app.webview_windows().get("main") { let mut display = url.to_string(); @@ -290,7 +292,7 @@ impl CliProcessManager { match child.try_wait() { Ok(Some(_)) => break, Ok(None) => { - if start.elapsed() > Duration::from_secs(4) { + if start.elapsed() > Duration::from_secs(CLI_STOP_GRACE_SECS) { #[cfg(unix)] unsafe { libc::kill(child.id() as i32, libc::SIGKILL); diff --git a/packages/tauri-app/src-tauri/src/main.rs b/packages/tauri-app/src-tauri/src/main.rs index e92cce9d..81645233 100644 --- a/packages/tauri-app/src-tauri/src/main.rs +++ b/packages/tauri-app/src-tauri/src/main.rs @@ -163,7 +163,8 @@ fn main() { .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| match event { - tauri::RunEvent::ExitRequested { .. } => { + tauri::RunEvent::ExitRequested { api, .. } => { + api.prevent_exit(); let app = app_handle.clone(); std::thread::spawn(move || { if let Some(state) = app.try_state::() { @@ -173,18 +174,18 @@ fn main() { }); } tauri::RunEvent::WindowEvent { - event: tauri::WindowEvent::Destroyed, + event: tauri::WindowEvent::CloseRequested { api, .. }, .. } => { - if app_handle.webview_windows().len() <= 1 { - let app = app_handle.clone(); - std::thread::spawn(move || { - if let Some(state) = app.try_state::() { - let _ = state.manager.stop(); - } - app.exit(0); - }); - } + // Ensure we have time to stop the CLI process before the app exits. + api.prevent_close(); + let app = app_handle.clone(); + std::thread::spawn(move || { + if let Some(state) = app.try_state::() { + let _ = state.manager.stop(); + } + app.exit(0); + }); } _ => {} });