Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Windows: console window pops up on every browse server start (subprocess spawns missing windowsHide) #2182

Description

@tomfluff

Summary

On Windows, a console window (a Windows Terminal window on Win11) pops up every time the browse server starts. Several console-subprocess spawns are missing windowsHide: true.

Environment

  • Windows 11 (10.0.26200), gstack v1.58.5.0
  • bun 1.3.14, node v22.23.1

Root cause

On Windows, spawning a console subprocess without windowsHide: true allocates a console window (windowsHide maps to the CREATE_NO_WINDOW creation flag; Node's default is false). Enumerating top-level windows (EnumWindows) during server start showed the popup owned by WindowsTerminal, running:

  • icacls.exe — ACL lockdown of state/log files at startup (file-permissions.ts, execFileSync)
  • tasklist.exe — PID liveness probe (error-handling.ts, via the Bun.spawnSync polyfill)

The Bun polyfill wrapper (bun-polyfill.cjs) also drops any windowsHide the caller passes, so per-call fixes wouldn't stick — best fixed centrally there. Also patched the detached server launcher (cli.ts) and the detached telemetry spawn (security.ts) defensively.

Fix

diff --git a/browse/src/bun-polyfill.cjs b/browse/src/bun-polyfill.cjs
index e0ada11..4307659 100644
--- a/browse/src/bun-polyfill.cjs
+++ b/browse/src/bun-polyfill.cjs
@@ -75,6 +75,7 @@ globalThis.Bun = {
       timeout: options.timeout,
       env: options.env,
       cwd: options.cwd,
+      windowsHide: true, // don't pop a console window for CLI tools (icacls, tasklist, …) on Windows
     });
 
     return {
@@ -91,6 +92,7 @@ globalThis.Bun = {
       stdio,
       env: options.env,
       cwd: options.cwd,
+      windowsHide: true, // don't pop a console window for CLI tools on Windows
     });
 
     return {
diff --git a/browse/src/cli.ts b/browse/src/cli.ts
index 59327b7..e4a9f91 100644
--- a/browse/src/cli.ts
+++ b/browse/src/cli.ts
@@ -323,7 +323,9 @@ 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,` +
+      // windowsHide: a detached child otherwise gets its own console window on
+      // Windows (Node default windowsHide:false) — suppress that popup.
+      `{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 {
diff --git a/browse/src/file-permissions.ts b/browse/src/file-permissions.ts
index d3d404a..df204c7 100644
--- a/browse/src/file-permissions.ts
+++ b/browse/src/file-permissions.ts
@@ -71,7 +71,7 @@ export function restrictFilePermissions(filePath: string): void {
       execFileSync(
         'icacls',
         [filePath, '/inheritance:r', '/grant:r', `${user}:(F)`],
-        { stdio: 'ignore' },
+        { stdio: 'ignore', windowsHide: true },
       );
     } catch (err) {
       warnIcaclsFailure(filePath, err);
@@ -101,7 +101,7 @@ export function restrictDirectoryPermissions(dirPath: string): void {
       execFileSync(
         'icacls',
         [dirPath, '/inheritance:r', '/grant:r', `${user}:(OI)(CI)(F)`],
-        { stdio: 'ignore' },
+        { stdio: 'ignore', windowsHide: true },
       );
     } catch (err) {
       warnIcaclsFailure(dirPath, err);
diff --git a/browse/src/security.ts b/browse/src/security.ts
index d0e4039..fec8762 100644
--- a/browse/src/security.ts
+++ b/browse/src/security.ts
@@ -494,6 +494,7 @@ function reportAttemptTelemetry(record: AttemptRecord): void {
     const child = spawn(result.cmd, result.cmdArgs, {
       stdio: 'ignore',
       detached: true,
+      windowsHide: true, // detached child would otherwise pop a console window on Windows
     });
     // unref so this subprocess doesn't hold the event loop open
     child.unref();

Verification

Re-ran the EnumWindows diff around server start before/after — the icacls/tasklist Windows Terminal popups are gone. Windows-only; windowsHide is ignored on other platforms, so no behavior change elsewhere.

Note (separate, upstream)

There's a second popup source: Playwright's own browser spawn (chrome-headless-shell.exe) also lacks windowsHide in playwright-core's processLauncher.js. Filed separately upstream — not part of this change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions