routerOnly / src /lib /headroom /process.js
HF Deployer
Init 9Router for HF Space (no binaries)
ebd68ab
Raw
History Blame Contribute Delete
3.77 kB
import fs from "fs";
import path from "path";
import { spawn } from "child_process";
import { DATA_DIR } from "@/lib/dataDir.js";
import { findHeadroomBinary } from "./detect.js";
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;
function ensureDir() {
if (!fs.existsSync(HEADROOM_DIR)) fs.mkdirSync(HEADROOM_DIR, { recursive: true });
}
function readPid() {
try {
if (fs.existsSync(PID_FILE)) return parseInt(fs.readFileSync(PID_FILE, "utf8"), 10);
} catch { /* ignore */ }
return null;
}
function writePid(pid) {
ensureDir();
fs.writeFileSync(PID_FILE, String(pid));
}
function clearPid() {
try { if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
}
// process.kill throws if pid is dead — use this to probe.
export function isPidAlive(pid) {
if (!pid || typeof pid !== "number") return false;
try { process.kill(pid, 0); return true; } catch { return false; }
}
export function getManagedPid() {
const pid = readPid();
return pid && isPidAlive(pid) ? pid : null;
}
export async function startHeadroomProxy({ port = DEFAULT_PORT } = {}) {
const safePort = Number(port) > 0 && Number(port) < 65536 ? Number(port) : DEFAULT_PORT;
const binary = findHeadroomBinary();
if (!binary) {
const err = new Error("Headroom CLI not installed");
err.code = "NOT_INSTALLED";
throw err;
}
const existing = getManagedPid();
if (existing) return { pid: existing, alreadyRunning: true };
ensureDir();
// spawn stdio requires fd numbers, not WriteStream objects.
const outFd = fs.openSync(LOG_FILE, "a");
const child = spawn(binary, ["proxy", "--port", String(safePort)], {
stdio: ["ignore", outFd, outFd],
detached: true,
windowsHide: true,
env: { ...process.env },
});
if (!child.pid) {
fs.closeSync(outFd);
const err = new Error("Failed to spawn headroom proxy");
err.code = "SPAWN_FAILED";
throw err;
}
child.unref();
writePid(child.pid);
// Wait until the process either stays alive briefly (success) or exits fast (failure).
await new Promise((resolve, reject) => {
const startupTimer = setTimeout(() => {
if (isPidAlive(child.pid)) resolve();
else reject(new Error("headroom proxy exited during startup — see proxy.log"));
}, STARTUP_TIMEOUT_MS);
child.once("exit", (code) => {
clearTimeout(startupTimer);
clearPid();
fs.closeSync(outFd);
const e = new Error(`headroom proxy exited early (code=${code}) — see proxy.log`);
e.code = "EARLY_EXIT";
reject(e);
});
});
// Close parent's copy of the fd; child retains its own after unref.
fs.closeSync(outFd);
return { pid: child.pid, alreadyRunning: false };
}
export function stopHeadroomProxy() {
const pid = getManagedPid();
if (!pid) return { stopped: false, reason: "not_running" };
try {
process.kill(pid, "SIGTERM");
// Give it a moment, then force if still alive.
setTimeout(() => {
if (isPidAlive(pid)) {
try { process.kill(pid, "SIGKILL"); } catch { /* already gone */ }
}
}, 2000);
clearPid();
return { stopped: true, pid };
} catch (e) {
clearPid();
const err = new Error(`Failed to stop headroom proxy: ${e.message}`);
err.code = "STOP_FAILED";
throw err;
}
}
export function getHeadroomLogTail(maxLines = 200) {
try {
if (!fs.existsSync(LOG_FILE)) return "";
const content = fs.readFileSync(LOG_FILE, "utf8");
const lines = content.split(/\r?\n/).filter(Boolean);
return lines.slice(-maxLines).join("\n");
} catch { return ""; }
}