import { createServer } from "node:http"; import { readFile } from "node:fs/promises"; import { mkdtemp, rm } from "node:fs/promises"; import { normalize, join } from "node:path"; import { tmpdir } from "node:os"; import { pathToFileURL } from "node:url"; import { spawn } from "node:child_process"; const root = new URL("../web/", import.meta.url).pathname.replace(/\/$/, ""); const modulePath = process.env.PLAYWRIGHT_MODULE; function blocked(code, reason = null) { console.log(JSON.stringify({ schema: "browser-opfs-s6-07-crash-sample.v1", status: "blocked", browser: "chromium", code, reason })); } if (!modulePath) { blocked("playwright_unavailable", "PLAYWRIGHT_MODULE is not set"); } else { let server; let firstBrowser; let secondBrowser; let firstProcess; let secondProcess; const profile = await mkdtemp(join(tmpdir(), "linuxcnc-opfs-crash-s6-07-")); try { const imported = await import(pathToFileURL(modulePath).href); const playwright = imported.default ?? imported; server = createServer(async (request, response) => { const requested = (request.url ?? "/").split("?")[0]; const relative = requested === "/" ? "/app/browser-opfs-probe.html" : requested; const file = normalize(join(root, relative)); if (!file.startsWith(root + "/")) { response.writeHead(403); response.end(); return; } try { const body = await readFile(file); response.writeHead(200, { "content-type": file.endsWith(".html") ? "text/html" : "text/javascript", "cache-control": "no-store" }); response.end(body); } catch { response.writeHead(404); response.end(); } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const port = server.address().port; const executable = process.env.BROWSER_EXECUTABLE_PATH || "google-chrome"; async function launchBrowser() { const debugPort = 39000 + Math.floor(Math.random() * 1000); const child = spawn(executable, [ "--headless=new", "--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage", "--disable-background-networking", `--user-data-dir=${profile}`, `--remote-debugging-port=${debugPort}`, "--remote-allow-origins=*", "about:blank", ], { stdio: ["ignore", "ignore", "ignore"] }); let version = null; for (let attempt = 0; attempt < 100; attempt += 1) { try { const response = await fetch(`http://127.0.0.1:${debugPort}/json/version`); if (response.ok) { version = await response.json(); break; } } catch {} await new Promise((resolve) => setTimeout(resolve, 50)); } if (!version) { child.kill("SIGKILL"); throw new Error("browser_devtools_unavailable"); } const browser = await playwright.chromium.connectOverCDP(`http://127.0.0.1:${debugPort}`); return { child, browser, version: version.Browser ?? "unknown" }; } ({ child: firstProcess, browser: firstBrowser } = await launchBrowser()); const firstContext = firstBrowser.contexts()[0]; const writer = firstContext.pages()[0] ?? await firstContext.newPage(); await writer.goto(`http://127.0.0.1:${port}/app/browser-opfs-probe.html?mode=write&cleanup=false`); await writer.waitForFunction(() => document.querySelector("#result")?.textContent !== "pending", null, { timeout: 10000 }); const written = JSON.parse(await writer.locator("#result").textContent()); if (written.status !== "verified") throw new Error(written.code ?? "opfs_write_blocked"); const processId = firstProcess.pid ?? null; if (!processId) throw new Error("browser_process_missing"); firstProcess.kill("SIGKILL"); await new Promise((resolve) => firstProcess.once("close", resolve)); await firstBrowser.close().catch(() => {}); firstBrowser = null; ({ child: secondProcess, browser: secondBrowser } = await launchBrowser()); const secondContext = secondBrowser.contexts()[0]; const reader = secondContext.pages()[0] ?? await secondContext.newPage(); await reader.goto(`http://127.0.0.1:${port}/app/browser-opfs-probe.html?mode=read&cleanup=true`); await reader.waitForFunction(() => document.querySelector("#result")?.textContent !== "pending", null, { timeout: 10000 }); const restored = JSON.parse(await reader.locator("#result").textContent()); if (restored.status !== "verified" || restored.content_hash !== written.content_hash || restored.cleanup !== true) throw new Error("opfs_crash_record_mismatch"); console.log(JSON.stringify({ schema: "browser-opfs-s6-07-crash-sample.v1", status: "verified", browser: "chromium", browser_version: secondProcess ? (await secondBrowser.version()) : "unknown", mode: "process_kill_profile_reopen", process_killed: true, commit_seq: restored.commit_seq, content_hash: restored.content_hash, cleanup: restored.cleanup })); } catch (error) { blocked("opfs_crash_probe_failed", error?.message ?? error?.name ?? String(error)); } finally { try { firstProcess?.kill("SIGKILL"); } catch {} try { secondProcess?.kill("SIGKILL"); } catch {} await secondBrowser?.close().catch(() => {}); await firstBrowser?.close().catch(() => {}); await Promise.all([firstProcess, secondProcess].filter(Boolean).map((child) => new Promise((resolve) => { if (child.exitCode !== null || child.signalCode !== null) { resolve(); return; } child.once("close", resolve); setTimeout(resolve, 2000); }))); server?.close(); await rm(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } }