Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion browse/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,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
Expand Down Expand Up @@ -323,7 +324,7 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
const launcherCode =
`const{spawn}=require('child_process');` +
`spawn(process.execPath,[${JSON.stringify(NODE_SERVER_SCRIPT)}],` +
`{detached:true,stdio:['ignore','ignore','ignore'],env:Object.assign({},process.env,` +
`{detached:true,windowsHide:true,stdio:['ignore','ignore','ignore'],env:Object.assign({},process.env,` +
`${extraEnvStr})}).unref()`;
Bun.spawnSync(['node', '-e', launcherCode], { stdio: ['ignore', 'ignore', 'ignore'] });
} else {
Expand All @@ -340,6 +341,7 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
// the Windows path's rationale — same root cause, different OS API.
nodeSpawn('bun', ['run', SERVER_SCRIPT], {
detached: true,
windowsHide: true,
stdio: ['ignore', 'ignore', 'ignore'],
env: { ...process.env, BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: parentPid, ...extraEnv },
}).unref();
Expand Down
36 changes: 36 additions & 0 deletions browse/test/cli-windows-hide.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, test } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";

const ROOT = path.resolve(import.meta.dir, "..", "..");
const CLI = path.join(ROOT, "browse", "src", "cli.ts");

function readCodeOnly(): string {
return fs.readFileSync(CLI, "utf-8")
.split("\n")
.filter((line) => !line.trim().startsWith("//"))
.join("\n");
}

// #1835 tripwire. On Windows, detached child_process.spawn allocates a
// console window unless windowsHide:true is set. These static checks fail CI
// if any detached spawn path in cli.ts drops the Windows no-window flag.
describe("#1835 detached browse spawns hide Windows consoles", () => {
test("Windows Node launcher spawn carries windowsHide:true", () => {
const src = readCodeOnly();
expect(src).toMatch(/spawn\(process\.execPath,[\s\S]{0,500}detached:\s*true[\s\S]{0,100}windowsHide:\s*true/);
});

test("non-Windows server nodeSpawn carries windowsHide:true", () => {
const src = readCodeOnly();
expect(src).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 src = readCodeOnly();
const detachedSpawns = src.match(/detached:\s*true/g)?.length ?? 0;
const windowsHideFlags = src.match(/windowsHide:\s*true/g)?.length ?? 0;
expect(detachedSpawns).toBeGreaterThan(0);
expect(windowsHideFlags).toBeGreaterThanOrEqual(detachedSpawns);
});
});
Loading