fix(cli): hide the Windows console for the port lookup and clipboard (#6449)

Co-authored-by: lorenzozanee <lorenzozanee@users.noreply.github.com>
This commit is contained in:
Miguel Ángel
2026-10-26 17:25:01 +00:00
committed by GitHub
co-authored by lorenzozanee
parent c9e86a3b01
commit 37543d223a
4 changed files with 143 additions and 2 deletions
+4 -1
View File
@@ -217,7 +217,10 @@ async function getProcessOnPort(port: number): Promise<string | null> {
async function windowsListenerPid(port: number): Promise<string | null> {
try {
const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "tcp"], { timeout: 4000 });
const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "tcp"], {
timeout: 4000,
windowsHide: true,
});
for (const line of stdout.split(/\r?\n/)) {
const columns = line.trim().split(/\s+/);
if (columns.length < 5 || columns[3] !== "LISTENING") continue;
@@ -0,0 +1,101 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createServer as createHttpServer, type Server as HttpServer } from "node:http";
import { promisify } from "node:util";
const { execFileMock } = vi.hoisted(() => ({ execFileMock: vi.fn() }));
vi.mock("node:child_process", () => ({ execFile: execFileMock }));
// portUtils promisifies execFile at import time. The mock has no
// promisify.custom implementation, so teach it to resolve { stdout, stderr }
// like the real child_process.execFile does.
type ExecCallback = (error: unknown, stdout: string, stderr: string) => void;
(
execFileMock as unknown as Record<
symbol,
(command: string, args: string[], options: unknown) => Promise<unknown>
>
)[promisify.custom] = (command: string, args: string[], options: unknown) =>
new Promise((resolve, reject) => {
(execFileMock as unknown as (...call: unknown[]) => void)(command, args, options, ((
error: unknown,
stdout: string,
stderr: string,
) => (error ? reject(error) : resolve({ stdout, stderr }))) as unknown);
});
const { activeServerOnPort } = await import("./portUtils.js");
const openHttpServers: HttpServer[] = [];
async function startConfigProbeServer(port: number): Promise<void> {
const server = createHttpServer((_req, res) => {
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify({
isHyperframes: true,
projectName: "demo-project",
projectDir: "/tmp/demo-project",
serverBuildSignature: null,
version: "0.6.42",
pid: 4242,
}),
);
});
openHttpServers.push(server);
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(port, "127.0.0.1", () => resolve());
});
}
describe("port listener lookup child-process options", () => {
const originalPlatform = process.platform;
beforeEach(() => {
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
execFileMock.mockImplementation(
(_command: string, _args: string[], _options: unknown, callback: ExecCallback) => {
callback(null, "", "");
},
);
});
afterEach(async () => {
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
vi.clearAllMocks();
await Promise.all(
openHttpServers
.splice(0)
.map((s) => new Promise<void>((resolve) => s.close(() => resolve()))),
);
});
it("hides the netstat console window used for Windows listener lookup", async () => {
// Occupy an ephemeral port with a HyperFrames config responder so the
// default OS listener lookup runs instead of the injected stub.
const ephemeral = createHttpServer();
await new Promise<void>((resolve) => ephemeral.listen(0, "127.0.0.1", () => resolve()));
const port = (ephemeral.address() as import("node:net").AddressInfo).port;
await new Promise<void>((resolve) => ephemeral.close(() => resolve()));
await startConfigProbeServer(port);
execFileMock.mockImplementation(
(_command: string, _args: string[], _options: unknown, callback: ExecCallback) => {
callback(
null,
` TCP 127.0.0.1:${port} 0.0.0.0:0 LISTENING 1234\r\n`,
"",
);
},
);
const server = await activeServerOnPort(port);
expect(execFileMock).toHaveBeenCalledTimes(1);
expect(execFileMock.mock.calls[0]?.[0]).toBe("netstat");
expect(execFileMock.mock.calls[0]?.[2]).toEqual(expect.objectContaining({ windowsHide: true }));
expect(server?.pid).toBe("1234");
expect(server?.pidSource).toBe("os");
});
});
+2 -1
View File
@@ -35,7 +35,7 @@ function detectProvider(): ClipboardProvider | undefined {
];
const cmd = process.platform === "win32" ? "where" : "which";
for (const p of candidates) {
const result = spawnSync(cmd, [p.cmd], { stdio: "ignore" });
const result = spawnSync(cmd, [p.cmd], { stdio: "ignore", windowsHide: true });
if (result.status === 0) return p;
}
return undefined;
@@ -53,6 +53,7 @@ export function copyToClipboard(text: string): boolean {
encoding: "utf-8",
stdio: ["pipe", "ignore", "ignore"],
timeout: COPY_TIMEOUT_MS,
windowsHide: true,
});
return res.status === 0;
} catch {
@@ -0,0 +1,36 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { spawnSyncMock } = vi.hoisted(() => ({ spawnSyncMock: vi.fn() }));
vi.mock("node:child_process", () => ({ spawnSync: spawnSyncMock }));
vi.mock("node:os", () => ({ platform: () => "linux" }));
import { copyToClipboard } from "./clipboard.js";
describe("clipboard child-process options", () => {
const originalPlatform = process.platform;
beforeEach(() => {
// Exercise the `where`-based provider probe: os.platform() reports a
// non-Windows OS (so the candidate loop runs) while the probe itself
// resolves `where`, the branch taken on Windows.
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
spawnSyncMock.mockReturnValue({ status: 0 });
});
afterEach(() => {
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
vi.clearAllMocks();
});
it("hides console windows for provider probing and copying", () => {
expect(copyToClipboard("snippet")).toBe(true);
// One `where` probe plus the provider copy itself.
expect(spawnSyncMock.mock.calls.length).toBeGreaterThanOrEqual(2);
expect(spawnSyncMock.mock.calls[0]?.[0]).toBe("where");
for (const call of spawnSyncMock.mock.calls) {
expect(call[2]).toEqual(expect.objectContaining({ windowsHide: true }));
}
});
});