import { rmSync } from "node:fs"; import type { Content } from "../ledger/index.ts"; import { formalRun as runDefinition, judge, prepareJudging, prepareRun } from "../tasks/index.ts"; import type { HypothesisMetric, JudgedResult, ProcessResult } from "../vocab/index.ts"; import { checkoutVersion } from "../workspace/index.ts"; import { containerName, freshDir, judgeContainerName, runPaths, workspaceRepo } from "./paths.ts"; import type { Study } from "./study.ts"; // 共用动作二:跑一次正式运行(任务书 12 第 2.7 节)。先登记、再启动、再写结果,前后顺序由这个入口保证 // (方法.md 第七、九部分):登记占名额,确认启动才扣次,确认没启动的写结果"没启动"、退还名额。 // 判分不扣次数,不登记。恢复时接着等一次已经启动的运行,走同一个 finishRun。 export type RunPlan = Omit, "definition">; /** * 一次正式运行怎么结束:写完结果;被用户停下;或 docker run 没启动,附 Docker 的报错原文。 * 没启动是外部错误,本进程不重试(CLAUDE.md"错误怎么处理",重试 0 次):调用方写停下事件, * 下次启动时"下一步做什么"照旧起新编号重跑。 */ export type RunOutcome = "done" | "stopped" | { notStarted: string }; /** 跑一次正式运行:取出 version 的代码,在任务镜像里跑,判分,写结果。用户停下时不等容器,交回 "stopped"。 */ export async function formalRun(s: Study, plan: RunPlan, version: string, metrics: HypothesisMetric[]): Promise { const definition = runDefinition(s.task, s.frozen.image, String(s.frozen.seed)); s.ledger.append("runPlanned", { ...plan, definition }); const paths = runPaths(s.dir, plan.run); freshDir(paths.base); checkoutVersion(workspaceRepo(s.dir), version, freshDir(paths.code)); prepareRun(s.task, paths.code, freshDir(paths.run), metrics); const started = await s.runner.start(containerName(s.frozen.config.studyId, plan.run), definition, paths.run); if (!started.started) { s.ledger.append("runResult", { run: plan.run, process: { ending: "notStarted", reason: started.reason }, judged: null }); return { notStarted: started.reason }; } s.ledger.append("runStarted", { run: plan.run }); return finishRun(s, plan.run, metrics); } /** * 等一次已经确认启动的运行结束,判分,写结果,删掉容器。日志写进这次运行自己的日志目录。 * 收到"停下"信号就不等了,直接交回 "stopped";容器不管它,下次启动时恢复入口再来接着等。 */ export async function finishRun(s: Study, run: string, metrics: HypothesisMetric[]): Promise<"done" | "stopped"> { const name = containerName(s.frozen.config.studyId, run); const paths = runPaths(s.dir, run); const waited = await untilStopped(s.stop, s.runner.wait(name, emptyDir(paths.logs))); if (waited === "stopped") return "stopped"; const judged = await judgeRun(s, run, waited, metrics); s.ledger.append("runResult", { run, process: waited, judged }); await s.runner.remove(name); return "done"; } /** * 判分:tasks 的 prepareJudging,要跑校验器就让 runner 在镜像里跑,再交给 judge。 * 判分目录每次重新建空的(恢复时会再判一次);判分容器还在就先删掉再用。 */ async function judgeRun(s: Study, run: string, formal: ProcessResult, metrics: HypothesisMetric[]): Promise { const paths = runPaths(s.dir, run); const step = prepareJudging(s.task, paths.run, emptyDir(paths.judge), s.frozen.image, formal, metrics); if (step.status === "judged") return step.result; const name = judgeContainerName(s.frozen.config.studyId, run); if ((await s.runner.status(name)) !== "absent") await s.runner.remove(name); const started = await s.runner.start(name, step.definition, paths.judge); if (!started.started) throw new Error(`运行 ${run} 的校验器容器没有启动:${started.reason}`); const validator = await s.runner.wait(name, emptyDir(paths.judgeLogs)); await s.runner.remove(name); return judge(s.task, paths.run, paths.judge, formal, validator, metrics); } /** 清空重建一个目录:恢复时会再等一次、再判一次,上一次没写完的日志和判分目录不留。 */ function emptyDir(dir: string): string { rmSync(dir, { recursive: true, force: true }); return freshDir(dir); } /** 等一件事做完,或者收到"停下"信号先交回 "stopped"。被放下的那个 Promise 之后的结果没人用。 */ async function untilStopped(stop: AbortSignal, work: Promise): Promise { let onStop = (): void => undefined; const stopped = new Promise<"stopped">((resolve) => { onStop = () => resolve("stopped"); if (stop.aborted) onStop(); else stop.addEventListener("abort", onStop, { once: true }); }); try { return await Promise.race([work, stopped]); } finally { stop.removeEventListener("abort", onStop); } }