Spaces:
Sleeping
Sleeping
| 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 ""; } | |
| } | |