From 92d987e600ad3600346be9d11e9c521a0c00d433 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:04:22 -0700 Subject: [PATCH 1/4] fix: suppress Windows console flashes from browse cold-start child spawns --- browse/src/cli.ts | 19 ++++++--- browse/src/config.ts | 28 +++++++++++++ browse/src/file-permissions.ts | 4 +- browse/test/cli-windows-hide.test.ts | 63 ++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 browse/test/cli-windows-hide.test.ts diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 59327b792..9803c2c3b 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -11,7 +11,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { spawn as nodeSpawn } from 'child_process'; +import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from 'child_process'; import { safeUnlink, safeUnlinkQuiet, safeKill, isProcessAlive } from './error-handling'; import { writeSecureFile, mkdirSecure } from './file-permissions'; import { resolveConfig, ensureStateDir, readVersionHash } from './config'; @@ -141,10 +141,13 @@ async function killServer(pid: number): Promise { if (IS_WINDOWS) { // taskkill /T /F kills the process tree (Node + Chromium) try { - Bun.spawnSync( - ['taskkill', '/PID', String(pid), '/T', '/F'], - { stdout: 'pipe', stderr: 'pipe', timeout: 5000 } - ); + const proc = nodeSpawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { + stdout: 'pipe', + stderr: 'pipe', + timeout: 5000, + windowsHide: true, + }); + if (proc.error) throw proc.error; } catch (err: any) { if (err?.code !== 'ENOENT') throw err; } @@ -325,7 +328,11 @@ async function startServer(extraEnv?: Record): Promise { + test('Windows detach launcher uses hidden Node spawnSync', () => { + const body = read(CLI); + expect(body).toMatch(/spawnSync as nodeSpawnSync/); + expect(body).toMatch( + /nodeSpawnSync\(\s*['"]node['"],\s*\[\s*['"]-e['"],\s*launcherCode\s*\][\s\S]{0,250}windowsHide:\s*true/, + ); + expect(body).not.toMatch(/Bun\.spawnSync\(\s*\[\s*['"]node['"]/); + }); + + test('Windows taskkill helper uses hidden Node spawnSync', () => { + const body = read(CLI); + expect(body).toMatch( + /nodeSpawnSync\(\s*['"]taskkill['"][\s\S]{0,250}windowsHide:\s*true/, + ); + expect(body).not.toMatch(/Bun\.spawnSync\(\s*\[\s*['"]taskkill['"]/); + }); + + test('git probes use hidden Node spawnSync on Windows and keep Bun on POSIX', () => { + const body = read(CONFIG); + const hiddenGitSpawns = body.match( + /nodeSpawnSync\(\s*['"]git['"][\s\S]{0,250}windowsHide:\s*true/g, + ) || []; + expect(body).toContain("process.platform === 'win32'"); + expect(hiddenGitSpawns).toHaveLength(2); + expect(body).toMatch( + /Bun\.spawnSync\(\s*\[\s*['"]git['"],\s*['"]rev-parse['"],\s*['"]--show-toplevel['"]/, + ); + expect(body).toMatch( + /Bun\.spawnSync\(\s*\[\s*['"]git['"],\s*['"]remote['"],\s*['"]get-url['"],\s*['"]origin['"]/, + ); + }); + + test('icacls ACL helpers pass windowsHide to execFileSync', () => { + const body = read(FILE_PERMISSIONS); + const hiddenIcaclsCalls = body.match( + /execFileSync\(\s*['"]icacls['"][\s\S]{0,250}windowsHide:\s*true/g, + ) || []; + expect(hiddenIcaclsCalls).toHaveLength(2); + }); +}); From 4d30746a1b83caded12f60acff685956111fb293 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 4 Jun 2026 07:35:28 -0700 Subject: [PATCH 2/4] fix: hide the detached server windows too (folds in #1863) --- browse/src/cli.ts | 3 ++- browse/test/cli-windows-hide.test.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 9803c2c3b..516fb6eb3 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -326,7 +326,7 @@ async function startServer(extraEnv?: Record): Promise): Promise { expect(hiddenIcaclsCalls).toHaveLength(2); }); }); + +describe('detached server spawns carry windowsHide (#1863 fold-in)', () => { + test('Windows Node launcher inner spawn carries windowsHide:true', () => { + const body = read(CLI); + expect(body).toMatch(/spawn\(process\.execPath,[\s\S]{0,500}detached:true,windowsHide:true/); + }); + + test('non-Windows server nodeSpawn carries windowsHide:true', () => { + const body = read(CLI); + expect(body).toMatch(/nodeSpawn\('bun',[\s\S]{0,500}detached:\s*true[\s\S]{0,100}windowsHide:\s*true/); + }); + + test('every detached spawn site in cli.ts carries windowsHide:true', () => { + const body = read(CLI); + const detachedSpawns = body.match(/detached:\s*true/g)?.length ?? 0; + const windowsHideFlags = body.match(/windowsHide:\s*true/g)?.length ?? 0; + expect(windowsHideFlags).toBeGreaterThanOrEqual(detachedSpawns); + }); +}); From 77ebc1489ef4f5acce0147b3d7d732a8dc4f63e2 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 4 Jun 2026 07:36:13 -0700 Subject: [PATCH 3/4] fix: hide the osascript raise spawn too; exclude comments from the spawn invariant --- browse/src/cli.ts | 1 + browse/test/cli-windows-hide.test.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 516fb6eb3..fbd90a848 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -288,6 +288,7 @@ function raiseHeadedWindowMacOS(): void { nodeSpawn('osascript', ['-e', 'tell application "Google Chrome for Testing" to activate'], { stdio: 'ignore', detached: true, + windowsHide: true, }).unref(); } catch { // osascript missing or app not present — non-fatal diff --git a/browse/test/cli-windows-hide.test.ts b/browse/test/cli-windows-hide.test.ts index cd08adfad..73e377ec4 100644 --- a/browse/test/cli-windows-hide.test.ts +++ b/browse/test/cli-windows-hide.test.ts @@ -74,7 +74,10 @@ describe('detached server spawns carry windowsHide (#1863 fold-in)', () => { }); test('every detached spawn site in cli.ts carries windowsHide:true', () => { - const body = read(CLI); + const body = read(CLI) + .split('\n') + .filter((line) => !line.trim().startsWith('//') && !line.trim().startsWith('*')) + .join('\n'); const detachedSpawns = body.match(/detached:\s*true/g)?.length ?? 0; const windowsHideFlags = body.match(/windowsHide:\s*true/g)?.length ?? 0; expect(windowsHideFlags).toBeGreaterThanOrEqual(detachedSpawns); From 299b3a4e639addcc8ca372fb3dd50fc5a88c3dbb Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:22:52 -0700 Subject: [PATCH 4/4] fix: suppress Windows console flash from find-browse getGitRoot find-browse.ts compiles to its own console-subsystem binary and its getGitRoot() used the same uncovered Bun.spawnSync git rev-parse that config.ts fixed, so it kept flashing a conhost window per invocation. Apply the same Windows-only nodeSpawnSync + windowsHide:true treatment and keep Bun.spawnSync on POSIX. Extend cli-windows-hide.test.ts with a regression pinning the hidden git spawn at that site. Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> --- browse/src/find-browse.ts | 16 +++++++++++++++- browse/test/cli-windows-hide.test.ts | 13 +++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/browse/src/find-browse.ts b/browse/src/find-browse.ts index ab9f6a54d..5b0c1742e 100644 --- a/browse/src/find-browse.ts +++ b/browse/src/find-browse.ts @@ -8,17 +8,31 @@ import { accessSync, constants } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; +import { spawnSync as nodeSpawnSync } from 'child_process'; + +const IS_WINDOWS = process.platform === 'win32'; // ─── Binary Discovery ─────────────────────────────────────────── function getGitRoot(): string | null { try { + if (IS_WINDOWS) { + const proc = nodeSpawnSync('git', ['rev-parse', '--show-toplevel'], { + stdout: 'pipe', + stderr: 'pipe', + timeout: 2_000, + windowsHide: true, + }); + if (proc.error || proc.status !== 0) return null; + return proc.stdout.toString().trim() || null; + } const proc = Bun.spawnSync(['git', 'rev-parse', '--show-toplevel'], { stdout: 'pipe', stderr: 'pipe', + timeout: 2_000, }); if (proc.exitCode !== 0) return null; - return proc.stdout.toString().trim(); + return proc.stdout.toString().trim() || null; } catch { return null; } diff --git a/browse/test/cli-windows-hide.test.ts b/browse/test/cli-windows-hide.test.ts index 73e377ec4..3f450bd34 100644 --- a/browse/test/cli-windows-hide.test.ts +++ b/browse/test/cli-windows-hide.test.ts @@ -14,6 +14,7 @@ import * as path from 'node:path'; const ROOT = path.resolve(import.meta.dir, '..', '..'); const CLI = path.join(ROOT, 'browse', 'src', 'cli.ts'); const CONFIG = path.join(ROOT, 'browse', 'src', 'config.ts'); +const FIND_BROWSE = path.join(ROOT, 'browse', 'src', 'find-browse.ts'); const FILE_PERMISSIONS = path.join(ROOT, 'browse', 'src', 'file-permissions.ts'); function read(filePath: string): string { @@ -53,6 +54,18 @@ describe('#1784 Windows console flash suppression', () => { ); }); + test('find-browse git probe uses hidden Node spawnSync on Windows and keeps Bun on POSIX', () => { + const body = read(FIND_BROWSE); + const hiddenGitSpawns = body.match( + /nodeSpawnSync\(\s*['"]git['"][\s\S]{0,250}windowsHide:\s*true/g, + ) || []; + expect(body).toContain("process.platform === 'win32'"); + expect(hiddenGitSpawns).toHaveLength(1); + expect(body).toMatch( + /Bun\.spawnSync\(\s*\[\s*['"]git['"],\s*['"]rev-parse['"],\s*['"]--show-toplevel['"]/, + ); + }); + test('icacls ACL helpers pass windowsHide to execFileSync', () => { const body = read(FILE_PERMISSIONS); const hiddenIcaclsCalls = body.match(