fix(detect): use system Chrome on Windows to stop GPU crash-loop window (#372)

On Windows, `impeccable detect <url>` flashed a persistent black window during
scans. The scan uses puppeteer's bundled Chrome, which runs from an untrusted
user-cache path; Windows blocks its GPU process, so it crash-loops and flashes a
compositor surface on every retry. It is not a real application window (not in
Alt+Tab, not clickable, invisible to window enumeration) and not malware.

Prefer the system-installed Chrome via channel:'chrome' on Windows, which runs
from a trusted location with a healthy GPU: no crash loop, no window. Fall back
to the bundled browser when Chrome is not installed. Scoped to Windows only, so
mac and linux keep the pinned bundled build for consistent measurement. Both
render on hardware GPU, so contrast measurement is unaffected.

Also routes both launch sites through one helper and fixes a pre-existing bug
where detectUrl hardcoded headless:true instead of honoring options.headless.

Tests: new tests/detect-url-launch.test.mjs covers the launch choice per
platform (Windows prefers channel:'chrome' and falls back to bundled;
non-Windows never attempts it), wired into the detector suite. Verified on
Windows 11 / Chrome 150: zero GPU crashes, window gone, findings unchanged.

This change was prepared with AI assistance.
This commit is contained in:
Vinaywho
2026-07-22 20:29:18 +05:30
parent 386f9883cf
commit 33d7684c06
3 changed files with 108 additions and 4 deletions
+25 -3
View File
@@ -7,6 +7,28 @@ import { profileFindingsAsync, profileStep, profileStepAsync } from '../../profi
import { captureVisualContrastCandidate } from '../visual/screenshot-contrast.mjs';
import { checkContentHiddenAtRest } from '../../rules/checks.mjs';
// On Windows, puppeteer's bundled Chrome lives in a user-writable cache
// directory. Its GPU process can be denied (STATUS_ACCESS_DENIED) by security
// software or the GPU sandbox because it launches from an untrusted path.
// Chrome then crash-loops the GPU process, and each relaunch briefly flashes a
// compositor surface, the black window users report during `detect <url>`
// (issue #372). The system-installed Chrome runs from a trusted location with a
// healthy GPU, so channel:'chrome' avoids the crash entirely; both use hardware
// GPU, so contrast measurement is unaffected. Scope this to Windows only: other
// platforms do not have the bug, so they keep the pinned bundled build for
// consistent measurement across machines. Fall back to bundled when the switch
// fails (Chrome not installed, or channel resolution fails).
async function launchBrowser(puppeteer, { headless = true, args = [] } = {}) {
if (process.platform === 'win32') {
try {
return await puppeteer.default.launch({ channel: 'chrome', headless, args });
} catch {
// No system Chrome available; fall through to the bundled browser.
}
}
return await puppeteer.default.launch({ headless, args });
}
// Reveal sweep + invisible-text measurement for the content-hidden-at-rest
// rule. Scrolls through the document with instant jumps (bypasses CSS
// scroll-behavior: smooth) so IntersectionObserver / scroll reveal handlers
@@ -178,7 +200,7 @@ async function detectUrl(url, options = {}) {
phase: 'load',
ruleId: 'launch-browser',
target: url,
}, () => puppeteer.default.launch({ headless: true, args: launchArgs }));
}, () => launchBrowser(puppeteer, { headless: options?.headless ?? true, args: launchArgs }));
const page = await profileStepAsync(profile, {
engine: 'browser',
phase: 'load',
@@ -312,7 +334,7 @@ async function createBrowserDetector(options = {}) {
throw new Error('puppeteer is required for URL scanning. Install: npm install puppeteer');
}
const launchArgs = options.launchArgs || (process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : []);
const browser = options.browser || await puppeteer.default.launch({
const browser = options.browser || await launchBrowser(puppeteer, {
headless: options.headless ?? true,
args: launchArgs,
});
@@ -337,4 +359,4 @@ async function createBrowserDetector(options = {}) {
};
}
export { runVisualContrastFallback, detectUrl, createBrowserDetector };
export { runVisualContrastFallback, detectUrl, createBrowserDetector, launchBrowser };