Multi-agent audit + aggressive cleanup across server/web/coder/booterm, delivered behind a DEFER discipline so none of the in-flight files were touched. Removes dead code/deps/columns, dedups server + coder helpers, and splits the oversized modules (tools.ts, opencode-server.ts, sentinel-summaries, turn.ts, TerminalPane.tsx) behind stable contracts. Adds 78 parity/unit tests (server 587, coder 323); fixes two latent bugs (ChatPane queue keys, FileViewerOverlay blank-line parity). Intended tag: v2.7.12-audit-cleanup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
89 lines
3.1 KiB
TypeScript
89 lines
3.1 KiB
TypeScript
/**
|
|
* Generic POSIX loopback-port utilities.
|
|
*
|
|
* Extracted verbatim (v2.7 audit reshape) from `backends/opencode-server.ts`,
|
|
* where they were embedded in the backend god-class. They have nothing to do with
|
|
* opencode semantics — they reclaim/await/allocate a 127.0.0.1 port — so they live
|
|
* here as reusable infra. No behavior change from the original.
|
|
*/
|
|
import { createServer, connect as netConnect } from 'node:net';
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
/**
|
|
* Reclaim a loopback port a dead child may still hold (lift of openchamber
|
|
* `killProcessOnPort`). Best-effort, POSIX-only (`lsof`/`kill`); a failure is
|
|
* harmless because the next spawn allocates a fresh ephemeral port. Never kills
|
|
* this process. Synchronous + short-timeout so a crash handler doesn't block.
|
|
*/
|
|
export function reclaimPort(port: number | null): void {
|
|
if (!port || process.platform === 'win32') return;
|
|
try {
|
|
const res = spawnSync('lsof', ['-ti', `:${port}`], { encoding: 'utf8', timeout: 3_000, windowsHide: true });
|
|
const out = res.stdout || '';
|
|
const myPid = process.pid;
|
|
for (const pidStr of out.split(/\s+/)) {
|
|
const pid = parseInt(pidStr.trim(), 10);
|
|
if (pid && pid !== myPid) {
|
|
try {
|
|
spawnSync('kill', ['-9', String(pid)], { stdio: 'ignore', timeout: 2_000 });
|
|
} catch {
|
|
// ignore — best effort
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// lsof absent or failed — the fresh-ephemeral-port spawn doesn't need this.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve true once nothing is listening on `port` (lift of openchamber
|
|
* `waitForPortRelease`). Used before re-spawning on a fixed port; with ephemeral
|
|
* ports it's a fast no-op. Probes 127.0.0.1; resolves false at the deadline.
|
|
*/
|
|
export function waitForPortRelease(port: number, timeoutMs: number): Promise<boolean> {
|
|
const deadline = Date.now() + timeoutMs;
|
|
return new Promise((resolve) => {
|
|
const attempt = () => {
|
|
const socket = netConnect({ port, host: '127.0.0.1' });
|
|
let settled = false;
|
|
const finish = (released: boolean) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
socket.removeAllListeners();
|
|
socket.destroy();
|
|
if (released || Date.now() >= deadline) {
|
|
resolve(released);
|
|
return;
|
|
}
|
|
setTimeout(attempt, 150);
|
|
};
|
|
socket.once('connect', () => finish(false));
|
|
socket.once('error', (err: NodeJS.ErrnoException) => {
|
|
if (err && (err.code === 'ECONNREFUSED' || err.code === 'EHOSTUNREACH')) finish(true);
|
|
else finish(false);
|
|
});
|
|
socket.setTimeout(500, () => finish(true));
|
|
};
|
|
attempt();
|
|
});
|
|
}
|
|
|
|
/** Bind-probe an ephemeral port on loopback. */
|
|
export function freePort(): Promise<number> {
|
|
return new Promise((resolve, reject) => {
|
|
const srv = createServer();
|
|
srv.unref();
|
|
srv.on('error', reject);
|
|
srv.listen(0, '127.0.0.1', () => {
|
|
const addr = srv.address();
|
|
if (addr && typeof addr === 'object') {
|
|
const { port } = addr;
|
|
srv.close(() => resolve(port));
|
|
} else {
|
|
srv.close(() => reject(new Error('port-utils: could not determine a free port')));
|
|
}
|
|
});
|
|
});
|
|
}
|