fix(windows): hide supervised server console (#13992)

* fix(windows): hide supervised server console

* fix(windows): port icon.ico fix from #13991 and add regression tests

Adds a source-pattern test asserting the supervised server spawn() passes
windowsHide: true (Hard Rule #8 gap noted in review), and ports the
icon.ico-on-win32 fix from #13991 (credit @prabhtheone) with its own
regression test, so both real fixes ship without #13991's unrelated
comment purge.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Prabhjot Singh
2026-09-18 20:56:26 +05:30
committed by GitHub
parent dfcc4baed8
commit 2ebafaedce
4 changed files with 40 additions and 1 deletions

View File

@@ -90,6 +90,10 @@ export class ServerSupervisor {
cwd: dirname(this.serverPath),
env: this.env,
stdio: showLog ? "inherit" : ["ignore", "pipe", "pipe"],
// Tray mode has no visible console. Keep the supervised server hidden on Windows,
// including when it is restarted after a crash. Without this, each supervised
// spawn can create a visible terminal window.
windowsHide: true,
});
writePidFile("server", this.child.pid);

View File

@@ -29,7 +29,9 @@ async function loadSystray2() {
function getIconBase64() {
// Icon ships at bin/cli/tray/icon.png — the previous "icons/icon.png" path
// never existed, so the tray was created with an empty icon (#4605).
const iconPath = join(__dirname, "icon.png");
// systray2 expects an ICO payload on Windows; the PNG asset is used elsewhere.
// (ported from #13991, credit @prabhtheone)
const iconPath = join(__dirname, process.platform === "win32" ? "icon.ico" : "icon.png");
if (existsSync(iconPath)) return readFileSync(iconPath).toString("base64");
return "";
}

View File

@@ -335,3 +335,17 @@ test("every Bun server spawn (supervisor, --daemon, --no-recovery) uses the shar
);
assert.doesNotMatch(serveSrc, /join\(APP_DIR,\s*"open-sse/);
});
test("#13992: supervised server spawn hides the console window on Windows", () => {
const supervisorSrc = fs.readFileSync(
path.join(REPO_ROOT, "bin/cli/runtime/processSupervisor.mjs"),
"utf8"
);
assert.match(
supervisorSrc,
/spawn\(process\.execPath,\s*buildServerSpawnArgs\([\s\S]*?\),\s*\{[\s\S]*?windowsHide:\s*true[\s\S]*?\}\)/,
"the supervised server spawn() must pass windowsHide: true so a tray-mode restart never " +
"flashes a visible console on Windows"
);
});

View File

@@ -19,8 +19,12 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { initSystrayUnix } from "../../bin/cli/tray/traySystray.mjs";
const REPO_ROOT = path.resolve(import.meta.dirname, "../..");
class FakeSysTray {
static lastOpts: unknown = null;
onClickFn: unknown = null;
@@ -78,3 +82,18 @@ test("initSystrayUnix returns null without throwing when the loader yields null
const tray = await initSystrayUnix(opts, async () => null);
assert.equal(tray, null, "must degrade to null when systray2 cannot be loaded");
});
// #13992 (ported from #13991, credit @prabhtheone): systray2 expects an ICO
// payload on Windows — icon.png alone left the Windows tray icon blank.
test("getIconBase64 selects icon.ico on win32 and icon.png elsewhere (#13991/#13992)", () => {
const traySrc = fs.readFileSync(path.join(REPO_ROOT, "bin/cli/tray/traySystray.mjs"), "utf8");
assert.match(
traySrc,
/join\(__dirname,\s*process\.platform === "win32" \? "icon\.ico" : "icon\.png"\)/,
"getIconBase64 must resolve icon.ico on Windows instead of always using icon.png"
);
assert.ok(
fs.existsSync(path.join(REPO_ROOT, "bin/cli/tray/icon.ico")),
"bin/cli/tray/icon.ico must exist for the win32 branch to resolve to a real file"
);
});