chore(web): expose live continuation status
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-30 16:35:42 +08:00
parent 4743e12608
commit 2a304b6576
3 changed files with 165 additions and 2 deletions

View File

@@ -0,0 +1,138 @@
import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const abs = (file) => path.join(root, file);
const controlRoot = "tests/golden/WBV2";
const guardianLockPath = `${controlRoot}/continuous-guardian.lock`;
const supervisorLockPath = `${controlRoot}/continuous-supervisor.lock`;
const guardianCheckpointPath = `${controlRoot}/continuous-guardian.json`;
const supervisorCheckpointPath = `${controlRoot}/continuous-supervisor.json`;
const supervisorLogPath = `${controlRoot}/continuous-supervisor.log`;
const argv = process.argv.slice(2);
const watch = argv.includes("--watch");
const jsonOnly = argv.includes("--json");
const intervalIndex = argv.indexOf("--interval-ms");
const intervalMs = Number(intervalIndex >= 0 ? argv[intervalIndex + 1] : 2000);
const knownArgs = new Set(["--watch", "--json", "--interval-ms"]);
for (let index = 0; index < argv.length; index += 1) {
if (!knownArgs.has(argv[index])) throw new Error(`unknown argument: ${argv[index]}`);
if (argv[index] === "--interval-ms") index += 1;
}
if (!Number.isSafeInteger(intervalMs) || intervalMs < 100) throw new Error("--interval-ms must be an integer >= 100");
const readJson = (file) => {
try { return JSON.parse(fs.readFileSync(abs(file), "utf8")); }
catch { return null; }
};
const pidAlive = (pid) => {
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
try {
process.kill(pid, 0);
return true;
}
catch (error) {
return error?.code === "EPERM";
}
};
const lockInfo = (file) => {
const lock = readJson(file);
return lock ? { pid: lock.pid ?? null, startedAt: lock.startedAt ?? null, alive: pidAlive(lock.pid) } : { pid: null, startedAt: null, alive: false };
};
const currentPointer = () => {
const queue = fs.readFileSync(abs("docs/EXECUTION_QUEUE.md"), "utf8");
return {
task: queue.match(/\| 当前任务 \| `(WBV2-P\d+-\d+)` \|/u)?.[1] ?? null,
parentManifest: queue.match(/\| parent manifest \| `([^`]+)` \|/u)?.[1] ?? null,
};
};
const gitHead = () => {
try { return execFileSync("git", ["rev-parse", "--verify", "HEAD"], { cwd: root, encoding: "utf8" }).trim(); }
catch { return null; }
};
const tail = (file, maxBytes = 256 * 1024) => {
try {
const stat = fs.statSync(abs(file));
const start = Math.max(0, stat.size - maxBytes);
const handle = fs.openSync(abs(file), "r");
const buffer = Buffer.alloc(stat.size - start);
fs.readSync(handle, buffer, 0, buffer.length, start);
fs.closeSync(handle);
return buffer.toString("utf8");
}
catch { return ""; }
};
const latestHandoff = () => {
const matches = [...tail(supervisorLogPath).matchAll(/handoff iteration=(\d+) before=(WBV2-P\d+-\d+) after=(WBV2-P\d+-\d+)/gu)];
const match = matches.at(-1);
return match ? { iteration: Number(match[1]), before: match[2], after: match[3] } : null;
};
const pushVerification = (task) => {
if (!task) return null;
const file = `tests/golden/${task}/continuous-push-verify.log`;
if (!fs.existsSync(abs(file))) return null;
const output = tail(file, 4096).trim();
const match = output.match(/local_head=([0-9a-f]+) remote_head=([0-9a-f]+)/u);
return { path: file, localHead: match?.[1] ?? null, remoteHead: match?.[2] ?? null, equal: Boolean(match && match[1] === match[2]) };
};
const collect = () => {
const queue = currentPointer();
const guardian = readJson(guardianCheckpointPath);
const supervisor = readJson(supervisorCheckpointPath);
const guardianLock = lockInfo(guardianLockPath);
const supervisorLock = lockInfo(supervisorLockPath);
const handoff = latestHandoff();
const active = guardianLock.alive && supervisorLock.alive;
const blocked = guardian?.state === "blocked" || supervisor?.state === "blocked";
const state = blocked ? "BLOCKED" : active ? "RUNNING" : "IDLE";
const phase = blocked ? "blocked" : supervisor?.state === "running" ? "task-or-gate" : supervisor?.state ?? "unknown";
const observed = new Date().toISOString();
return {
schemaVersion: 1,
observedAt: observed,
state,
phase,
message: state === "RUNNING"
? `RUNNING: ${queue.task ?? "unknown task"} is owned by the guardian/supervisor process; wait for its handoff.`
: state === "BLOCKED" ? "BLOCKED: inspect the checkpoint reason before any write or retry." : "IDLE: no live guardian/supervisor lock was found.",
queue,
guardian: { pid: guardian?.pid ?? guardianLock.pid, alive: guardianLock.alive, state: guardian?.state ?? null, updatedAt: guardian?.updatedAt ?? null, heartbeatAt: guardian?.heartbeatAt ?? null, restartCount: guardian?.restartCount ?? null },
supervisor: { pid: supervisor?.pid ?? supervisorLock.pid, alive: supervisorLock.alive, state: supervisor?.state ?? null, updatedAt: supervisor?.updatedAt ?? null, iteration: supervisor?.iteration ?? null, before: supervisor?.before ?? null, after: supervisor?.after ?? null },
latestHandoff: handoff,
latestPushVerification: pushVerification(handoff?.before ?? supervisor?.before?.task),
localHead: gitHead(),
checkpoints: { guardian: guardianCheckpointPath, supervisor: supervisorCheckpointPath },
};
};
const format = (status) => [
`状态: ${status.state}${status.state === "RUNNING" ? "(正在执行)" : status.state === "BLOCKED" ? "(已阻塞)" : "(未运行)"}`,
`阶段: ${status.phase}`,
`当前任务: ${status.queue.task ?? "unknown"}`,
`parent manifest: ${status.queue.parentManifest ?? "unknown"}`,
`guardian: pid=${status.guardian.pid ?? "none"} alive=${status.guardian.alive} state=${status.guardian.state ?? "unknown"} heartbeat=${status.guardian.heartbeatAt ?? "none"}`,
`supervisor: pid=${status.supervisor.pid ?? "none"} alive=${status.supervisor.alive} state=${status.supervisor.state ?? "unknown"} iteration=${status.supervisor.iteration ?? "none"}`,
`最近交接: ${status.latestHandoff ? `${status.latestHandoff.before} -> ${status.latestHandoff.after} (#${status.latestHandoff.iteration})` : "none"}`,
`本地 HEAD: ${status.localHead ?? "unknown"}`,
`最近 push 校验: ${status.latestPushVerification?.equal ? "PASS" : status.latestPushVerification ? "NOT_EQUAL_OR_INCOMPLETE" : "none"}`,
`观察时间: ${status.observedAt}`,
status.message,
].join("\n");
const print = () => {
const status = collect();
if (jsonOnly) {
process.stdout.write(`${JSON.stringify(status)}\n`);
return;
}
if (watch) process.stdout.write("\x1b[2J\x1b[H");
process.stdout.write(`${format(status)}\n`);
};
print();
if (watch) {
const timer = setInterval(print, intervalMs);
const stop = () => { clearInterval(timer); process.exitCode = 0; };
process.once("SIGINT", stop);
process.once("SIGTERM", stop);
}

View File

@@ -72,7 +72,11 @@ const releaseLock = () => {
catch (error) { if (error?.code !== "ENOENT") throw error; }
};
const appendLog = (text) => fs.appendFileSync(abs(logPath), `[${new Date().toISOString()}] ${text}\n`);
const writeCheckpoint = (payload) => fs.writeFileSync(abs(checkpointPath), `${JSON.stringify({ schemaVersion: 1, pid: process.pid, updatedAt: new Date().toISOString(), ...payload }, null, 2)}\n`);
const writeCheckpoint = (payload) => {
const temporaryPath = `${checkpointPath}.tmp-${process.pid}`;
fs.writeFileSync(abs(temporaryPath), `${JSON.stringify({ schemaVersion: 1, pid: process.pid, updatedAt: new Date().toISOString(), ...payload }, null, 2)}\n`);
fs.renameSync(abs(temporaryPath), abs(checkpointPath));
};
const readJson = (file) => {
try { return JSON.parse(fs.readFileSync(abs(file), "utf8")); }
catch { return null; }
@@ -90,6 +94,23 @@ const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, mil
let child;
let stopRequested = false;
let stopSignal;
let heartbeatTimer;
const writeHeartbeat = () => {
if (stopRequested || released) return;
try {
writeCheckpoint({
state: "running",
restartCount,
current: currentTask(),
supervisorPid: child?.pid ?? null,
heartbeatAt: new Date().toISOString(),
supervisor: supervisorCommand,
});
}
catch (error) {
appendLog(`heartbeat-failed error=${error?.message ?? String(error)}`);
}
};
const signalChild = (signal) => {
if (!child?.pid) return;
try { process.kill(-child.pid, signal); }
@@ -137,6 +158,7 @@ let restartCount = 0;
try {
appendLog(`started pid=${process.pid} supervisor=${supervisorCommand} maxRestarts=${maxRestarts} restartDelayMs=${restartDelayMs}`);
writeCheckpoint({ state: "started", restartCount, supervisor: supervisorCommand, current: currentTask() });
heartbeatTimer = setInterval(writeHeartbeat, 2000);
while (true) {
if (stopRequested) {
writeCheckpoint({ state: "stopped", restartCount, reason: stopSignal, current: currentTask() });
@@ -183,6 +205,7 @@ catch (error) {
appendLog(`blocked reason=guardian_error error=${error?.message ?? String(error)}`);
}
finally {
if (heartbeatTimer !== undefined) clearInterval(heartbeatTimer);
signalChild("SIGTERM");
releaseLock();
}

View File

@@ -97,7 +97,9 @@ function appendLog(text) {
fs.appendFileSync(abs(supervisorLog), `[${new Date().toISOString()}] ${text}\n`);
}
function writeCheckpoint(payload) {
fs.writeFileSync(abs(checkpointPath), `${JSON.stringify({ schemaVersion: 1, pid: process.pid, updatedAt: new Date().toISOString(), ...payload }, null, 2)}\n`);
const temporaryPath = `${checkpointPath}.tmp-${process.pid}`;
fs.writeFileSync(abs(temporaryPath), `${JSON.stringify({ schemaVersion: 1, pid: process.pid, updatedAt: new Date().toISOString(), ...payload }, null, 2)}\n`);
fs.renameSync(abs(temporaryPath), abs(checkpointPath));
}
const run = (command, args = []) => {
const result = spawnSync(command, args, { cwd: root, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });