mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 02:34:57 +00:00
fix(crestodian): bound local command probes
This commit is contained in:
34
src/crestodian/probes.test.ts
Normal file
34
src/crestodian/probes.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { probeLocalCommand } from "./probes.js";
|
||||
|
||||
describe("crestodian probes", () => {
|
||||
it("bounds noisy local command probe output", async () => {
|
||||
const result = await probeLocalCommand(
|
||||
process.execPath,
|
||||
["-e", "process.stdout.write('x'.repeat(4096));"],
|
||||
{ outputLimit: 64, timeoutMs: 1_000 },
|
||||
);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.version).toHaveLength(64);
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"force-kills timed-out local command probes that ignore SIGTERM",
|
||||
async () => {
|
||||
const startedAt = Date.now();
|
||||
const result = await probeLocalCommand(
|
||||
process.execPath,
|
||||
["-e", "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);"],
|
||||
{ timeoutKillGraceMs: 25, timeoutMs: 25 },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
command: process.execPath,
|
||||
error: "timed out after 25ms",
|
||||
found: true,
|
||||
});
|
||||
expect(Date.now() - startedAt).toBeLessThan(2_000);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -7,38 +7,68 @@ export type LocalCommandProbe = {
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const LOCAL_COMMAND_PROBE_OUTPUT_MAX_CHARS = 16 * 1024;
|
||||
const LOCAL_COMMAND_PROBE_KILL_GRACE_MS = 500;
|
||||
|
||||
function appendBounded(previous: string, chunk: string, limit: number): string {
|
||||
const next = previous + chunk;
|
||||
return next.length > limit ? next.slice(-limit) : next;
|
||||
}
|
||||
|
||||
export async function probeLocalCommand(
|
||||
command: string,
|
||||
args: string[] = ["--version"],
|
||||
opts: { timeoutMs?: number } = {},
|
||||
opts: { outputLimit?: number; timeoutKillGraceMs?: number; timeoutMs?: number } = {},
|
||||
): Promise<LocalCommandProbe> {
|
||||
const timeoutMs = opts.timeoutMs ?? 1_500;
|
||||
const outputLimit = opts.outputLimit ?? LOCAL_COMMAND_PROBE_OUTPUT_MAX_CHARS;
|
||||
const timeoutKillGraceMs = opts.timeoutKillGraceMs ?? LOCAL_COMMAND_PROBE_KILL_GRACE_MS;
|
||||
return await new Promise((resolve) => {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let settled = false;
|
||||
let timedOut = false;
|
||||
let killTimer: NodeJS.Timeout | undefined;
|
||||
const child = spawn(command, args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
const timeoutResult = (): LocalCommandProbe => ({
|
||||
command,
|
||||
found: true,
|
||||
error: `timed out after ${timeoutMs}ms`,
|
||||
});
|
||||
const finish = (result: LocalCommandProbe) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
if (killTimer) {
|
||||
clearTimeout(killTimer);
|
||||
}
|
||||
resolve(result);
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill("SIGTERM");
|
||||
finish({ command, found: true, error: `timed out after ${timeoutMs}ms` });
|
||||
killTimer = setTimeout(
|
||||
() => {
|
||||
child.kill("SIGKILL");
|
||||
child.stdout.destroy();
|
||||
child.stderr.destroy();
|
||||
finish(timeoutResult());
|
||||
},
|
||||
Math.max(0, timeoutKillGraceMs),
|
||||
);
|
||||
killTimer.unref?.();
|
||||
}, timeoutMs);
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += String(chunk);
|
||||
stdout = appendBounded(stdout, String(chunk), outputLimit);
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += String(chunk);
|
||||
stderr = appendBounded(stderr, String(chunk), outputLimit);
|
||||
});
|
||||
child.on("error", (err: NodeJS.ErrnoException) => {
|
||||
finish({
|
||||
@@ -48,6 +78,10 @@ export async function probeLocalCommand(
|
||||
});
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
if (timedOut) {
|
||||
finish(timeoutResult());
|
||||
return;
|
||||
}
|
||||
const text = `${stdout}\n${stderr}`.trim().split(/\r?\n/)[0]?.trim();
|
||||
finish({
|
||||
command,
|
||||
|
||||
Reference in New Issue
Block a user