New packages/runs reads run records, result.json, the release pointer
and the activation log without writing, pruning or following a link out
of the data root. mosaic-task.mjs list and show use it, so a malformed
result.json or a run that is a regular file no longer crashes them.
Deliberate deltas are listed in the README.
Rocko built it. Round 1 (ed3c5392) was approved with notes by Darkwing
(27115) and Filbert (27116); round 2 (2727198f, tests and wording only)
was approved by Darkwing (27123) and Filbert (27124). Sage's gate on
c9a25a47 plus the candidate: runs 41/0, queue 148/0, webui 22/0,
conversation 182/0 (181/1 in the full run on the K12 cgroup timing
test under load, 182/0 alone), control-board 124/0, every
scripts/test-*.sh 0 failed (task 98/0 with Docker, 26/0 without).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
73 lines
2.7 KiB
JavaScript
73 lines
2.7 KiB
JavaScript
import fs from "node:fs";
|
|
import { RunsError } from "./errors.mjs";
|
|
import { RUNS_DIRNAME, isMissing, readJsonObject, resolveInside } from "./paths.mjs";
|
|
|
|
export const RUN_ID_PATTERN = /^r-[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
export const RUN_DOCUMENTS = ["result.json", "task.json", "mission.json"];
|
|
|
|
export function isRunId(value) {
|
|
return typeof value === "string" && RUN_ID_PATTERN.test(value);
|
|
}
|
|
|
|
function requireRunId(runId) {
|
|
if (!isRunId(runId)) throw new RunsError(`invalid run id: ${JSON.stringify(runId)} (expected r-<id>)`);
|
|
}
|
|
|
|
// Every name under <dataRoot>/runs/ that starts with "r-", sorted. Run ids
|
|
// begin with a UTC stamp, so the order is oldest first. A missing data root
|
|
// or runs directory is an empty list.
|
|
export function listRunIds(dataRoot) {
|
|
const root = resolveInside(dataRoot, RUNS_DIRNAME);
|
|
if (root === null) return [];
|
|
let names;
|
|
try {
|
|
names = fs.readdirSync(root);
|
|
} catch (error) {
|
|
if (isMissing(error)) return [];
|
|
throw new RunsError(`cannot read ${root}: ${error.code ?? error.message}`);
|
|
}
|
|
return names.filter((name) => name.startsWith("r-")).sort();
|
|
}
|
|
|
|
// One of RUN_DOCUMENTS from a run, or null when it is missing, unreadable,
|
|
// not a JSON object or outside the data root.
|
|
export function readRunDocument(dataRoot, runId, name) {
|
|
requireRunId(runId);
|
|
if (!RUN_DOCUMENTS.includes(name)) throw new RunsError(`unknown run document: ${JSON.stringify(name)}`);
|
|
return readJsonObject(dataRoot, RUNS_DIRNAME, runId, name);
|
|
}
|
|
|
|
// [{ runId, result }] for every listed run; result is null for an
|
|
// incomplete or unreadable record. Names come from listRunIds, so a name
|
|
// that starts with "r-" but isn't a valid run id is still listed.
|
|
export function listRunRecords(dataRoot) {
|
|
return listRunIds(dataRoot).map((runId) => ({
|
|
runId,
|
|
result: readJsonObject(dataRoot, RUNS_DIRNAME, runId, "result.json"),
|
|
}));
|
|
}
|
|
|
|
// One run's documents and artifact names, or null when the run directory
|
|
// doesn't exist or isn't a directory. Artifacts are in directory order.
|
|
export function readRunRecord(dataRoot, runId) {
|
|
requireRunId(runId);
|
|
// The runs directory first, so a refusal names the link that escapes.
|
|
if (resolveInside(dataRoot, RUNS_DIRNAME) === null) return null;
|
|
const dir = resolveInside(dataRoot, RUNS_DIRNAME, runId);
|
|
if (dir === null) return null;
|
|
let artifacts;
|
|
try {
|
|
artifacts = fs.readdirSync(dir);
|
|
} catch (error) {
|
|
if (isMissing(error)) return null;
|
|
throw new RunsError(`cannot read ${dir}: ${error.code ?? error.message}`);
|
|
}
|
|
return {
|
|
runId,
|
|
result: readRunDocument(dataRoot, runId, "result.json"),
|
|
task: readRunDocument(dataRoot, runId, "task.json"),
|
|
mission: readRunDocument(dataRoot, runId, "mission.json"),
|
|
artifacts,
|
|
};
|
|
}
|