Skip to content

Windows: emulators launched with shell: true open a visible console window (detached spawn without windowsHide) #11261

Open

Description

@code-yeongyu

[REQUIRED] Environment info

firebase-tools: 15.33.0 (behavior unchanged on main)

Platform: Windows (reproduced on GitHub Actions windows-latest)

[REQUIRED] Test case

Two small Node scripts and no Firebase project needed. launcher.cjs uses the same spawn options that _runBinary in src/emulator/downloadableEmulators.ts uses for an emulator whose command has shell: true (today that is the Pub/Sub emulator):

// launcher.cjs  (run: node launcher.cjs [hidden])
const { spawn } = require("node:child_process");
const leaf = require("node:path").join(__dirname, "leaf.cjs");
const child = spawn(`"${process.execPath}"`, [`"${leaf}"`], {
  detached: true,
  stdio: ["inherit", "pipe", "pipe"],
  shell: true,
  ...(process.argv[2] === "hidden" ? { windowsHide: true } : {}),
});
child.stdout.resume();
child.stderr.resume();
// leaf.cjs: stands in for the emulator's console program
console.log("leaf pid", process.pid);
setTimeout(() => {}, 30000);

[REQUIRED] Steps to reproduce

  1. On Windows, start node launcher.cjs from a process that has no console of its own, such as an IDE, an editor's task runner, a GUI app, an agent host, or a service. (When run from an interactive terminal the child shares that terminal's console, so the bug is hidden.)
  2. Observe a new console window for the leaf process.
  3. Run node launcher.cjs hidden the same way. The window still appears: windowsHide alone does not fix this (see below).

We checked this on CI with AttachConsole(pid) + GetConsoleWindow() + IsWindowVisible() against the leaf pid:

  • with today's options (detached: true, shell: true): the leaf owns a console window and IsWindowVisible is true;
  • the same options plus windowsHide: true: the leaf still owns a visible console window (see the correction comment below);
  • the same detached spawn without shell: true (the form the Java emulators use): the leaf owns no console at all, so no window.

[REQUIRED] Expected behavior

Emulators start without opening a console window, as firebase emulators:exec already does for the user's script: src/emulator/commandUtils.ts spawns it with shell: true, windowsHide: true.

[REQUIRED] Actual behavior

With detached: true (DETACHED_PROCESS), the shell child does not inherit the parent's console, so the console program it launches allocates a fresh, visible console window. That window takes focus. This looks like the cause of #3506 ("PubSub Emulator on Windows launches a Java window"), which became the default path when #7033 switched the Pub/Sub launch to shell: true on Windows.

Proposed fix

Not settled yet. windowsHide: true alone does not help: with detached: true, the cmd.exe that shell: true starts gets no console, so the console program it launches (the .bat's java.exe) allocates a new, visible one. windowsHide applies to cmd.exe, not to the program it starts. We're measuring which launch options avoid the window while keeping the reason detached is set (graceful SIGINT handling), and will post the results here.

Activity

  1. code-yeongyu commented on Oct 10, 2026

    @code-yeongyu
    Author

    Correction to the original post: I claimed that adding windowsHide: true hides the window, and proposed that as a one-line fix. That was wrong. I had reasoned it out and had not yet run that variant.

    The measurement, on GitHub Actions windows-latest from a console-less parent, with the leaf checked via AttachConsole + GetConsoleWindow + IsWindowVisible:

    spawn options leaf has a visible console window
    detached: true, shell: true (today's _runBinary for shell commands) yes
    detached: true, shell: true, windowsHide: true yes
    detached: true, no shell (the Java emulators' form) no (the leaf has no console at all)

    So the bug is real, but windowsHide is not the fix: with DETACHED_PROCESS, the intermediate cmd.exe has no console, and the console program it starts creates a fresh, visible one. I've updated the issue body. I'll follow up with the options that do avoid the window (for example a non-detached hidden launch, or starting the emulator without the .bat/shell layer) once they're measured, rather than guessing again. Sorry for the noise.

  2. code-yeongyu commented on Oct 10, 2026

    @code-yeongyu
    Author

    Measured results, as promised. GitHub Actions windows-latest, launched from a console-less parent. The leaf (the console program that cmd.exe or the launcher starts) was checked with AttachConsole + GetConsoleWindow + IsWindowVisible. All rows come from one run.

    spawn options for the leaf launcher (stdio: ["inherit", "pipe", "pipe"] throughout) leaf has a console leaf window visible
    shell: true, detached: true (today's _runBinary for shell commands) yes yes
    shell: true, detached: true, windowsHide: true yes yes
    shell: true, detached: false, windowsHide: true yes, hidden no
    no shell, detached: true (the Java emulators' current form) none no
    no shell, detached: true, windowsHide: true none no
    no shell, detached: false, windowsHide: true yes, hidden no

    What this shows:

    • The window appears only when shell: true and detached: true are combined. windowsHide does not change that, because it applies to the intermediate cmd.exe, and DETACHED_PROCESS leaves cmd.exe without a console for its child to inherit.
    • Launching without the shell layer (as the Java emulators already do) avoids the window even with detached: true.
    • Keeping the shell but dropping detached (with windowsHide: true) also avoids it, but detached is there for the SIGINT/graceful-shutdown reason noted in _runBinary, so that trade-off is yours to judge.

    I'm not proposing a specific patch here; the table is meant to inform whichever direction you prefer for #7033's .bat launch.

  3. self-assigned this
    on Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    ⌂ Proxy