/** * 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); });