diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 59327b7923..fbd90a848e 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; } @@ -285,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 @@ -323,9 +327,13 @@ async function startServer(extraEnv?: Record): Promise): 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); + }); +}); + +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) + .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); + }); +});