From 052c7fe0108719b59cee49020950c335f2c7a483 Mon Sep 17 00:00:00 2001 From: Wim van den Heijkant Date: Fri, 17 Jul 2026 11:59:06 +0200 Subject: [PATCH] fix(browse): stop the Bun polyfill flashing console windows on Windows The Node bun-polyfill forwards Bun.spawn/spawnSync to child_process without windowsHide. Node defaults that option to false, so on Windows every child the browse server starts opens a console window -- Bun itself does not behave this way, so the polyfill was drifting from the API it emulates. The visible symptom is an empty black bun.exe window appearing every ~60s: server.ts's agent watchdog respawns a crashed terminal-agent on each tick, and the agent is spawned with stdio ['ignore','ignore', 'ignore'], so the window it opens has nothing in it. The tasklist, powershell and chrome --version helpers pop windows the same way. Fixed at the polyfill rather than the call sites, since it is the one choke point every Bun.spawn under Node passes through. An explicit windowsHide: false is still honoured for callers that want a visible console. Verified against the unpatched polyfill (spawn/spawnSync both received undefined) and the patched one (both receive true, explicit false still wins). Co-Authored-By: Claude Opus 4.8 (1M context) --- browse/src/bun-polyfill.cjs | 8 +++++++ browse/test/bun-polyfill.test.ts | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/browse/src/bun-polyfill.cjs b/browse/src/bun-polyfill.cjs index e0ada11b3..9b03f1f7a 100644 --- a/browse/src/bun-polyfill.cjs +++ b/browse/src/bun-polyfill.cjs @@ -75,6 +75,10 @@ globalThis.Bun = { timeout: options.timeout, env: options.env, cwd: options.cwd, + // Node defaults windowsHide to false, so every child opens a console + // window on Windows. Bun.spawnSync has no such behaviour, so the + // polyfill has to opt in to stay faithful to the API it's emulating. + windowsHide: options.windowsHide !== false, }); return { @@ -91,6 +95,10 @@ globalThis.Bun = { stdio, env: options.env, cwd: options.cwd, + // See spawnSync above. This one is the visible offender: the detached + // terminal-agent the browse watchdog respawns every 60s left an empty + // console window on screen each time. + windowsHide: options.windowsHide !== false, }); return { diff --git a/browse/test/bun-polyfill.test.ts b/browse/test/bun-polyfill.test.ts index 7ca25dfab..086e6487b 100644 --- a/browse/test/bun-polyfill.test.ts +++ b/browse/test/bun-polyfill.test.ts @@ -48,6 +48,44 @@ describe('bun-polyfill', () => { expect(lines[2]).toBe('HAS_UNREF'); }); + // Regression: Node defaults windowsHide to false, so before this the browse + // server flashed an empty console window on Windows for every child it + // spawned -- most visibly the terminal-agent the watchdog respawns on a 60s + // tick. Asserts the option reaches child_process rather than trying to + // observe a window, so it is meaningful on every platform. + test('Bun.spawn/spawnSync pass windowsHide to child_process', () => { + const result = Bun.spawnSync(['node', '-e', ` + const cp = require('child_process'); + const realSpawn = cp.spawn, realSpawnSync = cp.spawnSync; + const seen = {}; + cp.spawn = (c, a, o) => { seen.spawn = o.windowsHide; return realSpawn(c, a, o); }; + cp.spawnSync = (c, a, o) => { seen.spawnSync = o.windowsHide; return realSpawnSync(c, a, o); }; + require('${polyfillPath}'); + Bun.spawnSync([process.execPath, '-e', ''], { stdout: 'pipe' }); + Bun.spawn([process.execPath, '-e', ''], { stdio: ['ignore', 'ignore', 'ignore'] }); + console.log('spawnSync:' + seen.spawnSync); + console.log('spawn:' + seen.spawn); + `], { stdout: 'pipe', stderr: 'pipe' }); + const lines = result.stdout.toString().trim().split('\n'); + expect(lines[0]).toBe('spawnSync:true'); + expect(lines[1]).toBe('spawn:true'); + }); + + // An explicit windowsHide: false must still win, so a caller that genuinely + // wants a visible console (e.g. debugging a child) keeps that escape hatch. + test('Bun.spawn honours an explicit windowsHide: false', () => { + const result = Bun.spawnSync(['node', '-e', ` + const cp = require('child_process'); + const realSpawn = cp.spawn; + const seen = {}; + cp.spawn = (c, a, o) => { seen.spawn = o.windowsHide; return realSpawn(c, a, o); }; + require('${polyfillPath}'); + Bun.spawn([process.execPath, '-e', ''], { stdio: ['ignore', 'ignore', 'ignore'], windowsHide: false }); + console.log('spawn:' + seen.spawn); + `], { stdout: 'pipe', stderr: 'pipe' }); + expect(result.stdout.toString().trim()).toBe('spawn:false'); + }); + test('Bun.serve creates an HTTP server that responds', async () => { const result = Bun.spawnSync(['node', '-e', ` require('${polyfillPath}');