wave E stage 3: demos run in the app's own Chromium (browser-service helper)
Demos execute JS and need a browser; the app's own binary now provides one: main.cjs boots a hidden window + CDP port under --browser-service (before the single-instance lock - a helper is deliberately a second instance of this binary), lifetime tied to the server's stdin (EOF = quit, no orphans). headless.ts connects over CDP as PRIMARY (owner ruling 2026-10-10), system Chrome stays the fallback, graceful refusal when neither. Electron's CDP has no Target.createTarget, so the helper's pre-created window is THE page, reused and reset per job behind a serialized queue. PDFs and SVGs never reach a browser anymore (native since stages 1-2); stale Chrome wording in model-facing tool copy made engine-neutral. Live-proofed end-to-end from the dev tree (helper seen by process watch, js-ran 42 probe, no orphan). Gates 1523/1523.
This commit is contained in:
+56
-18
@@ -57,25 +57,63 @@ function serverEntry() {
|
||||
}
|
||||
|
||||
// ─── Single instance: two instances would fight over the data dir ─────────
|
||||
// Dev shell (LH_DEV_URL) runs BESIDE the packaged app, not instead of it:
|
||||
// its own userData so (a) the single-instance lock — checked below — doesn't
|
||||
// silently kill it, and (b) it never reads/writes the real app's
|
||||
// localStorage, auth.json, or logs. Must run before the lock check.
|
||||
if (process.env.LH_DEV_URL) {
|
||||
app.setPath("userData", `${app.getPath("userData")}-dev`);
|
||||
}
|
||||
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on("second-instance", () => {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
if (win) {
|
||||
if (win.isMinimized()) win.restore();
|
||||
win.focus();
|
||||
// ─── Browser-service helper (Wave E stage 3, owner 2026-10-10) ────────
|
||||
// The render engines for agent jobs: PDFs and SVGs rasterize natively
|
||||
// (pdfjs+Skia, resvg — no browser); DEMOS execute JS and need one. The
|
||||
// app's own Chromium serves that via this helper — the server spawns
|
||||
// <this binary> --browser-service
|
||||
// and connects over CDP (system Chrome stays the fallback). Handshake:
|
||||
// the helper prints "BROWSER_SERVICE_CDP <port>" on stdout once the CDP
|
||||
// endpoint listens. Lifetime: stdin stays open; parent death = EOF = quit.
|
||||
// MUST run before the single-instance lock — a helper is a second
|
||||
// instance of this very binary, and before the LH_DEV_URL redirect — the
|
||||
// helper owns no user data at all.
|
||||
if (process.argv.includes("--browser-service")) {
|
||||
(async () => {
|
||||
try {
|
||||
app.dock?.hide();
|
||||
const port = await freePort();
|
||||
// Must land before app-ready for Chromium to pick it up.
|
||||
app.commandLine.appendSwitch("remote-debugging-port", String(port));
|
||||
await app.whenReady();
|
||||
// A hidden window keeps the app lifecycle ordinary on every
|
||||
// platform (no window-all-closed handler is installed here).
|
||||
const win = new BrowserWindow({ show: false });
|
||||
await win.loadURL("about:blank");
|
||||
console.log(`BROWSER_SERVICE_CDP ${port}`);
|
||||
// Parent-tied lifetime: the server holds our stdin open.
|
||||
process.stdin.resume();
|
||||
process.stdin.on("end", () => app.quit());
|
||||
process.stdin.on("close", () => app.quit());
|
||||
} catch (err) {
|
||||
console.error(`browser-service failed: ${err?.stack ?? err}`);
|
||||
app.exit(1);
|
||||
}
|
||||
});
|
||||
main();
|
||||
})();
|
||||
} else {
|
||||
// Dev shell (LH_DEV_URL) runs BESIDE the packaged app, not instead of it:
|
||||
// its own userData so (a) the single-instance lock — checked below — doesn't
|
||||
// silently kill it, and (b) it never reads/writes the real app's
|
||||
// localStorage, auth.json, or logs. Must run before the lock check.
|
||||
if (process.env.LH_DEV_URL) {
|
||||
app.setPath("userData", `${app.getPath("userData")}-dev`);
|
||||
}
|
||||
|
||||
// The whole normal boot — lock included — lives in this else: a helper is
|
||||
// a SECOND instance of this very binary on purpose (the running app owns
|
||||
// the lock), so it must never reach this check.
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on("second-instance", () => {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
if (win) {
|
||||
if (win.isMinimized()) win.restore();
|
||||
win.focus();
|
||||
}
|
||||
});
|
||||
main();
|
||||
}
|
||||
}
|
||||
|
||||
let serverProcess = null;
|
||||
|
||||
Reference in New Issue
Block a user