1
0
Files
DiAL/src/server/checker/command-runner.ts

152 lines
3.9 KiB
TypeScript

import type { CheckResult, ResolvedCommandTarget } from "./types";
import { checkCommandExpect } from "./expect/command";
import { errorFailure } from "./expect/failure";
async function readOutput(
stdout: ReadableStream<Uint8Array>,
stderr: ReadableStream<Uint8Array>,
kill: () => void,
maxBytes: number,
): Promise<{ stdout: string; stderr: string; exceeded: boolean }> {
let totalBytes = 0;
let exceeded = false;
let killed = false;
async function readStream(stream: ReadableStream<Uint8Array>): Promise<string> {
const reader = stream.getReader();
const decoder = new TextDecoder();
let text = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
totalBytes += value.byteLength;
text += decoder.decode(value, { stream: true });
if (totalBytes > maxBytes && !killed) {
exceeded = true;
killed = true;
try {
kill();
} catch {
/* best-effort kill */
}
}
}
} catch {
/* stream already closed */
} finally {
try {
reader.releaseLock();
} catch {
/* already released */
}
}
return text;
}
const [out, err] = await Promise.all([readStream(stdout), readStream(stderr)]);
return { stdout: out, stderr: err, exceeded };
}
export async function runCommandCheck(target: ResolvedCommandTarget): Promise<CheckResult> {
const timestamp = new Date().toISOString();
const start = performance.now();
let proc: ReturnType<typeof Bun.spawn>;
try {
proc = Bun.spawn([target.command.exec, ...target.command.args], {
cwd: target.command.cwd,
env: target.command.env,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});
} catch (error) {
const durationMs = Math.round(performance.now() - start);
return {
targetName: target.name,
timestamp,
matched: false,
durationMs,
statusDetail: null,
failure: errorFailure("exitCode", "spawn", error instanceof Error ? error.message : String(error)),
};
}
let timedOut = false;
const timeoutId = setTimeout(() => {
timedOut = true;
try {
proc.kill();
} catch {
/* best-effort kill */
}
}, target.timeoutMs);
let outputResult: { stdout: string; stderr: string; exceeded: boolean };
try {
outputResult = await readOutput(
proc.stdout as ReadableStream<Uint8Array>,
proc.stderr as ReadableStream<Uint8Array>,
() => proc.kill(),
target.command.maxOutputBytes,
);
} catch {
clearTimeout(timeoutId);
const durationMs = Math.round(performance.now() - start);
return {
targetName: target.name,
timestamp,
matched: false,
durationMs,
statusDetail: null,
failure: errorFailure("exitCode", "execution", "输出读取失败"),
};
}
await proc.exited;
clearTimeout(timeoutId);
const durationMs = Math.round(performance.now() - start);
const exitCode = proc.exitCode ?? 1;
if (outputResult.exceeded) {
return {
targetName: target.name,
timestamp,
matched: false,
durationMs,
statusDetail: `exitCode=${exitCode}`,
failure: errorFailure("exitCode", "output", `输出超过限制 ${target.command.maxOutputBytes} 字节`),
};
}
if (timedOut) {
return {
targetName: target.name,
timestamp,
matched: false,
durationMs,
statusDetail: null,
failure: errorFailure("exitCode", "timeout", `命令执行超时 (${target.timeoutMs}ms)`),
};
}
const obs = { exitCode, stdout: outputResult.stdout, stderr: outputResult.stderr, durationMs };
const expectResult = checkCommandExpect(obs, target.expect);
return {
targetName: target.name,
timestamp,
matched: expectResult.matched,
durationMs,
statusDetail: `exitCode=${exitCode}`,
failure: expectResult.failure,
};
}