114 lines
4.0 KiB
JavaScript
114 lines
4.0 KiB
JavaScript
#!/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);
|
||
});
|