#!/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, dieAtState, // 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`, or at `dieAtState`'s // barrier when the claim patch there has that state. Lines on stdin: // "go" releases a held barrier; "close" closes and exits; "kill-engine" // closes with the engine killed; "launch " 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, TEST_ENGINE } 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 || (cfg.dieAtState?.[name] && detail?.patch?.state === cfg.dieAtState[name])) { 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: { cwd: fx.proj }, [TEST_ENGINE]: { command: process.execPath, preArgs: [fakePi], env: { ...process.env, ...(cfg.fakeEnv ?? {}) } }, 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); }