Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:05:55 +08:00
commit 89c3c1ab83
18 changed files with 1955 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { platform } from "node:os";
import puppeteer from "puppeteer-core";
import {
DEFAULT_PORT,
createLogger,
delay,
parseArgs,
printJSON,
resolveBrowserConnection,
normalizeNumber,
} from "./config.js";
const args = parseArgs(process.argv.slice(2), {
boolean: ["force", "json", "quiet"],
string: ["ws", "host"],
number: ["port", "timeout"],
alias: {
j: "json",
q: "quiet",
},
defaults: {
port: DEFAULT_PORT,
timeout: 5000,
},
});
const jsonOutput = Boolean(args.json);
const logger = createLogger({ quiet: Boolean(args.quiet), json: jsonOutput });
const port = normalizeNumber(args.port, DEFAULT_PORT);
const timeout = normalizeNumber(args.timeout, 5000);
const connectionOptions = resolveBrowserConnection({ port, host: args.host, ws: args.ws });
const result = {
ok: true,
port,
graceful: false,
forced: false,
closedTabs: 0,
};
logger.info("🔄 Shutting down browser...");
let browser;
if (!args.force) {
try {
browser = await puppeteer.connect({
...connectionOptions,
timeout,
});
const pages = await browser.pages();
result.closedTabs = pages.length;
await browser.close();
result.graceful = true;
logger.info(`✅ Closed browser gracefully (${pages.length} tabs)`);
} catch (error) {
logger.warn(`⚠️ Graceful shutdown failed: ${error.message}`);
} finally {
if (browser) await browser.disconnect();
}
}
if (!result.graceful) {
const forced = await forceCloseProcesses({ logger, port });
result.forced = forced;
if (!forced) {
logger.warn("⚠️ No Chrome processes were terminated");
}
}
await delay(300);
if (jsonOutput) {
printJSON(result);
} else {
process.stdout.write(`${JSON.stringify(result)}\n`);
logger.info("🎉 Browser shutdown complete");
}
async function forceCloseProcesses({ logger: log, port: debuggingPort }) {
const commands = buildTerminationCommands();
let anyKilled = false;
for (const command of commands) {
const { file, args } = command;
const execution = spawnSync(file, args, { stdio: "ignore" });
if (execution.status === 0) {
anyKilled = true;
log.info(`🔨 Executed ${file} ${args.join(" ")}`);
}
}
// Attempt to free the port via lsof if available (Unix only)
if (platform() !== "win32") {
const killByPort = spawnSync("bash", ["-c", `lsof -ti:${debuggingPort} 2>/dev/null | xargs -r kill -9`], {
stdio: "ignore",
});
if (killByPort.status === 0) {
anyKilled = true;
log.info(`🔨 Cleared processes on port ${debuggingPort}`);
}
}
return anyKilled;
}
function buildTerminationCommands() {
if (platform() === "win32") {
return [
{ file: "taskkill", args: ["/F", "/IM", "chrome.exe", "/T"] },
{ file: "taskkill", args: ["/F", "/IM", "msedge.exe", "/T"] },
];
}
return [
{ file: "pkill", args: ["-f", "Google Chrome"] },
{ file: "pkill", args: ["-f", "chrome"] },
{ file: "pkill", args: ["-f", "chromium"] },
{ file: "pkill", args: ["-f", "msedge"] },
];
}