Files
pikasTech-HWLAB/scripts/src/browser-launcher.mjs
T
2026-06-04 20:35:58 +08:00

252 lines
8.1 KiB
JavaScript

import fs from "node:fs";
import { execFileSync } from "node:child_process";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
export const playwrightDependencyCommand = "npm install";
export const playwrightDependencySummary =
"Install repository dependencies before running browser smokes: npm install provides the declared playwright package; the Code Queue image may also provide the same package globally.";
const chromiumExecutableEnvKeys = Object.freeze([
"HWLAB_PLAYWRIGHT_CHROMIUM",
"PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH",
"CHROMIUM_PATH"
]);
const chromiumExecutableCandidates = Object.freeze([
"/snap/bin/chromium",
"/usr/bin/chromium-browser",
"/usr/bin/chromium",
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable"
]);
export async function importPlaywright() {
try {
return normalizePlaywrightModule(await import("playwright"));
} catch (localError) {
const globalRoot = npmGlobalRoot();
if (globalRoot) {
try {
return normalizePlaywrightModule(await import(pathToFileURL(path.join(globalRoot, "playwright/index.js")).href));
} catch {
// Keep the local resolution error so remediation points at this repository's package.json.
}
}
throw localError;
}
}
export async function launchChromium(chromium, options = {}) {
const plan = chromiumLaunchPlan();
const launchOptions = {
...options,
headless: options.headless ?? true,
args: ["--no-sandbox", ...(options.args ?? [])],
...(plan.executablePath ? { executablePath: plan.executablePath } : {})
};
try {
const browser = await chromium.launch(launchOptions);
browser.hwlabLaunchPlan = { ...plan, failureCode: null };
return browser;
} catch (error) {
error.hwlabBrowserLaunch = {
...plan,
failureCode: classifyBrowserLaunchFailure(error),
remediation: browserLaunchRemediation(plan)
};
throw error;
}
}
export async function browserDoctor() {
let playwright;
try {
playwright = await importPlaywright();
} catch (error) {
return {
status: "fail",
failureCode: "playwright-import-failed",
playwrightPackageSource: "missing",
platform: browserRuntimePlatform(),
browser: chromiumLaunchPlan(),
remediation: [playwrightDependencyCommand],
error: error instanceof Error ? error.message : String(error)
};
}
const managedBrowser = playwrightManagedBrowserPlan(playwright.chromium);
const installCheck = managedBrowser.cacheStatus === "missing" ? playwrightInstallDryRun() : null;
const browser = await launchChromium(playwright.chromium).catch((error) => ({ error }));
if (browser?.error) {
const plan = browser.error.hwlabBrowserLaunch ?? chromiumLaunchPlan();
return {
status: "fail",
failureCode: plan.failureCode ?? classifyBrowserLaunchFailure(browser.error),
playwrightPackageSource: "playwright",
platform: browserRuntimePlatform(),
managedBrowser,
installCheck,
browser: plan,
remediation: browserLaunchRemediation(plan),
error: browser.error instanceof Error ? browser.error.message : String(browser.error)
};
}
try {
const page = await browser.newPage();
await page.goto("data:text/html,<title>hwlab-browser-doctor</title>", { waitUntil: "domcontentloaded", timeout: 5000 });
await page.close();
} finally {
await browser.close();
}
return {
status: "pass",
failureCode: null,
playwrightPackageSource: "playwright",
platform: browserRuntimePlatform(),
managedBrowser,
installCheck,
browser: browser.hwlabLaunchPlan ?? chromiumLaunchPlan(),
remediation: []
};
}
export function playwrightManagedBrowserPlan(chromium) {
const executablePath = typeof chromium?.executablePath === "function" ? chromium.executablePath() : "";
const exists = executablePath ? fs.existsSync(executablePath) : false;
return {
browserSource: "playwright-cache",
executablePath,
exists,
cacheStatus: exists ? "available" : "missing"
};
}
export function playwrightInstallDryRun() {
const command = ["npx", "playwright", "install", "--dry-run", "chromium"];
try {
const output = execFileSync(command[0], command.slice(1), {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: 15000
}).trim();
return {
status: "dry-run-available",
command: command.join(" "),
note: "Dry-run reports the Playwright download plan only; this doctor uses system Chromium fallback when the managed cache is missing.",
output: boundedText(output)
};
} catch (error) {
const output = `${error?.stdout ?? ""}${error?.stderr ?? ""}`.trim();
const message = error instanceof Error ? error.message : String(error);
return {
status: /does not support|unsupported platform|unsupported/iu.test(`${output}\n${message}`) ? "unsupported" : "unknown",
command: command.join(" "),
output: boundedText(output || message),
exitCode: typeof error?.status === "number" ? error.status : null
};
}
}
export function browserRuntimePlatform() {
return {
platform: process.platform,
arch: process.arch,
osRelease: readOsReleaseSummary(),
glibcVersionRuntime: process.report?.getReport?.().header?.glibcVersionRuntime ?? null
};
}
export function chromiumLaunchPlan(env = process.env) {
const candidatePaths = chromiumExecutableCandidates.filter((candidate) => fs.existsSync(candidate));
for (const key of chromiumExecutableEnvKeys) {
const value = env[key];
if (value && fs.existsSync(value)) {
return {
browserSource: "env",
executablePath: value,
executablePathEnvKey: key,
fallbackUsed: key !== "HWLAB_PLAYWRIGHT_CHROMIUM",
candidatePaths
};
}
}
for (const candidate of chromiumExecutableCandidates) {
if (fs.existsSync(candidate)) {
return {
browserSource: "system-chromium",
executablePath: candidate,
executablePathEnvKey: null,
fallbackUsed: true,
candidatePaths
};
}
}
return {
browserSource: "playwright-cache",
executablePath: "",
executablePathEnvKey: null,
fallbackUsed: false,
candidatePaths
};
}
export function classifyBrowserLaunchFailure(error) {
const message = error instanceof Error ? error.message : String(error);
if (/Executable doesn't exist|download new browsers|playwright install/iu.test(message)) return "browser-cache-missing";
if (/spawn|ENOENT|permission denied|EACCES/iu.test(message)) return "browser-executable-unavailable";
return "browser-launch-failed";
}
export function browserLaunchRemediation(plan = chromiumLaunchPlan()) {
if (plan.executablePath) return [`using ${plan.executablePath}`];
return [
"Set HWLAB_PLAYWRIGHT_CHROMIUM=/snap/bin/chromium when system Chromium exists.",
"Install Playwright browsers with npx playwright install when no system Chromium is available."
];
}
function normalizePlaywrightModule(module) {
if (module?.chromium) return module;
if (module?.default?.chromium) return module.default;
return module;
}
function npmGlobalRoot() {
try {
return execFileSync("npm", ["root", "-g"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
timeout: 5000
}).trim();
} catch {
return "";
}
}
function readOsReleaseSummary() {
try {
const fields = Object.fromEntries(fs.readFileSync("/etc/os-release", "utf8")
.split(/\r?\n/u)
.map((line) => line.match(/^([A-Z_]+)=(.*)$/u))
.filter(Boolean)
.map((match) => [match[1], match[2].replace(/^"|"$/gu, "")]));
return {
name: fields.NAME ?? os.type(),
versionId: fields.VERSION_ID ?? "",
prettyName: fields.PRETTY_NAME ?? "",
codename: fields.VERSION_CODENAME ?? fields.UBUNTU_CODENAME ?? ""
};
} catch {
return { name: os.type(), versionId: os.release(), prettyName: os.platform(), codename: "" };
}
}
function boundedText(value, maxChars = 2000) {
const text = String(value ?? "");
return text.length > maxChars ? `${text.slice(0, maxChars)}...<truncated ${text.length - maxChars} chars>` : text;
}