import type { CheckResult, ResolvedCommandTarget } from "./types"; import { checkCommandExpect } from "./expect/command"; import { errorFailure } from "./expect/failure"; async function readOutput( stdout: ReadableStream, stderr: ReadableStream, 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): Promise { 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 { const timestamp = new Date().toISOString(); const start = performance.now(); let proc: ReturnType; 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, proc.stderr as ReadableStream, () => 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, }; }