Files
feuerwerk/tools/lib/browser.mjs
T
root 1bd6e89ff5 Kill only this run's browser on the remote host
Both shutdown paths ran Get-Process chrome,msedge | Stop-Process -Force,
which is every browser on the machine. The remote host is somebody's
desktop: a test run must not close the windows they are working in. It
only stayed harmless because the test account cannot touch another
user's processes.

The launcher now writes its browser pid to %TEMP%\gm-browser-<port>.pid
and kills that pid; the job object still takes the renderer children.

Measured against the Windows desktop: five msedge processes belonging to
the logged-in user before the run, five after, and our chrome gone.
24 smoke checks ALL PASSED.
2026-09-09 09:24:51 +02:00

129 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Where the headless browser for a check run lives.
*
* Local by default. Set `GM_REMOTE=user@host` to run it on another machine instead: an SSH
* session forwards the CDP port back to 127.0.0.1, so everything downstream – waitForChrome,
* /json/new, the WebSocket – stays unchanged. The checks load the public base URL, so the
* remote side needs nothing but a browser and internet.
*
* Why bother: even with the GPU in use an idle editor page keeps rendering frames that never
* change, and this machine has 8 GB with most of the swap already gone.
*/
import { spawn, execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { homedir } from 'node:os';
const HERE = dirname(fileURLToPath(import.meta.url));
const LAUNCHER = join(HERE, '..', 'remote', 'gm-browser.ps1');
const FLAGS = [
'--headless',
'--hide-scrollbars',
/**
* Measured on this host (idle editor page, 20 s): software WebGL costs 373% of a core at 8 fps,
* the Intel HD 630 via ANGLE/Vulkan costs 69% at 56 fps. There is deliberately no software
* fallback — a host without a usable GPU should fail loudly rather than melt down. `--use-angle=gl`
* and `--use-gl=egl` look even cheaper but get no WebGL context at all — do not use them.
*/
'--use-angle=vulkan',
'--ignore-gpu-blocklist',
'--enable-gpu',
'--enable-features=Vulkan',
// None of this serves a headless check, and its stderr was full of GCM sign-in retries.
'--disable-background-networking',
'--disable-sync',
'--disable-extensions',
'--no-first-run',
'--mute-audio',
];
function sshArgs(key) {
return [
'-i',
key,
'-o',
'IdentitiesOnly=yes',
'-o',
'BatchMode=yes',
'-o',
'StrictHostKeyChecking=accept-new',
'-o',
'ConnectTimeout=15',
// A dead peer must surface as a closed socket, not as a run that waits forever.
'-o',
'ServerAliveInterval=10',
'-o',
'ServerAliveCountMax=3',
];
}
/**
* Starts the browser and returns its process plus a `stop()` that ends it the way that host
* needs. `proc.stderr` carries the browser's own last words either way.
*/
export function launchBrowser({ port, width, height }) {
const remote = process.env.GM_REMOTE;
if (!remote) {
const proc = spawn(
'chromium',
// --no-sandbox: this container runs as root, where chromium refuses to start otherwise.
[
...FLAGS,
'--no-sandbox',
`--remote-debugging-port=${port}`,
`--window-size=${width},${height}`,
'about:blank',
],
{ stdio: ['ignore', 'ignore', 'pipe'] },
);
return { proc, stop: () => proc.kill('SIGTERM'), where: 'local chromium' };
}
const key = process.env.GM_REMOTE_KEY ?? join(homedir(), '.ssh', 'gm-win.key');
// Copied every run: a stale launcher on the remote is a debugging session nobody enjoys.
execFileSync('scp', [...sshArgs(key), LAUNCHER, `${remote}:gm-browser.ps1`], { stdio: 'pipe' });
const proc = spawn(
'ssh',
[
...sshArgs(key),
'-o',
'ExitOnForwardFailure=yes',
'-L',
`${port}:127.0.0.1:${port}`,
remote,
`powershell -NoProfile -ExecutionPolicy Bypass -File gm-browser.ps1 -Port ${port} -Size ${width},${height}`,
],
{ stdio: ['ignore', 'ignore', 'pipe'] },
);
return {
proc,
/**
* The launcher notices a finished run on its own, but only after its idle window – a browser
* lingering a minute on someone else's desktop is rude, so end it now and let that window be
* the net for runs that die without reaching here. Only this run's pid: the host is somebody's
* desktop, and killing by name would close the browser they are reading this in.
*/
stop: () => {
const pidFile = `$env:TEMP\\gm-browser-${port}.pid`;
try {
execFileSync(
'ssh',
[
...sshArgs(key),
remote,
`powershell -NoProfile -Command "if (Test-Path ${pidFile}) { Stop-Process -Id (Get-Content ${pidFile}) -Force -ErrorAction SilentlyContinue }"`,
],
{ stdio: 'pipe', timeout: 20_000 },
);
} catch {
// Unreachable host, denied, already gone: the idle window still collects it.
}
proc.kill('SIGTERM');
},
where: `remote browser on ${remote}`,
};
}