mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
fix(cli): hide Windows child process consoles (#3529)
Rebase #3529 onto current main. Preserve all 16 issue-scoped Studio server, lint, and CLI child-process windowsHide options, including main's PowerShell null guards and stderr suppression in orphanCleanup. Regression tests continue to assert windowsHide at each scoped spawn site. #3476 and #3430 remain out of scope. Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com> Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com>
This commit is contained in:
co-authored by
miguel.sierra
parent
73aa71c9ec
commit
9097d539b1
@@ -0,0 +1,54 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { execFileMock, spawnMock } = vi.hoisted(() => ({
|
||||
execFileMock: vi.fn(),
|
||||
spawnMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:child_process", () => {
|
||||
const mocked = { execFile: execFileMock, spawn: spawnMock };
|
||||
return { ...mocked, default: mocked };
|
||||
});
|
||||
vi.mock("@hyperframes/parsers/ff-binaries", () => ({
|
||||
findFfBinary: (name: string) => `/fake/bin/${name}`,
|
||||
}));
|
||||
|
||||
import { probeMediaMetadata } from "./mediaMetadata.js";
|
||||
import { decodeAudioPeaks } from "./waveform.js";
|
||||
|
||||
describe("Studio child-process options", () => {
|
||||
it("hides the ffprobe console window", async () => {
|
||||
execFileMock.mockImplementation(
|
||||
(
|
||||
_command: string,
|
||||
_args: string[],
|
||||
_options: unknown,
|
||||
callback: (...args: unknown[]) => void,
|
||||
) => {
|
||||
callback(
|
||||
null,
|
||||
JSON.stringify({ streams: [{ codec_type: "video", codec_name: "h264" }] }),
|
||||
"",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await probeMediaMetadata("/tmp/clip.mp4");
|
||||
|
||||
expect(execFileMock.mock.calls[0]?.[2]).toEqual(expect.objectContaining({ windowsHide: true }));
|
||||
});
|
||||
|
||||
it("hides the waveform FFmpeg console window", async () => {
|
||||
const proc = new EventEmitter() as EventEmitter & { stdout: EventEmitter };
|
||||
proc.stdout = new EventEmitter();
|
||||
spawnMock.mockReturnValue(proc);
|
||||
|
||||
const peaks = decodeAudioPeaks("/tmp/audio.wav");
|
||||
proc.stdout.emit("data", Buffer.from(new Float32Array([0.5]).buffer));
|
||||
proc.emit("close", 0);
|
||||
await peaks;
|
||||
|
||||
expect(spawnMock.mock.calls[0]?.[2]).toEqual(expect.objectContaining({ windowsHide: true }));
|
||||
});
|
||||
});
|
||||
@@ -28,7 +28,7 @@ const execFileRunner: FfprobeRunner = (command, args, options) =>
|
||||
execFile(
|
||||
command,
|
||||
args,
|
||||
{ timeout: options?.timeout, maxBuffer: options?.maxBuffer },
|
||||
{ timeout: options?.timeout, maxBuffer: options?.maxBuffer, windowsHide: true },
|
||||
(error, stdout, stderr) => {
|
||||
if (error && error.code === "ENOENT") {
|
||||
resolvePromise({ status: null, stdout: "", stderr: "", error });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { validateUploadedMedia, validateUploadedMediaBuffer } from "./mediaValidation.js";
|
||||
|
||||
describe("validateUploadedMedia", () => {
|
||||
@@ -11,13 +11,18 @@ describe("validateUploadedMedia", () => {
|
||||
});
|
||||
|
||||
it("accepts video files with a video stream", () => {
|
||||
expect(
|
||||
validateUploadedMedia("/tmp/test.mp4", () => ({
|
||||
status: 0,
|
||||
stdout: JSON.stringify({ streams: [{ codec_type: "video" }] }),
|
||||
stderr: "",
|
||||
})),
|
||||
).toEqual({ ok: true });
|
||||
const runner = vi.fn(() => ({
|
||||
status: 0,
|
||||
stdout: JSON.stringify({ streams: [{ codec_type: "video" }] }),
|
||||
stderr: "",
|
||||
}));
|
||||
|
||||
expect(validateUploadedMedia("/tmp/test.mp4", runner)).toEqual({ ok: true });
|
||||
expect(runner).toHaveBeenCalledWith(
|
||||
"ffprobe",
|
||||
expect.any(Array),
|
||||
expect.objectContaining({ windowsHide: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects video files with no supported video stream", () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ const AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
|
||||
type FfprobeRunner = (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: { windowsHide: boolean },
|
||||
) => {
|
||||
status: number | null;
|
||||
stdout: string | Buffer;
|
||||
@@ -26,16 +27,11 @@ export function validateUploadedMedia(
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const result = runner("ffprobe", [
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"stream=codec_type",
|
||||
"-of",
|
||||
"json",
|
||||
"--",
|
||||
filePath,
|
||||
]);
|
||||
const result = runner(
|
||||
"ffprobe",
|
||||
["-v", "error", "-show_entries", "stream=codec_type", "-of", "json", "--", filePath],
|
||||
{ windowsHide: true },
|
||||
);
|
||||
|
||||
if (result.error?.code === "ENOENT") {
|
||||
return { ok: true };
|
||||
|
||||
@@ -28,14 +28,15 @@ function createFakeProc(): FakeProc {
|
||||
return proc;
|
||||
}
|
||||
|
||||
type SpawnCall = { command: string; args: string[]; proc: FakeProc };
|
||||
type SpawnImpl = (command: string, args: string[]) => FakeProc;
|
||||
type SpawnOptions = { windowsHide?: boolean };
|
||||
type SpawnCall = { command: string; args: string[]; options?: SpawnOptions; proc: FakeProc };
|
||||
type SpawnImpl = (command: string, args: string[], options?: SpawnOptions) => FakeProc;
|
||||
|
||||
function createSpawnSpy(): { spawn: SpawnImpl; calls: SpawnCall[] } {
|
||||
const calls: SpawnCall[] = [];
|
||||
const spawn: SpawnImpl = (command, args) => {
|
||||
const spawn: SpawnImpl = (command, args, options) => {
|
||||
const proc = createFakeProc();
|
||||
calls.push({ command, args, proc });
|
||||
calls.push({ command, args, options, proc });
|
||||
return proc;
|
||||
};
|
||||
return { spawn, calls };
|
||||
@@ -222,6 +223,7 @@ describe("resolveProxy", () => {
|
||||
await flush();
|
||||
|
||||
expect(calls[0]!.args).toEqual(["-hide_banner", "-filters"]);
|
||||
expect(calls[0]!.options?.windowsHide).toBe(true);
|
||||
calls[0]!.proc.stdout.emit(
|
||||
"data",
|
||||
Buffer.from(" ..C zscale V->V zimg scale\n T.C tonemap V->V tone map\n"),
|
||||
@@ -459,6 +461,7 @@ describe("resolveProxy", () => {
|
||||
const retry = resolveProxy(projectDir, sourcePath);
|
||||
await flush();
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(calls[1]!.options?.windowsHide).toBe(true);
|
||||
succeed(calls[1]!);
|
||||
await expect(retry).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -289,6 +289,7 @@ function ensureHdrFilters(ffmpegPath: string): Promise<void> {
|
||||
const promise = new Promise<void>((resolveCheck, rejectCheck) => {
|
||||
const proc = spawn(ffmpegPath, ["-hide_banner", "-filters"], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
let stdout = "";
|
||||
proc.stdout?.on("data", (chunk: Buffer) => {
|
||||
@@ -411,6 +412,7 @@ async function runFfmpeg(
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
timeout: TRANSCODE_TIMEOUT_MS,
|
||||
killSignal: "SIGKILL",
|
||||
windowsHide: true,
|
||||
});
|
||||
let stderrTail = "";
|
||||
proc.stderr?.on("data", (chunk: Buffer) => {
|
||||
|
||||
@@ -67,7 +67,7 @@ export function decodeAudioPeaks(audioPath: string): Promise<number[]> {
|
||||
"-vn",
|
||||
"pipe:1",
|
||||
],
|
||||
{ stdio: ["ignore", "pipe", "ignore"] },
|
||||
{ stdio: ["ignore", "pipe", "ignore"], windowsHide: true },
|
||||
);
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
Reference in New Issue
Block a user