Files
OmniRoute/src/lib/headroom/process.ts
Diego Rodrigues de Sa e Souza 0adae00c7b Release v3.8.42 (#5459)
Release v3.8.42 — full CHANGELOG in CHANGELOG.md.

CI: 103 checks green incl. CodeQL (all languages), Semgrep, all 8 unit shards,
coverage, Node 24 compat, and integration tests. Full unit suite validated
locally: 19437 pass / 0 fail. The 3 red checks are advisory and do not gate
main (no required status checks): SonarCloud/SonarQube new-code coverage gate,
and PR Test Policy (test-masking detector flagging the legitimate dead-Phind
provider removal in #5530 — reviewed, correct).

Includes cycle-close reconciliation + repair of inherited base-red tests from
#5480/#5527/#5427/#5521 that the PR->release fast-path did not exercise.
2026-06-30 06:54:29 -03:00

178 lines
4.5 KiB
TypeScript

/**
* Local headroom-proxy lifecycle management.
*
* Ported from upstream 9router @ b55cf36d (Cursor / decolua).
*
* Spawns the local `headroom proxy --port <port>` as a detached process,
* tracks its PID in `<DATA_DIR>/headroom/proxy.pid`, and exposes start/stop/
* status helpers. Only invoked behind the LOCAL_ONLY route-guard tier
* (Hard Rules #15 + #17): a tunneled JWT cannot reach the start/stop routes.
*/
import fs from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
import { DATA_DIR } from "@/lib/db/core";
import { findHeadroomBinary } from "./detect";
const HEADROOM_DIR = path.join(DATA_DIR ?? ".", "headroom");
const PID_FILE = path.join(HEADROOM_DIR, "proxy.pid");
const LOG_FILE = path.join(HEADROOM_DIR, "proxy.log");
const DEFAULT_PORT = 8787;
const STARTUP_TIMEOUT_MS = 8000;
const STOP_GRACE_MS = 2000;
export interface StartResult {
pid: number;
alreadyRunning: boolean;
}
export interface StopResult {
stopped: boolean;
pid?: number;
reason?: string;
}
export class HeadroomError extends Error {
code: string;
constructor(message: string, code: string) {
super(message);
this.code = code;
}
}
function ensureDir(): void {
if (!fs.existsSync(HEADROOM_DIR)) fs.mkdirSync(HEADROOM_DIR, { recursive: true });
}
function readPid(): number | null {
try {
if (!fs.existsSync(PID_FILE)) return null;
const raw = fs.readFileSync(PID_FILE, "utf8").trim();
const pid = parseInt(raw, 10);
return Number.isFinite(pid) && pid > 0 ? pid : null;
} catch {
return null;
}
}
function writePid(pid: number): void {
ensureDir();
fs.writeFileSync(PID_FILE, String(pid));
}
function clearPid(): void {
try {
if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
} catch {
// ignore
}
}
export function isPidAlive(pid: number | null | undefined): boolean {
if (!pid || typeof pid !== "number") return false;
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
export function getManagedPid(): number | null {
const pid = readPid();
return pid && isPidAlive(pid) ? pid : null;
}
function safePort(port: unknown): number {
const n = Number(port);
return Number.isFinite(n) && n > 0 && n < 65536 ? n : DEFAULT_PORT;
}
export async function startHeadroomProxy(opts: { port?: number } = {}): Promise<StartResult> {
const binary = findHeadroomBinary();
if (!binary) {
throw new HeadroomError("Headroom CLI not installed", "NOT_INSTALLED");
}
const existing = getManagedPid();
if (existing) return { pid: existing, alreadyRunning: true };
ensureDir();
const outFd = fs.openSync(LOG_FILE, "a");
// spawn (no shell) with array args — argv is passed directly to the
// headroom binary, so no shell-metacharacter handling is needed.
const child = spawn(binary, ["proxy", "--port", String(safePort(opts.port))], {
stdio: ["ignore", outFd, outFd],
detached: true,
windowsHide: true,
env: { ...process.env },
});
if (!child.pid) {
fs.closeSync(outFd);
throw new HeadroomError("Failed to spawn headroom proxy", "SPAWN_FAILED");
}
child.unref();
writePid(child.pid);
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
if (isPidAlive(child.pid!)) resolve();
else
reject(
new HeadroomError("headroom proxy exited during startup — see proxy.log", "EARLY_EXIT")
);
}, STARTUP_TIMEOUT_MS);
child.once("exit", (code) => {
clearTimeout(timer);
clearPid();
try {
fs.closeSync(outFd);
} catch {
// already closed
}
reject(
new HeadroomError(
`headroom proxy exited early (code=${code}) — see proxy.log`,
"EARLY_EXIT"
)
);
});
});
try {
fs.closeSync(outFd);
} catch {
// already closed
}
return { pid: child.pid, alreadyRunning: false };
}
export function stopHeadroomProxy(): StopResult {
const pid = getManagedPid();
if (!pid) return { stopped: false, reason: "not_running" };
try {
process.kill(pid, "SIGTERM");
setTimeout(() => {
if (isPidAlive(pid)) {
try {
process.kill(pid, "SIGKILL");
} catch {
// already gone
}
}
}, STOP_GRACE_MS);
clearPid();
return { stopped: true, pid };
} catch (e) {
clearPid();
const msg = e instanceof Error ? e.message : String(e);
throw new HeadroomError(`Failed to stop headroom proxy: ${msg}`, "STOP_FAILED");
}
}