fix: harden CLI runner for Windows shims
This commit is contained in:
parent
32338932de
commit
37b964450c
|
|
@ -1,8 +1,21 @@
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
|
|
||||||
|
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
function cmdQuote(value: string): string {
|
||||||
|
// ponytail: minimal cmd.exe quoting for Windows .cmd shims; callers still pass argv, not shell text.
|
||||||
|
return `"${value.replace(/(["^&|<>()%!])/g, "^$1")}"`;
|
||||||
|
}
|
||||||
|
|
||||||
export function runCli(bin: string, args: string[], timeoutMs = 120_000): Promise<string> {
|
export function runCli(bin: string, args: string[], timeoutMs = 120_000): Promise<string> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const child = spawn(bin, args, {
|
const isWin = process.platform === "win32";
|
||||||
|
const command = isWin ? "cmd.exe" : bin;
|
||||||
|
const commandArgs = isWin
|
||||||
|
? ["/d", "/s", "/c", [cmdQuote(bin), ...args.map(cmdQuote)].join(" ")]
|
||||||
|
: args;
|
||||||
|
|
||||||
|
const child = spawn(command, commandArgs, {
|
||||||
shell: false,
|
shell: false,
|
||||||
timeout: timeoutMs,
|
timeout: timeoutMs,
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
|
|
@ -10,13 +23,30 @@ export function runCli(bin: string, args: string[], timeoutMs = 120_000): Promis
|
||||||
|
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
let killedForOutput = false;
|
||||||
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
const append = (target: "stdout" | "stderr", chunk: Buffer) => {
|
||||||
|
if (stdout.length + stderr.length + chunk.length > MAX_OUTPUT_BYTES) {
|
||||||
|
killedForOutput = true;
|
||||||
|
child.kill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target === "stdout") stdout += chunk.toString();
|
||||||
|
else stderr += chunk.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
child.stdout.on("data", (d) => append("stdout", d));
|
||||||
|
child.stderr.on("data", (d) => append("stderr", d));
|
||||||
child.on("error", (err) => resolve(`[CLI error: ${err.message.slice(0, 200)}]`));
|
child.on("error", (err) => resolve(`[CLI error: ${err.message.slice(0, 200)}]`));
|
||||||
child.on("close", (code) => {
|
child.on("close", (code) => {
|
||||||
const out = stdout.trim();
|
if (killedForOutput) {
|
||||||
if (out) resolve(out);
|
resolve(`[CLI error: output exceeded ${MAX_OUTPUT_BYTES} bytes]`);
|
||||||
else resolve(`[CLI exited ${code ?? "unknown"}: ${stderr.slice(0, 200) || "no output"}]`);
|
return;
|
||||||
|
}
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(stdout.trim());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(`[CLI exited ${code ?? "unknown"}: ${(stderr || stdout).slice(0, 400) || "no output"}]`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue