Controller, claim store, live-session guard, engine link and seal, turn tracker, cohort force stop and recovery, client library, transcript and mediated terminal, with the fake engine and tests. Fixtures only; no live cutover. Dewey built it. Darkwing (comment 26690) and Filbert (comment 26694) approved round 2. Manifest I1-r2-manifest.sha256 (2b48e333, 27 files). Suites on an export: conversation 152/152, control-board 124, webui 14, seat 19, chat-00/01/01c checks, and all nine scripts/test-*.sh green. Follow-ups for I3 are in DEFERRED. Gate E stays with Jason. Co-Authored-By: Claude Opus 5.5 <[email protected]>
89 lines
4.1 KiB
JavaScript
89 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
||
// A controller in its own process, for the crash fixtures (#1507: W5, W6,
|
||
// W12–W15, W20, H18, H21–H23, K-series). A test can't kill the controller it
|
||
// runs in, and the claim's owner check sees the test's own pid as live, so
|
||
// these fixtures run the controller here.
|
||
//
|
||
// argv[2] is JSON: { fx, launcher: "pgroup" | "scope", dieAt, holdAt,
|
||
// timeouts, host, fakeEnv, verifier }. Lines on stdout are JSON:
|
||
// { ready, ... } after start, { held: name } at a held barrier,
|
||
// { dying: name } just before a SIGKILL at `dieAt`. Lines on stdin:
|
||
// "go" releases a held barrier; "close" closes and exits; "kill-engine"
|
||
// closes with the engine killed; "launch <eligibility>" launches after a
|
||
// recover; "evidence" and "proof" print the controller's evidence and its
|
||
// verified cohort proof.
|
||
|
||
import { createInterface } from "node:readline";
|
||
import { join } from "node:path";
|
||
import { Controller } from "../src/controller.mjs";
|
||
import { AUTHORITY, PgroupLauncher, ScopeLauncher, systemdUnits } from "../src/cohort.mjs";
|
||
import { FixtureVerifier } from "../src/records.mjs";
|
||
import { defaultHost } from "../src/claim.mjs";
|
||
|
||
const cfg = JSON.parse(process.argv[2]);
|
||
const out = (v) => process.stdout.write(JSON.stringify(v) + "\n");
|
||
const dieAt = new Map(Object.entries(cfg.dieAt ?? {}));
|
||
const holdAt = new Set(cfg.holdAt ?? []);
|
||
let release = null;
|
||
|
||
// The test's stdin pipe is line-oriented control, not engine input.
|
||
const rl = createInterface({ input: process.stdin });
|
||
let ctrl = null;
|
||
rl.on("line", async (line) => {
|
||
if (line === "go" && release) {
|
||
const r = release;
|
||
release = null;
|
||
r();
|
||
} else if (line === "close" || line === "kill-engine") {
|
||
await ctrl?.close({ killEngine: line === "kill-engine" });
|
||
out({ closed: true });
|
||
process.exit(0);
|
||
} else if (line === "evidence") out({ evidence: ctrl?.evidence ?? null, binding: ctrl?.binding ?? null });
|
||
else if (line === "proof") out({ proof: ctrl?.stoppedProof?.proof ?? null });
|
||
else if (line.startsWith("launch ")) {
|
||
// The launcher's call after `recover` (Q15), never a client command.
|
||
try {
|
||
const r = await ctrl.launch(line.slice(7));
|
||
out({ launched: true, binding: r.binding, incarnation: ctrl.incarnation });
|
||
} catch (err) {
|
||
out({ launched: false, error: String(err.code ?? "error"), message: String(err.message) });
|
||
}
|
||
}
|
||
});
|
||
|
||
// `dieAt` maps a barrier name to the occurrence to die at (1 = first).
|
||
const seen = new Map();
|
||
async function barrier(name, detail) {
|
||
const n = (seen.get(name) ?? 0) + 1;
|
||
seen.set(name, n);
|
||
if (cfg.trace) out({ barrier: name, n });
|
||
if (dieAt.get(name) === n) {
|
||
out({ dying: name, n, detail });
|
||
process.kill(process.pid, "SIGKILL");
|
||
await new Promise(() => {});
|
||
}
|
||
if (holdAt.has(`${name}#${n}`) || (n === 1 && holdAt.has(name))) {
|
||
out({ held: name, n, detail });
|
||
await new Promise((r) => (release = r));
|
||
}
|
||
}
|
||
|
||
const host = cfg.host ? { machineId: () => cfg.host.machineId ?? defaultHost.machineId(), bootId: () => cfg.host.bootId ?? defaultHost.bootId() } : undefined;
|
||
const fx = cfg.fx;
|
||
const fakePi = join(import.meta.dirname, "fake-pi.mjs");
|
||
try {
|
||
ctrl = new Controller({
|
||
fixtureRoot: fx.base, claimRoot: fx.claimRoot, socketDir: cfg.socketDir ?? fx.socketDir, sessionFile: fx.sessionFile, seat: fx.seat,
|
||
launcher: cfg.launcher === "scope" ? new ScopeLauncher() : new PgroupLauncher(),
|
||
engine: { command: process.execPath, preArgs: [fakePi], env: { ...process.env, ...(cfg.fakeEnv ?? {}) }, cwd: fx.proj },
|
||
verifier: cfg.verifier === false ? null : new FixtureVerifier({ authorities: [AUTHORITY] }),
|
||
units: cfg.units === "absent" ? { lookup: async () => ({ state: "absent" }) } : systemdUnits,
|
||
host, barrier, timeouts: cfg.timeouts,
|
||
});
|
||
const started = await ctrl.start();
|
||
out({ ready: true, started, socketPath: ctrl.socketPath, incarnation: ctrl.incarnation, pid: process.pid, binding: ctrl.binding, claim: ctrl.claim ? { claimId: ctrl.claim.claimId, record: ctrl.claim.record } : null });
|
||
} catch (err) {
|
||
out({ error: String(err.code ?? "error"), message: String(err.message) });
|
||
process.exit(3);
|
||
}
|