/** * 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 { 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 { 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'))); } }); }); }