feat(opencode): never leave orphaned OpenCode server processes
OpenChamber spawns the OpenCode server as an external child binary (detached on Unix), so a hard crash, SIGKILL, or Ctrl+C of the host before graceful teardown could leave it running. Orphaned servers then accumulate and contend on the shared SQLite DB, causing severe startup slowdowns. Add a per-process registry plus a startup reaper, mirroring the pattern OpenCode's own CLI daemon uses for its detached server: - One file per spawned process at ~/.config/openchamber/managed-opencode/<pid>.json. Per-process files avoid the read-modify-write clobber race between concurrent runtimes/windows that a single shared file would suffer. - On spawn, record the child (pid, owner pid, port, binary, host runtime). - On graceful close/restart, delete the record. - On startup, reap only our own, verified, genuinely-orphaned processes: recorded by us AND still a live `opencode serve` on the recorded port AND whose spawner is provably gone (reparented to pid 1, or recorded owner dead). It never touches a process a live instance is using, the user's standalone server, the official desktop app, or the TUI. Wire it into every runtime that spawns the server: - web/desktop via the OpenCode lifecycle (register on spawn, unregister on close/restart, reap at startup). The restart-for-config-change flow inherits this automatically through the same kill/spawn paths. - VS Code carries a parity implementation (it does not bundle the web package) that reads/writes the same registry directory and uses the same algorithm. - Tag the actual host runtime (desktop/web/ssh-remote/vscode) for observability. Also tighten teardown so the registry stays accurate and orphans die promptly instead of only on the next start: - The web server now also handles SIGHUP and SIGUSR2 (terminal close and the nodemon restart used by dev:server:watch / dev:web:hmr). - Electron now installs SIGINT/SIGTERM/SIGHUP handlers that run the same background teardown as a normal quit, covering Ctrl+C on electron:dev. External OpenCode servers (OPENCODE_SKIP_START) are intentionally excluded: we never manage or kill processes we did not spawn.
This commit is contained in:
@@ -285,6 +285,22 @@ const performConfirmedQuit = () => {
|
||||
app.exit(0);
|
||||
};
|
||||
|
||||
// Hard-stop signals (`Ctrl+C` on `electron:dev`, an external `kill`/SIGTERM,
|
||||
// terminal close) bypass the normal app-quit flow — which would orphan the
|
||||
// in-process web server's managed OpenCode child. Run the same background
|
||||
// teardown the quit path uses (which kills the sidecar), then exit. The startup
|
||||
// reaper remains the backstop for an unhandled hard crash (SIGKILL).
|
||||
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
||||
process.on(signal, () => {
|
||||
try {
|
||||
shutdownBackgroundServices();
|
||||
} catch (error) {
|
||||
log.warn(`[electron] ${signal} shutdown failed:`, error);
|
||||
}
|
||||
app.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
const requestQuitWithConfirmation = async () => {
|
||||
await refreshQuitRiskFlags();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user