Files

116 lines
9.1 KiB
JavaScript
Executable File

#!/usr/bin/node
// sage-1004/1006 Window B rehearsal: a stand-in `systemctl --user`, based on the one in
// packages/cli/tests/stack-release.test.mjs. Unit state lives in $SD_STATE/units.json and every call
// goes to calls.log. Unlike the test's holder, a bus start runs the copy's real
// `scripts/mosaic bus start <business>` from the unit's WorkingDirectory, without --pm and under this
// scratch environment. Its output becomes journal lines. With --pm in ExecStart, the PM launch is
// EMULATED: no claude runs. A scratch runner process stands in for the session, with one
// sessions.json entry and one `launched` journal line. A Console start runs a scratch /healthz
// server on the unit's --port. Nothing here reads or writes a live unit, store or journal.
const fs = require('node:fs'), path = require('node:path'), cp = require('node:child_process');
const sd = process.env.SD_STATE, data = process.env.SD_DATA, unitDir = path.join(process.env.XDG_CONFIG_HOME, 'systemd', 'user');
const args = process.argv.slice(2).filter((a) => a !== '--user'), [verb, ...rest] = args;
fs.appendFileSync(path.join(sd, 'calls.log'), `${new Date().toISOString()} ${args.join(' ')}\n`);
const stFile = path.join(sd, 'units.json');
const load = () => { try { return JSON.parse(fs.readFileSync(stFile, 'utf8')); } catch { return {}; } };
const save = (s) => { fs.writeFileSync(stFile + '.tmp', JSON.stringify(s)); fs.renameSync(stFile + '.tmp', stFile); };
const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
const alive = (pid) => { try { const s = fs.readFileSync(`/proc/${pid}/stat`, 'utf8'); return s[s.lastIndexOf(')') + 2] !== 'Z'; } catch { return false; } };
const startTime = (pid) => { const s = fs.readFileSync(`/proc/${pid}/stat`, 'utf8'); return s.slice(s.lastIndexOf(')') + 2).split(' ')[19]; };
const isBus = (u) => /^mosaic-bus@[a-z0-9-]+\.service$/.test(u), inst = (u) => u.slice('mosaic-bus@'.length, -'.service'.length);
const journal = (unit, text) => fs.appendFileSync(path.join(sd, 'journal.jsonl'), JSON.stringify({ epoch: Math.floor(Date.now() / 1000), unit, text }) + '\n');
const unitFile = (u) => {
const main = isBus(u) ? '[email protected]' : u, file = path.join(unitDir, main);
if (!fs.existsSync(file)) return null;
const drops = [];
for (const d of isBus(u) ? [main + '.d', u + '.d'] : [u + '.d']) { const dir = path.join(unitDir, d); if (fs.existsSync(dir)) for (const f of fs.readdirSync(dir).sort()) if (f.endsWith('.conf')) drops.push(path.join(dir, f)); }
const r = { wd: '', exec: '', env: [], drops };
for (const f of [file, ...drops]) for (let line of fs.readFileSync(f, 'utf8').split('\n')) {
line = line.replace(/%i/g, isBus(u) ? inst(u) : '').replace(/%h/g, process.env.HOME);
const m = /^(WorkingDirectory|ExecStart|Environment)=(.*)$/.exec(line); if (!m) continue;
if (m[1] === 'WorkingDirectory') r.wd = m[2]; else if (m[1] === 'ExecStart') { if (m[2]) r.exec = m[2]; } else r.env.push(m[2]);
}
return r;
};
// Every fd above 2 is closed first, so no child holds stack-release.sh's lock (fd 9).
const CLOSE = 'for fd in /proc/self/fd/*; do n=${fd##*/}; [ "$n" -gt 2 ] && eval "exec $n>&-" 2>/dev/null; done; exec "$@"';
const spawnDetached = (argv, opts) => { const ch = cp.spawn('bash', ['-c', CLOSE, 'standin', ...argv], { detached: true, ...opts }); ch.unref(); return ch; };
const sessionsFile = path.join(data, 'bus-host', 'sessions.json');
const readSessions = () => { try { return JSON.parse(fs.readFileSync(sessionsFile, 'utf8')); } catch { return { sessionsVersion: 1, sessions: [] }; } };
const writeSessions = (doc) => { fs.mkdirSync(path.dirname(sessionsFile), { recursive: true, mode: 0o700 }); fs.writeFileSync(sessionsFile + '.tmp', JSON.stringify(doc, null, 2) + '\n', { mode: 0o600 }); fs.renameSync(sessionsFile + '.tmp', sessionsFile); };
if (verb === 'show') {
const u = rest[0], props = []; for (let i = 1; i < rest.length; i++) if (rest[i] === '-p') props.push(rest[++i]);
const f = unitFile(u), st = load()[u] || {};
for (const p of props) {
if (p === 'LoadState') console.log(`LoadState=${f ? 'loaded' : 'not-found'}`);
else if (p === 'WorkingDirectory') console.log(`WorkingDirectory=${f ? f.wd : ''}`);
else if (p === 'ExecStart') console.log(f && f.exec ? `ExecStart={ path=${f.exec.split(' ')[0]} ; argv[]=${f.exec} ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }` : 'ExecStart=');
else if (p === 'Environment') console.log(`Environment=${f ? f.env.join(' ') : ''}`);
else if (p === 'MainPID') console.log(`MainPID=${st.state === 'active' && st.pid ? st.pid : 0}`);
else if (p === 'DropInPaths') console.log(`DropInPaths=${f ? f.drops.join(' ') : ''}`);
}
process.exit(0);
}
if (verb === 'is-active') { const q = rest[0] === '--quiet', u = q ? rest[1] : rest[0], st = load()[u] || {}; let s = st.state || 'inactive'; if (s === 'active' && st.pid && !alive(st.pid)) s = 'failed'; if (!q) console.log(s); process.exit(s === 'active' ? 0 : 3); }
if (verb === 'is-enabled') { console.log('disabled'); process.exit(1); }
if (verb === 'daemon-reload') process.exit(0);
if (verb === 'list-units') {
const pats = rest.filter((a) => !a.startsWith('--')).map((p) => new RegExp('^' + p.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'));
for (const [u, s] of Object.entries(load())) if (pats.some((re) => re.test(u))) console.log(`${u} loaded ${s.state} ${s.state === 'active' ? 'running' : 'dead'} standin`);
process.exit(0);
}
if (verb === 'start') {
const u = rest[0], f = unitFile(u); if (!f) { console.error(`Unit ${u} not found.`); process.exit(5); }
const s = load();
if (!isBus(u)) {
const argv = f.exec.split(' '), i = argv.indexOf('--port'), port = i > 0 ? argv[i + 1] : '7332';
const ch = spawnDetached([process.execPath, path.join(__dirname, 'healthz.cjs'), port], { stdio: 'ignore', cwd: sd });
fs.appendFileSync(path.join(sd, 'pids.log'), `healthz ${ch.pid}\n`);
const end = Date.now() + 10000;
while (!fs.existsSync(path.join(sd, `healthz-${ch.pid}`))) { if (Date.now() > end) { console.error('healthz stand-in did not start'); process.exit(1); } sleep(20); }
s[u] = { state: 'active', pid: ch.pid, wd: f.wd }; save(s); process.exit(0);
}
const argv = f.exec.split(' '), pm = argv.slice(1).includes('--pm'), real = argv.filter((a) => a !== '--pm');
const env = { ...process.env }; for (const e of f.env) { const k = e.slice(0, e.indexOf('=')); if (k === 'PATH' || k === 'MOSAIC_CLAUDE_BIN') env[k] = e.slice(k.length + 1); }
const out = path.join(sd, `bus-${Date.now()}.out`), fd = fs.openSync(out, 'a');
const ch = spawnDetached(real, { stdio: ['ignore', fd, fd], cwd: f.wd, env });
fs.closeSync(fd);
fs.appendFileSync(path.join(sd, 'pids.log'), `bus ${ch.pid} ${real.join(' ')} > ${out}\n`);
const end = Date.now() + 60000; let text = '';
while (!/^bus host up/m.test(text)) {
if (!alive(ch.pid)) { for (const l of text.split('\n').filter(Boolean)) journal(u, l); s[u] = { state: 'failed', pid: null, wd: f.wd }; save(s); console.error(`bus exited before host up; see ${out}`); process.exit(1); }
if (Date.now() > end) { console.error(`bus host up not seen in 60 s; see ${out}`); process.exit(1); }
sleep(100); text = fs.readFileSync(out, 'utf8');
}
for (const l of text.split('\n').filter(Boolean)) journal(u, l);
s[u] = { state: 'active', pid: ch.pid, wd: f.wd, out, journaled: text.length };
if (pm) {
// The emulated PM launch: a runner that only waits, one sessions.json entry, one journal line.
const runner = spawnDetached([process.execPath, '-e', 'setInterval(()=>{},1<<30)'], { stdio: 'ignore', cwd: sd });
const end2 = Date.now() + 5000; while (!fs.existsSync(`/proc/${runner.pid}/stat`) && Date.now() < end2) sleep(10);
const run = `remul${runner.pid}`, b = inst(u);
const doc = readSessions(); doc.sessions.push({ run, business: b, instance: 'chief', harness: 'claude-code', model: 'emulated', launchedBy: 'standin-pm-emulation', runnerPid: runner.pid, runnerStartTime: startTime(runner.pid), startedAt: new Date().toISOString() }); writeSessions(doc);
journal(u, `launched chief: run ${run}, claude-code emulated`);
fs.appendFileSync(path.join(sd, 'pids.log'), `runner ${runner.pid} ${run}\n`);
s[u].runner = runner.pid; s[u].run = run;
}
save(s); process.exit(0);
}
if (verb === 'stop') {
const u = rest[0], s = load(), st = s[u];
if (st && st.pid) {
try { process.kill(st.pid, 'SIGTERM'); } catch {}
const end = Date.now() + 20000; while (alive(st.pid) && Date.now() < end) sleep(50);
if (alive(st.pid)) { fs.appendFileSync(path.join(sd, 'pids.log'), `SIGKILL ${st.pid}\n`); try { process.kill(st.pid, 'SIGKILL'); } catch {} }
if (st.out) { const text = fs.readFileSync(st.out, 'utf8'); for (const l of text.slice(st.journaled || 0).split('\n').filter(Boolean)) journal(u, l); }
}
if (st && st.runner) {
try { process.kill(st.runner, 'SIGTERM'); } catch {}
const doc = readSessions(); doc.sessions = doc.sessions.filter((x) => x.run !== st.run); writeSessions(doc);
}
if (st) { s[u] = { wd: st.wd, state: 'inactive', pid: null }; save(s); }
process.exit(0);
}
console.error(`stand-in systemctl: unsupported: ${args.join(' ')}`); process.exit(1);