196 lines
5.7 KiB
JavaScript
196 lines
5.7 KiB
JavaScript
/**
|
|
* Playwright webServer 入口:先起临时 nixmsg,再起 Vite(代理到该进程)。
|
|
* 注意:Playwright 会在 globalSetup 之前启动 webServer。
|
|
*/
|
|
import { spawn } from "node:child_process";
|
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
|
|
const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const repoRoot = path.resolve(webRoot, "..");
|
|
const runtimePath = path.join(webRoot, "e2e", ".runtime.json");
|
|
const pidPath = path.join(webRoot, "e2e", ".server.pid");
|
|
const binDir = path.join(webRoot, "e2e", ".bin");
|
|
|
|
function run(cwd, cmd, args, extraEnv) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(cmd, args, {
|
|
cwd,
|
|
shell: process.platform === "win32",
|
|
env: { ...process.env, ...extraEnv },
|
|
});
|
|
let out = "";
|
|
let err = "";
|
|
child.stdout?.on("data", (d) => {
|
|
out += String(d);
|
|
});
|
|
child.stderr?.on("data", (d) => {
|
|
err += String(d);
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
if (code === 0) resolve(out + err);
|
|
else reject(new Error(`${cmd} ${args.join(" ")} failed (${code}):\n${out}\n${err}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
function parseAdminPassword(text) {
|
|
const lines = text.split(/\r?\n/);
|
|
for (const raw of lines) {
|
|
const line = raw.trim();
|
|
const lower = line.toLowerCase();
|
|
if (lower.startsWith("password:")) return line.slice("password:".length).trim();
|
|
if (lower.startsWith("admin password:")) return line.slice("admin password:".length).trim();
|
|
}
|
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
const line = lines[i].trim();
|
|
if (line.length >= 12 && !line.toLowerCase().includes("error") && !/\s/.test(line)) {
|
|
return line;
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
async function waitAddr(file, timeoutMs) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
let last = "";
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const addr = (await readFile(file, "utf8")).trim();
|
|
if (addr) return addr;
|
|
last = "empty";
|
|
} catch (e) {
|
|
last = e instanceof Error ? e.message : String(e);
|
|
}
|
|
await delay(50);
|
|
}
|
|
throw new Error(`wait listen.addr timeout: ${last}`);
|
|
}
|
|
|
|
async function killPid(pid) {
|
|
if (!pid) return;
|
|
if (process.platform === "win32") {
|
|
await new Promise((resolve) => {
|
|
const child = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], { shell: true });
|
|
child.on("close", () => resolve());
|
|
child.on("error", () => resolve());
|
|
});
|
|
return;
|
|
}
|
|
try {
|
|
process.kill(pid, "SIGKILL");
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
async function cleanup(dataDir, servePid) {
|
|
await killPid(servePid);
|
|
if (dataDir) await rm(dataDir, { recursive: true, force: true });
|
|
await rm(runtimePath, { force: true });
|
|
await rm(pidPath, { force: true });
|
|
await rm(binDir, { recursive: true, force: true });
|
|
}
|
|
|
|
async function main() {
|
|
await mkdir(binDir, { recursive: true });
|
|
const bin = path.join(binDir, process.platform === "win32" ? "nixmsg.exe" : "nixmsg");
|
|
await run(repoRoot, "go", ["build", "-o", bin, "./cmd/nixmsg"]);
|
|
|
|
const dataDir = await mkdtemp(path.join(tmpdir(), "nixmsg-w4-e2e-"));
|
|
const cfgPath = path.join(dataDir, "config.yaml");
|
|
const yaml = `listen: "127.0.0.1:0"\ndata_dir: ${JSON.stringify(dataDir.replace(/\\/g, "/"))}\n`;
|
|
await writeFile(cfgPath, yaml, "utf8");
|
|
|
|
const initOut = await run(repoRoot, bin, ["admin", "init"], { NIXMSG_CONFIG: cfgPath });
|
|
const adminPassword = parseAdminPassword(initOut);
|
|
if (!adminPassword) {
|
|
await rm(dataDir, { recursive: true, force: true });
|
|
throw new Error(`admin init password not found:\n${initOut}`);
|
|
}
|
|
|
|
const serve = spawn(bin, ["serve"], {
|
|
env: { ...process.env, NIXMSG_CONFIG: cfgPath },
|
|
cwd: repoRoot,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
let serveLog = "";
|
|
serve.stdout.on("data", (d) => {
|
|
serveLog += String(d);
|
|
});
|
|
serve.stderr.on("data", (d) => {
|
|
serveLog += String(d);
|
|
});
|
|
|
|
let addr;
|
|
try {
|
|
addr = await waitAddr(path.join(dataDir, "listen.addr"), 15_000);
|
|
} catch (e) {
|
|
await killPid(serve.pid);
|
|
await rm(dataDir, { recursive: true, force: true });
|
|
throw new Error(`${e}\nserve log:\n${serveLog}`);
|
|
}
|
|
|
|
const vitePort = 5179;
|
|
const info = {
|
|
httpBase: `http://${addr}`,
|
|
adminPassword,
|
|
dataDir,
|
|
binPath: bin,
|
|
vitePort,
|
|
servePid: serve.pid,
|
|
};
|
|
await writeFile(runtimePath, JSON.stringify(info, null, 2), "utf8");
|
|
await writeFile(pidPath, String(serve.pid ?? ""), "utf8");
|
|
|
|
const vite = spawn(
|
|
process.platform === "win32" ? "pnpm.cmd" : "pnpm",
|
|
["exec", "vite", "--host", "127.0.0.1", "--port", String(vitePort), "--strictPort"],
|
|
{
|
|
cwd: webRoot,
|
|
env: { ...process.env, NIXMSG_PROXY_TARGET: info.httpBase },
|
|
stdio: "inherit",
|
|
shell: process.platform === "win32",
|
|
},
|
|
);
|
|
|
|
const shutdown = async () => {
|
|
vite.kill();
|
|
await cleanup(dataDir, serve.pid);
|
|
process.exit(0);
|
|
};
|
|
process.on("SIGINT", () => {
|
|
void shutdown();
|
|
});
|
|
process.on("SIGTERM", () => {
|
|
void shutdown();
|
|
});
|
|
process.on("exit", () => {
|
|
try {
|
|
if (serve.pid) {
|
|
if (process.platform === "win32") {
|
|
spawn("taskkill", ["/PID", String(serve.pid), "/T", "/F"], { shell: true, detached: true, stdio: "ignore" });
|
|
} else {
|
|
process.kill(serve.pid, "SIGKILL");
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
});
|
|
|
|
vite.on("exit", async (code) => {
|
|
await cleanup(dataDir, serve.pid);
|
|
process.exit(code ?? 1);
|
|
});
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|