Files
SerpMax/stress/stop-browsers.ts
T

99 lines
4.8 KiB
TypeScript

/** # 脚本说明
* 压测清场:把某一轮发射器开过的浏览器全部停止。
* 1) 读 data/<label>-launched.json 的每条记录(--scan 模式则改从节点列表取全部 IN_USE Profile)
* 2) 逐个 GET /profiles/:fp 拿当前 leaseId → POST /profiles/:fp/browser/stop(x-lease-id;
* 显式停止走 closing 路径,不会触发「UI 误关自动复活」)
* 3) 兜底:对仍存的 user-data-dir 进程 SIGKILL(等待复活限频窗口过期,前后共两轮)
* 用法:bun stop-browsers.ts --label test1 [--scan]
*/
const fs = require("fs") as typeof import("fs");
const args = process.argv.slice(2);
function argOf(k: string, d: string): string { const i = args.indexOf(k); return i >= 0 && i + 1 < args.length ? args[i + 1] : d; }
const LABEL = argOf("--label", "test1");
const SCAN = args.includes("--scan");
const NODE = "http://127.0.0.1:3106";
const DATA = `${new URL(".", import.meta.url).pathname}data`;
/** # 函数:docker exec 查 admin DB 单值(取 Token 用) **/
function sqlOne(sql: string): string {
const p = Bun.spawnSync(["docker", "exec", "serpmax-pg", "psql", "-U", "postgres", "-d", "serpmax_admin", "-tAc", sql], { stdout: "pipe", stderr: "pipe" });
const out = p.stdout.toString().trim();
if (p.exitCode !== 0 || !out) throw new Error(`SQL 失败`);
return out;
}
const TOKEN = sqlOne("SELECT api_token FROM users WHERE id=2");
const NODE_KEY = /^NODE_KEY=(.*)$/m.exec(fs.readFileSync("/opt/serpmax/node/.env", "utf8"))?.[1]?.trim() ?? "";
const LEASE_TTL_MS = 120000;
const headersBase: Record<string, string> = { "content-type": "application/json", authorization: `Bearer ${TOKEN}`, "x-node-key": NODE_KEY };
/** # 成员变量 待停清单:label 文件或节点扫描(IN_USE/有活动 lease 的 Profile) **/
let targets: { fp: string }[] = [];
if (SCAN) {
const res = await fetch(`${NODE}/profiles`, { headers: headersBase, signal: AbortSignal.timeout(60000) });
const j: any = await res.json();
const rows: any[] = j?.data?.profiles ?? j?.profiles ?? [];
targets = rows.filter((r) => {
const lease = r?.lease ?? null;
return (lease && new Date(lease.expiresAt).getTime() > Date.now()) || ["IN_USE"].includes(r?.state);
}).map((r) => ({ fp: String(r.id) }));
console.log(`[stop] 扫描到 ${targets.length} 个在用 Profile`);
} else {
let list: { fp: string }[] = [];
try { list = JSON.parse(await Bun.file(`${DATA}/${LABEL}-launched.json`).text() || "[]"); } catch { list = []; }
targets = list.map((e: any) => ({ fp: String(e.fp) }));
}
console.log(`[stop] 目标 ${targets.length} 个`);
/** # 函数:显式停止一个 Profile 的浏览器(先拿当前 leaseId,过期则补一个短 lease) **/
async function stopOne(fp: string): Promise<void> {
const headers = { ...headersBase } as Record<string, string>;
try {
const d = await fetch(`${NODE}/profiles/${fp}`, { headers: headersBase, signal: AbortSignal.timeout(10000) });
const j: any = await d.json();
const lease = j?.data?.lease ?? j?.lease;
if (lease?.leaseId && new Date(lease.expiresAt).getTime() > Date.now()) headers["x-lease-id"] = lease.leaseId;
} catch { /* detail 拿不到就直接停 */ }
if (!headers["x-lease-id"]) {
const r = await fetch(`${NODE}/profiles/${fp}/lease`, { method: "POST", headers: headersBase, body: JSON.stringify({ consumerId: "stress-stop", ttlMs: LEASE_TTL_MS }), signal: AbortSignal.timeout(10000) });
if (r.ok) { const j2: any = await r.json(); headers["x-lease-id"] = (j2?.data ?? j2)?.leaseId; }
}
await fetch(`${NODE}/profiles/${fp}/browser/stop`, { method: "POST", headers, body: "{}", signal: AbortSignal.timeout(30000) }).catch(() => null);
}
const workers = 10;
let cursor = 0;
let stopped = 0;
async function worker(): Promise<void> {
while (cursor < targets.length) {
const e = targets[cursor++];
await stopOne(e.fp);
stopped++;
if (stopped % 25 === 0) console.log(`[stop] ${stopped}/${targets.length}`);
}
}
await Promise.all(Array.from({ length: workers }, () => worker()));
console.log(`[stop] API 显式停完 ${stopped}`);
/** # 函数:兜底统计存活压测浏览器进程数 **/
function countLeft(): number {
let n = 0;
for (const pid of fs.readdirSync("/proc").filter((d) => /^\d+$/.test(d))) {
try { if (fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").includes("--user-data-dir=")) n++; } catch { /* 已退出 */ }
}
return n;
}
for (const round of [1, 2]) {
const left = countLeft();
console.log(`[stop] round${round} 残存进程 ${left}`);
if (left === 0) break;
for (const pid of fs.readdirSync("/proc").filter((d) => /^\d+$/.test(d))) {
try {
if (fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").includes("--user-data-dir=")) process.kill(Number(pid), "SIGKILL");
} catch { /* ignore */ }
}
await new Promise((r) => setTimeout(r, 8000));
}
console.log(`[stop] done`);