← back to Fix Live Board
boardctl.mjs
47 lines
// Shared board control: spawn a detached board for a job, and report whether one is live.
import fs from 'fs';
import path from 'path';
import { spawn, spawnSync } from 'child_process';
import { fileURLToPath } from 'url';
export const ROOT = path.dirname(fileURLToPath(import.meta.url));
const RT = path.join(ROOT, '.runtime');
export function alive(pid) { if (!pid) return false; try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } }
export function boardStatus(jobId) {
const pidF = path.join(RT, jobId + '.pid'), portF = path.join(RT, jobId + '.port');
const pid = fs.existsSync(pidF) ? parseInt(fs.readFileSync(pidF, 'utf8'), 10) : 0;
const port = fs.existsSync(portF) ? fs.readFileSync(portF, 'utf8').trim() : '';
return { pid, port, up: alive(pid) && !!port };
}
export function spawnBoard(jobId) {
const s = boardStatus(jobId);
if (s.up) return { ...s, started: false, url: 'http://127.0.0.1:' + s.port + '/' };
const log = fs.openSync(path.join(RT, jobId + '.log'), 'a');
const child = spawn('node', ['server.js'], { cwd: ROOT, env: { ...process.env, JOB: 'jobs/' + jobId + '.json' }, detached: true, stdio: ['ignore', log, log] });
child.unref();
// wait up to ~5s for the server to write its port file
const portF = path.join(RT, jobId + '.port');
const before = fs.existsSync(portF) ? fs.statSync(portF).mtimeMs : 0;
for (let i = 0; i < 50; i++) {
if (fs.existsSync(portF) && fs.statSync(portF).mtimeMs >= before) { break; }
spawnSync('sleep', ['0.1']);
}
const port = fs.existsSync(portF) ? fs.readFileSync(portF, 'utf8').trim() : '';
return { pid: child.pid, port, up: !!port, started: true, url: port ? 'http://127.0.0.1:' + port + '/' : null };
}
// Count how many rows are "still to follow" (not fixed) for a job, by running its probe once.
export function brokenCount(jobId) {
const jobPath = path.join(ROOT, 'jobs', jobId + '.json');
const job = JSON.parse(fs.readFileSync(jobPath, 'utf8'));
const required = (job.fields || []).filter(f => !f.warn).map(f => f.key);
const cwd = job.cwd ? path.resolve(ROOT, job.cwd) : ROOT;
const r = spawnSync('/bin/sh', ['-c', job.probe], { cwd, encoding: 'utf8', timeout: job.probeTimeoutMs || 30000, env: process.env });
let rows = []; try { rows = JSON.parse(r.stdout); } catch (e) { return { ok: false, err: 'probe parse: ' + String(e).slice(0, 120) + (r.stderr ? ' | ' + r.stderr.slice(0, 120) : ''), total: 0, broken: 0 }; }
const fixed = rows.filter(row => typeof row.fixed === 'boolean' ? row.fixed : (required.length ? required.every(k => !!(row.fields || {})[k]) : false)).length;
return { ok: true, total: rows.length, broken: rows.length - fixed };
}