Files
NixMsg/web/e2e/start-stack.mjs

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