#!/usr/bin/env node // gateway: 直接运行开发用 native browser gateway;不依赖 systemd。 import { spawn, execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import net from "node:net"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; const root = path.dirname(path.dirname(fileURLToPath(import.meta.url))); function readEnvFile(filePath) { if (!existsSync(filePath)) return {}; const values = {}; for (const line of readFileSync(filePath, "utf8").split("\n")) { const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line.trim()); if (match) values[match[1]] = match[2]; } return values; } const rootEnv = readEnvFile(path.join(root, ".env")); const gatewayEnvPath = process.env.CREATORHUB_BROWSER_GATEWAY_ENV ?? path.join(os.homedir(), ".config", "creatorhub", "browser-gateway.env"); const gatewayEnv = readEnvFile(gatewayEnvPath); const env = { ...process.env, ...rootEnv, ...gatewayEnv }; if (!env.LISTEN_ADDR) env.LISTEN_ADDR = "127.0.0.1:28187"; if (!env.GATEWAY_TOKEN) env.GATEWAY_TOKEN = "dev-creatorhub-gateway-token"; // 与 Python 侧 split_listen_address 对齐。 function splitListenAddr(value) { let rest = value; let host = ""; if (rest.startsWith(":")) { rest = rest.slice(1); } else if (rest.startsWith("[")) { const closing = rest.indexOf("]:"); if (closing <= 1) throw new Error(`LISTEN_ADDR 必须是 host:port:${value}`); host = rest.slice(1, closing); rest = rest.slice(closing + 2); } else { const sep = rest.lastIndexOf(":"); if (sep < 0) throw new Error(`LISTEN_ADDR 必须是 host:port:${value}`); host = rest.slice(0, sep); rest = rest.slice(sep + 1); } const port = Number(rest); if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error(`LISTEN_ADDR 端口必须是 1..65535:${value}`); } return { host, port }; } function probePort(host, port) { return new Promise((resolve) => { const server = net.createServer(); server.once("error", (err) => resolve(err)); server.listen({ host: host || undefined, port, exclusive: true }, () => { server.close(() => resolve(null)); }); }); } // 通过 ss -ltnp 找出占用端口的进程;失败或拿不到进程信息时返回 []。 function findOccupants(port) { try { const out = execFileSync("ss", ["-ltnp"], { encoding: "utf8" }); const occupants = []; for (const line of out.split("\n")) { if (!line.includes(`:${port} `)) continue; for (const match of line.matchAll(/"([^"]+)",pid=(\d+)/g)) { occupants.push(`${match[1]} (pid ${match[2]})`); } } return [...new Set(occupants)]; } catch { return []; } } async function assertListenAddrFree(value) { const { host, port } = splitListenAddr(value); const shown = host || "0.0.0.0"; const err = await probePort(host, port); if (!err) return; if (err.code !== "EADDRINUSE") return; // 其余错误交给 Python 侧原样报出。 const occupants = findOccupants(port); console.error(`[gateway] 监听地址 ${shown}:${port} 已被占用,gateway 无法启动。`); if (occupants.length > 0) { console.error(`[gateway] 占用进程:${occupants.join("、")}(常见原因:上一个 gateway 实例还在运行)。`); console.error(`[gateway] 处理:先停掉占用进程,或用 LISTEN_ADDR 换一个端口(优先级:~/.config/creatorhub/browser-gateway.env > .env)。`); } else { console.error(`[gateway] 无法识别占用进程(可能需要 root 才能查看),可用命令:ss -ltnp | grep :${port}`); } process.exit(1); } await assertListenAddrFree(env.LISTEN_ADDR); const gateway = spawn("python3", ["-m", "browser_gateway.server.http"], { cwd: root, env, stdio: "inherit", }); for (const signal of ["SIGINT", "SIGTERM"]) { process.on(signal, () => gateway.kill(signal)); } gateway.on("exit", (code, signal) => { if (signal) process.exit(1); process.exit(code ?? 0); });