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.
This commit is contained in:
@@ -178,7 +178,7 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const killTimeout = setTimeout(() => {
|
const killTimeout = setTimeout(() => {
|
||||||
child.kill("SIGKILL")
|
child.kill("SIGKILL")
|
||||||
}, 4000)
|
}, 30000)
|
||||||
|
|
||||||
child.on("exit", () => {
|
child.on("exit", () => {
|
||||||
clearTimeout(killTimeout)
|
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.")
|
throw new Error("Unable to locate CodeNomad CLI build (dist/bin.js). Run npm run build --workspace @neuralnomads/codenomad.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -286,21 +286,33 @@ async function main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
shuttingDown = true
|
shuttingDown = true
|
||||||
logger.info("Received shutdown signal, closing server")
|
logger.info("Received shutdown signal, stopping workspaces and server")
|
||||||
try {
|
|
||||||
await server.stop()
|
|
||||||
logger.info("HTTP server stopped")
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, "Failed to stop HTTP server")
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const shutdownWorkspaces = (async () => {
|
||||||
instanceEventBridge.shutdown()
|
try {
|
||||||
await workspaceManager.shutdown()
|
instanceEventBridge.shutdown()
|
||||||
logger.info("Workspace manager shutdown complete")
|
} catch (error) {
|
||||||
} catch (error) {
|
logger.warn({ err: error }, "Instance event bridge shutdown failed")
|
||||||
logger.error({ err: error }, "Workspace manager 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
|
// no-op: remote UI manifest replaces GitHub release monitor
|
||||||
|
|
||||||
|
|||||||
@@ -187,16 +187,27 @@ export class WorkspaceManager {
|
|||||||
|
|
||||||
async shutdown() {
|
async shutdown() {
|
||||||
this.options.logger.info("Shutting down all workspaces")
|
this.options.logger.info("Shutting down all workspaces")
|
||||||
|
|
||||||
|
const stopTasks: Array<Promise<void>> = []
|
||||||
|
|
||||||
for (const [id, workspace] of this.workspaces) {
|
for (const [id, workspace] of this.workspaces) {
|
||||||
if (workspace.pid) {
|
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 {
|
|
||||||
this.options.logger.debug({ workspaceId: id }, "Workspace already stopped")
|
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.workspaces.clear()
|
||||||
this.opencodeAuth.clear()
|
this.opencodeAuth.clear()
|
||||||
this.options.logger.info("All workspaces cleared")
|
this.options.logger.info("All workspaces cleared")
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ fn workspace_root() -> Option<PathBuf> {
|
|||||||
|
|
||||||
const SESSION_COOKIE_NAME: &str = "codenomad_session";
|
const SESSION_COOKIE_NAME: &str = "codenomad_session";
|
||||||
|
|
||||||
|
const CLI_STOP_GRACE_SECS: u64 = 30;
|
||||||
|
|
||||||
fn navigate_main(app: &AppHandle, url: &str) {
|
fn navigate_main(app: &AppHandle, url: &str) {
|
||||||
if let Some(win) = app.webview_windows().get("main") {
|
if let Some(win) = app.webview_windows().get("main") {
|
||||||
let mut display = url.to_string();
|
let mut display = url.to_string();
|
||||||
@@ -290,7 +292,7 @@ impl CliProcessManager {
|
|||||||
match child.try_wait() {
|
match child.try_wait() {
|
||||||
Ok(Some(_)) => break,
|
Ok(Some(_)) => break,
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
if start.elapsed() > Duration::from_secs(4) {
|
if start.elapsed() > Duration::from_secs(CLI_STOP_GRACE_SECS) {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::kill(child.id() as i32, libc::SIGKILL);
|
libc::kill(child.id() as i32, libc::SIGKILL);
|
||||||
|
|||||||
@@ -163,7 +163,8 @@ fn main() {
|
|||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while building tauri application")
|
.expect("error while building tauri application")
|
||||||
.run(|app_handle, event| match event {
|
.run(|app_handle, event| match event {
|
||||||
tauri::RunEvent::ExitRequested { .. } => {
|
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||||
|
api.prevent_exit();
|
||||||
let app = app_handle.clone();
|
let app = app_handle.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
if let Some(state) = app.try_state::<AppState>() {
|
if let Some(state) = app.try_state::<AppState>() {
|
||||||
@@ -173,18 +174,18 @@ fn main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
tauri::RunEvent::WindowEvent {
|
tauri::RunEvent::WindowEvent {
|
||||||
event: tauri::WindowEvent::Destroyed,
|
event: tauri::WindowEvent::CloseRequested { api, .. },
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if app_handle.webview_windows().len() <= 1 {
|
// Ensure we have time to stop the CLI process before the app exits.
|
||||||
let app = app_handle.clone();
|
api.prevent_close();
|
||||||
std::thread::spawn(move || {
|
let app = app_handle.clone();
|
||||||
if let Some(state) = app.try_state::<AppState>() {
|
std::thread::spawn(move || {
|
||||||
let _ = state.manager.stop();
|
if let Some(state) = app.try_state::<AppState>() {
|
||||||
}
|
let _ = state.manager.stop();
|
||||||
app.exit(0);
|
}
|
||||||
});
|
app.exit(0);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user