99 lines
4.8 KiB
TypeScript
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`);
|
|
|