On Windows, child_process.spawn() and execFile() open a visible console window by default. The web server spawn, RPC bridge, browser opener, and all 15 web service subprocess calls were missing windowsHide: true, causing constant console window flashing when running gsd --web. Closes #2628 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
466c7dea18
commit
de9ba8aeb7
18 changed files with 143 additions and 1 deletions
|
|
@ -164,6 +164,7 @@ test('launchWebMode prefers the packaged standalone host and opens the resolved
|
|||
cwd: standaloneRoot,
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
env: {
|
||||
TEST_ENV: '1',
|
||||
HOSTNAME: '127.0.0.1',
|
||||
|
|
|
|||
120
src/tests/integration/web-mode-windows-hide.test.ts
Normal file
120
src/tests/integration/web-mode-windows-hide.test.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const webMode = await import("../../web-mode.ts");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2628 — On Windows, child processes spawned by web-mode must set
|
||||
// `windowsHide: true` to prevent console windows from flashing on screen.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("launchWebMode passes windowsHide: true in spawn options", async (t) => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "gsd-web-winhide-"));
|
||||
const standaloneRoot = join(tmp, "dist", "web", "standalone");
|
||||
const serverPath = join(standaloneRoot, "server.js");
|
||||
mkdirSync(standaloneRoot, { recursive: true });
|
||||
writeFileSync(serverPath, 'console.log("stub")\n');
|
||||
|
||||
const pidFilePath = join(tmp, "web-server.pid");
|
||||
const registryPath = join(tmp, "web-instances.json");
|
||||
|
||||
let capturedOptions: Record<string, unknown> | undefined;
|
||||
|
||||
t.after(() => {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const status = await webMode.launchWebMode(
|
||||
{
|
||||
cwd: "/tmp/winhide-project",
|
||||
projectSessionsDir: "/tmp/.gsd/sessions/winhide",
|
||||
agentDir: "/tmp/.gsd/agent",
|
||||
packageRoot: tmp,
|
||||
},
|
||||
{
|
||||
initResources: () => {},
|
||||
resolvePort: async () => 46000,
|
||||
execPath: "/custom/node",
|
||||
env: { TEST_ENV: "1" },
|
||||
spawn: (_command, _args, options) => {
|
||||
capturedOptions = options as Record<string, unknown>;
|
||||
return {
|
||||
pid: 70001,
|
||||
once: () => undefined,
|
||||
unref: () => {},
|
||||
} as any;
|
||||
},
|
||||
waitForBootReady: async () => undefined,
|
||||
openBrowser: () => {},
|
||||
pidFilePath,
|
||||
writePidFile: webMode.writePidFile,
|
||||
registryPath,
|
||||
stderr: { write: () => true },
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(status.ok, true, "launch should succeed");
|
||||
assert.ok(capturedOptions, "spawn must have been called");
|
||||
assert.equal(
|
||||
capturedOptions!.windowsHide,
|
||||
true,
|
||||
"spawn options must include windowsHide: true to prevent console window flashing on Windows (#2628)",
|
||||
);
|
||||
});
|
||||
|
||||
test("launchWebMode source-dev host also passes windowsHide: true", async (t) => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "gsd-web-winhide-src-"));
|
||||
const webRoot = join(tmp, "web");
|
||||
mkdirSync(webRoot, { recursive: true });
|
||||
writeFileSync(join(webRoot, "package.json"), '{"name":"web"}\n');
|
||||
|
||||
const pidFilePath = join(tmp, "web-server.pid");
|
||||
const registryPath = join(tmp, "web-instances.json");
|
||||
|
||||
let capturedOptions: Record<string, unknown> | undefined;
|
||||
|
||||
t.after(() => {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const status = await webMode.launchWebMode(
|
||||
{
|
||||
cwd: "/tmp/winhide-src-project",
|
||||
projectSessionsDir: "/tmp/.gsd/sessions/winhide-src",
|
||||
agentDir: "/tmp/.gsd/agent",
|
||||
packageRoot: tmp,
|
||||
},
|
||||
{
|
||||
initResources: () => {},
|
||||
resolvePort: async () => 46001,
|
||||
execPath: "/custom/node",
|
||||
env: { TEST_ENV: "1" },
|
||||
platform: "win32",
|
||||
spawn: (_command, _args, options) => {
|
||||
capturedOptions = options as Record<string, unknown>;
|
||||
return {
|
||||
pid: 70002,
|
||||
once: () => undefined,
|
||||
unref: () => {},
|
||||
} as any;
|
||||
},
|
||||
waitForBootReady: async () => undefined,
|
||||
openBrowser: () => {},
|
||||
pidFilePath,
|
||||
writePidFile: webMode.writePidFile,
|
||||
registryPath,
|
||||
stderr: { write: () => true },
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(status.ok, true, "launch should succeed");
|
||||
assert.ok(capturedOptions, "spawn must have been called");
|
||||
assert.equal(
|
||||
capturedOptions!.windowsHide,
|
||||
true,
|
||||
"source-dev spawn must also include windowsHide: true (#2628)",
|
||||
);
|
||||
});
|
||||
|
|
@ -14,7 +14,7 @@ const DEFAULT_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '.
|
|||
function openBrowser(url: string): void {
|
||||
if (process.platform === 'win32') {
|
||||
// PowerShell's Start-Process handles URLs with '&' safely; cmd /c start does not.
|
||||
execFile('powershell', ['-c', `Start-Process '${url.replace(/'/g, "''")}'`], () => {})
|
||||
execFile('powershell', ['-c', `Start-Process '${url.replace(/'/g, "''")}'`], { windowsHide: true }, () => {})
|
||||
} else {
|
||||
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open'
|
||||
execFile(cmd, [url], () => {})
|
||||
|
|
@ -635,6 +635,7 @@ export async function launchWebMode(
|
|||
cwd: spawnSpec.cwd,
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
env,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ export async function collectAuthoritativeAutoDashboardData(
|
|||
[AUTO_DASHBOARD_MODULE_ENV]: autoModulePath,
|
||||
},
|
||||
maxBuffer: AUTO_DASHBOARD_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -772,6 +772,7 @@ async function loadSessionBrowserSessionsViaChildProcess(config: BridgeRuntimeCo
|
|||
GSD_SESSION_BROWSER_DIR: config.projectSessionsDir,
|
||||
},
|
||||
maxBuffer: 1024 * 1024,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
@ -833,6 +834,7 @@ async function appendSessionInfoViaChildProcess(
|
|||
GSD_TARGET_SESSION_NAME: name,
|
||||
},
|
||||
maxBuffer: 1024 * 1024,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, _stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
@ -1031,6 +1033,7 @@ async function loadWorkspaceIndexViaChildProcess(basePath: string, packageRoot:
|
|||
GSD_WORKSPACE_BASE: basePath,
|
||||
},
|
||||
maxBuffer: 1024 * 1024,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
@ -1624,6 +1627,7 @@ export class BridgeService {
|
|||
cwd: cliEntry.cwd,
|
||||
env: childEnv,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
}) as SpawnedRpcChild;
|
||||
|
||||
this.process = child;
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ export async function collectCapturesData(projectCwdOverride?: string): Promise<
|
|||
GSD_CAPTURES_BASE: projectCwd,
|
||||
},
|
||||
maxBuffer: CAPTURES_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
@ -136,6 +137,7 @@ export async function resolveCaptureAction(request: CaptureResolveRequest, proje
|
|||
GSD_CAPTURES_BASE: projectCwd,
|
||||
},
|
||||
maxBuffer: CAPTURES_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ export async function collectCleanupData(projectCwdOverride?: string): Promise<C
|
|||
GSD_CLEANUP_BASE: projectCwd,
|
||||
},
|
||||
maxBuffer: CLEANUP_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
@ -170,6 +171,7 @@ export async function executeCleanup(
|
|||
GSD_CLEANUP_SNAPSHOTS: JSON.stringify(pruneSnapshots),
|
||||
},
|
||||
maxBuffer: CLEANUP_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ function runDoctorChild(
|
|||
GSD_DOCTOR_SCOPE: scope ?? "",
|
||||
},
|
||||
maxBuffer: DOCTOR_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export async function collectExportData(
|
|||
GSD_EXPORT_FORMAT: format,
|
||||
},
|
||||
maxBuffer: EXPORT_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export async function collectForensicsData(projectCwdOverride?: string): Promise
|
|||
GSD_FORENSICS_BASE: projectCwd,
|
||||
},
|
||||
maxBuffer: FORENSICS_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export async function collectHistoryData(projectCwdOverride?: string): Promise<H
|
|||
GSD_HISTORY_BASE: projectCwd,
|
||||
},
|
||||
maxBuffer: HISTORY_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export async function collectHooksData(projectCwdOverride?: string): Promise<Hoo
|
|||
[HOOKS_MODULE_ENV]: hooksModulePath,
|
||||
},
|
||||
maxBuffer: HOOKS_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -491,6 +491,7 @@ async function collectRecoveryDiagnosticsChildPayload(
|
|||
GSD_RECOVERY_FORENSICS_MODULE: sessionForensicsModulePath,
|
||||
},
|
||||
maxBuffer: RECOVERY_DIAGNOSTICS_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ export async function collectSettingsData(projectCwdOverride?: string): Promise<
|
|||
GSD_SETTINGS_BASE: projectCwd,
|
||||
},
|
||||
maxBuffer: SETTINGS_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export async function collectSkillHealthData(projectCwdOverride?: string): Promi
|
|||
GSD_SKILL_HEALTH_BASE: projectCwd,
|
||||
},
|
||||
maxBuffer: SKILL_HEALTH_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ export async function executeUndo(projectCwdOverride?: string): Promise<UndoResu
|
|||
GSD_UNDO_BASE: projectCwd,
|
||||
},
|
||||
maxBuffer: UNDO_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export function triggerUpdate(targetVersion?: string): boolean {
|
|||
stdio: ["ignore", "ignore", "pipe"],
|
||||
// Detach so the child process is not killed if the parent exits
|
||||
detached: false,
|
||||
windowsHide: true,
|
||||
})
|
||||
|
||||
let stderr = ""
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ export async function collectVisualizerData(projectCwdOverride?: string): Promis
|
|||
GSD_VISUALIZER_BASE: projectCwd,
|
||||
},
|
||||
maxBuffer: VISUALIZER_MAX_BUFFER,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue