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:
@@ -9,6 +9,7 @@ import { spawn } from 'child_process';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { normalizeWindowsDriveLetter } from './pathUtils';
|
||||
import { resolveWorkingDirectoryChange } from './workingDirectoryChange';
|
||||
import { registerManagedProcess, unregisterManagedProcess, reapOrphanedProcesses } from './opencodeProcessRegistry';
|
||||
|
||||
const t = vscode.l10n.t;
|
||||
|
||||
@@ -688,6 +689,9 @@ async function spawnManagedOpenCodeServer(
|
||||
child.on('error', onError);
|
||||
});
|
||||
|
||||
// Record this child so a future run can reap it if we crash before teardown.
|
||||
registerManagedProcess({ pid: child.pid, ownerPid: process.pid, port, binary, runtime: 'vscode' });
|
||||
|
||||
return {
|
||||
url,
|
||||
close: () => {
|
||||
@@ -696,6 +700,7 @@ async function spawnManagedOpenCodeServer(
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
unregisterManagedProcess(child.pid);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -726,6 +731,7 @@ async function allocateManagedOpenCodePort(): Promise<number> {
|
||||
|
||||
export function createOpenCodeManager(context: vscode.ExtensionContext): OpenCodeManager {
|
||||
let server: { url: string; close: () => void } | null = null;
|
||||
let reapedOrphansOnce = false;
|
||||
let managedApiUrlOverride: string | null = null;
|
||||
let managedPassword: string | null = null;
|
||||
let managedPasswordSource: 'user-env' | 'generated' | 'rotated' | null = null;
|
||||
@@ -866,6 +872,19 @@ export function createOpenCodeManager(context: vscode.ExtensionContext): OpenCod
|
||||
return;
|
||||
}
|
||||
|
||||
// Before spawning our own server, reap any OpenCode process WE spawned in a
|
||||
// prior run that was orphaned by a crash/host-kill. Verified + scoped to our
|
||||
// own pids, so it never touches a live instance's or the user's own server.
|
||||
if (!reapedOrphansOnce) {
|
||||
reapedOrphansOnce = true;
|
||||
try {
|
||||
const { reaped } = await reapOrphanedProcesses({ log: (msg) => console.log(msg) });
|
||||
if (reaped > 0) console.log(`[opencode] startup reaped ${reaped} orphaned process(es)`);
|
||||
} catch (error) {
|
||||
console.warn('[opencode] orphan reap failed:', error instanceof Error ? error.message : error);
|
||||
}
|
||||
}
|
||||
|
||||
setStatus('connecting');
|
||||
cliMissing = false;
|
||||
cliPath = null;
|
||||
|
||||
Reference in New Issue
Block a user