mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
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:
co-authored by
lorenzozanee
parent
c9e86a3b01
commit
37543d223a
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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 }));
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user