Files
bare-operating-system/packages/bare-os-booter/lib/bare-os-process-table.js
T
Raven Scott b1736b7a7a booter: apply 20-point Holepunch resilience and observability upgrades
add grouped retry orchestration, duplicate arbitration hardening, and transport-aware peer scoring
expand IPC with readiness gates, soft backpressure, request deadlines, batching, and method circuit-breakers
strengthen worker/control-plane lifecycle via idempotent termination, orphan detection, and liveness checkpoint helpers
improve lifecycle correctness with state-machine + transactional hook infrastructure and adaptive quiesce utilities
enrich operator surfaces/tests for degraded readiness, crash-safe telemetry patterns, and contract-level regressions
2026-04-26 07:22:31 -04:00

570 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Logical process table for `/proc`-style introspection (guest has no real PIDs).
*/
/** Stable synthetic initd pid mapping for the current booter session. */
const INITD_SYNTH_PID_BASE = 5100
/** @type {Map<string, number>} */
const initdSyntheticPidByName = new Map()
let nextInitdSyntheticPid = INITD_SYNTH_PID_BASE
/** @type {Map<number, { createdAtMs: number, lastStateChangeAtMs: number, lastState: string, completedAtMs?: number }>} */
const shellJobLifecycleById = new Map()
/** @type {Map<string, { createdAtMs: number, lastStateChangeAtMs: number, lastState: string, completedAtMs?: number }>} */
const initdLifecycleByName = new Map()
/**
* @param {string} unitName
*/
function syntheticInitdPidFor(unitName) {
const k = String(unitName || '').trim()
if (!k) return nextInitdSyntheticPid++
const existing = initdSyntheticPidByName.get(k)
if (Number.isFinite(existing)) return existing
const pid = nextInitdSyntheticPid++
initdSyntheticPidByName.set(k, pid)
return pid
}
/**
* @param {number} id
* @param {string} state
* @param {number} now
*/
function observeShellJobLifecycle(id, state, now) {
const cur = shellJobLifecycleById.get(id)
if (!cur) {
const row = { createdAtMs: now, lastStateChangeAtMs: now, lastState: state }
if (state === 'zombie') row.completedAtMs = now
shellJobLifecycleById.set(id, row)
return row
}
if (cur.lastState !== state) {
cur.lastState = state
cur.lastStateChangeAtMs = now
if (state === 'zombie' && !Number.isFinite(cur.completedAtMs)) cur.completedAtMs = now
}
return cur
}
/**
* @param {string} unit
* @param {string} state
* @param {number} startedAtMs
* @param {number} now
*/
function observeInitdLifecycle(unit, state, startedAtMs, now) {
const cur = initdLifecycleByName.get(unit)
if (!cur) {
const row = {
createdAtMs: Number.isFinite(startedAtMs) ? startedAtMs : now,
lastStateChangeAtMs: now,
lastState: state
}
if (state === 'failed' || state === 'dead' || state === 'inactive') row.completedAtMs = now
initdLifecycleByName.set(unit, row)
return row
}
if (cur.lastState !== state) {
cur.lastState = state
cur.lastStateChangeAtMs = now
if (
(state === 'failed' || state === 'dead' || state === 'inactive') &&
!Number.isFinite(cur.completedAtMs)
) {
cur.completedAtMs = now
}
if (state === 'active' || state === 'starting') cur.completedAtMs = undefined
}
return cur
}
/**
* @param {Record<string, string | undefined> | null | undefined} env
*/
function subprocessBridgeMetaFromEnv(env) {
if (!env || typeof env !== 'object') return null
const raw = String(env.BARE_OS_SUBPROCESS_BRIDGE_META_JSON || '')
.trim()
.slice(0, 8192)
if (!raw) return null
try {
const o = JSON.parse(raw)
if (!o || typeof o !== 'object' || Array.isArray(o)) return null
const schema =
typeof o.schema === 'number'
? o.schema
: typeof o.schemaVersion === 'number'
? o.schemaVersion
: null
const hostPidMap =
o.hostPidMap && typeof o.hostPidMap === 'object' && !Array.isArray(o.hostPidMap)
? o.hostPidMap
: null
return {
schema,
hostPidMapEntryCount: hostPidMap ? Object.keys(hostPidMap).length : null,
note: 'Non-secret summary of BARE_OS_SUBPROCESS_BRIDGE_META_JSON for bare-process / bare-subprocess alignment.'
}
} catch {
return { parseError: true }
}
}
/**
* @param {{
* sessionId?: string,
* bootStartedMs?: number,
* signalState?: Map<number, { signal: string, atMs: number }>,
* shellJobs?: Array<{ id?: number, done?: boolean, stopped?: boolean, pgid?: number, sid?: number, label?: string }>,
* ipcStats?: { processGroups?: Record<string, unknown> } | null,
* logicalFdRows?: Array<{ fd: number, target: string }>,
* initdReadiness?: { units?: Array<{ name: string, phase: string, startedAtMs: number, error?: string }>, schema?: number } | null,
* sigactionHandlers?: Record<string, string> | null,
* env?: Record<string, string | undefined> | null,
* niceByPid?: Map<number, number> | null
* }} opts
*/
function bareOsClampNice(n) {
if (!Number.isFinite(n)) return 0
return Math.max(-20, Math.min(19, Math.trunc(n)))
}
/**
* @param {number} pid
* @param {Map<number, number> | null | undefined} niceByPid
*/
function niceForPid(pid, niceByPid) {
if (!(niceByPid instanceof Map)) return 0
const v = niceByPid.get(pid)
return bareOsClampNice(typeof v === 'number' ? v : 0)
}
/**
* Logical `/proc/bare_os/process_maps.json` from a process table snapshot (not host VMAs).
* @param {ReturnType<typeof bareOsProcessTableSnapshot>} snap
*/
export function bareOsBuildLogicalMemoryMapsProc(snap) {
const processes = Array.isArray(snap.processes) ? snap.processes : []
/** @type {Record<string, unknown>[]} */
const regions = []
for (const p of processes) {
const pid = p && typeof p.pid === 'number' ? p.pid : NaN
if (!Number.isFinite(pid)) continue
const base = 0x40000000 + (pid | 0) * 0x00100000
const toHex = (x) => '0x' + (x >>> 0).toString(16)
regions.push({
pid,
start: toHex(base),
end: toHex(base + 0x80000),
perms: 'r--p',
kind: 'guest-text',
pathname: '/boot/init.js (logical guest image)'
})
regions.push({
pid,
start: toHex(base + 0x100000),
end: toHex(base + 0x300000),
perms: 'rw-p',
kind: 'guest-data',
pathname: '(anonymous logical)'
})
}
return {
schema: 2,
model: 'bare-os-logical-v1',
processTableSchemaRef:
typeof snap.schemaVersion === 'number'
? snap.schemaVersion
: typeof snap.schema === 'number'
? snap.schema
: null,
atMs: Date.now(),
note: 'Logical memory regions per synthetic PID for dashboards; not Linux /proc/pid/maps and not host VMAs.',
regions
}
}
/**
* @param {ReturnType<typeof bareOsProcessTableSnapshot>} snap
* @param {unknown} bareLib ctx.bare when available
*/
export function bareOsBuildProcessThreadsProc(snap, bareLib) {
const processes = Array.isArray(snap.processes) ? snap.processes : []
/** @type {Record<string, unknown>[]} */
const byPid = []
let bareThreadPresent = false
try {
const bt =
bareLib &&
typeof bareLib === 'object' &&
/** @type {{ bareThread?: unknown }} */ (bareLib).bareThread
bareThreadPresent = !!(bt && typeof bt === 'object')
} catch {
bareThreadPresent = false
}
for (const p of processes) {
const pid = p && typeof p.pid === 'number' ? p.pid : NaN
if (!Number.isFinite(pid)) continue
/** @type {Record<string, unknown>[]} */
const threads = [
{
tid: pid,
name: 'main',
state: String((p && p.state) || 'unknown'),
accountingSource: 'logical-main'
}
]
if (bareThreadPresent && pid === 3) {
threads.push({
tid: pid + 1_000_000,
name: 'bare-runtime',
state: 'attached',
accountingSource: 'ctx.bare.bareThread'
})
}
byPid.push({ pid, threads })
}
return {
schema: 2,
model: 'bare-os-logical-v1',
processTableSchemaRef:
typeof snap.schemaVersion === 'number'
? snap.schemaVersion
: typeof snap.schema === 'number'
? snap.schema
: null,
atMs: Date.now(),
note: 'One logical main thread per synthetic PID; optional bare-runtime row on PID 3 when ctx.bare exposes bareThread.',
bareThreadModulePresent: bareThreadPresent,
byPid
}
}
export function bareOsProcessTableSnapshot(opts = {}) {
const sid = String(opts.sessionId || '').trim().slice(0, 128)
const boot = typeof opts.bootStartedMs === 'number' ? opts.bootStartedMs : 0
const now = Date.now()
const sigState = opts.signalState instanceof Map ? opts.signalState : new Map()
const logicalRows = Array.isArray(opts.logicalFdRows) ? opts.logicalFdRows : []
const virtualSignalDeliveries = []
for (const [pid, v] of sigState.entries()) {
virtualSignalDeliveries.push({ pid, signal: v.signal, atMs: v.atMs })
}
const accountingOn =
opts.env &&
(opts.env.BARE_OS_PROCESS_ACCOUNTING === '1' ||
opts.env.BARE_OS_PROCESS_ACCOUNTING === 'true')
const accountingSource = accountingOn
? 'guest-accounting-env'
: 'synthetic-default'
const niceByPid = opts.niceByPid
const v8m = (pid) => ({
threads: 1,
cpuMsTotal: 0,
cpuMsWindow: 0,
readBytes: 0,
writeBytes: 0,
syscallsDelta: 0,
memRssBytes: null,
wchan: '',
ioPrio: null,
accountingSource,
nice: niceForPid(pid, niceByPid)
})
const mk = (pid, ppid, name, startedAtMs, pgid, sessionId) => {
const sig = sigState.get(pid)
const pg = typeof pgid === 'number' ? pgid : pid
const sidN = typeof sessionId === 'number' ? sessionId : 1
return {
pid,
ppid,
pgid: pg,
sid: sidN,
name,
state: sig ? 'signaled' : 'running',
startedAtMs,
lastSignal: sig ? sig.signal : undefined,
lastSignalAtMs: sig ? sig.atMs : undefined,
...v8m(pid)
}
}
const jobs = Array.isArray(opts.shellJobs) ? opts.shellJobs : []
const jobProcs = jobs
.filter((j) => j && !j.done && typeof j.id === 'number')
.map((j) => {
const pid = 4100 + j.id
const pgid = typeof j.pgid === 'number' ? j.pgid : pid
const session = typeof j.sid === 'number' ? j.sid : 1
const life = observeShellJobLifecycle(j.id, j.stopped ? 'stopped' : 'running', now)
return {
pid,
ppid: 3,
pgid,
sid: session,
name: 'bare-os-shell-job',
state: j.stopped ? 'stopped' : 'running',
startedAtMs: life.createdAtMs,
createdAtMs: life.createdAtMs,
lastStateChangeAtMs: life.lastStateChangeAtMs,
completedAtMs: life.completedAtMs,
jobId: j.id,
role: 'shell_job',
lifecyclePhase: j.stopped ? 'suspended' : 'running',
label: typeof j.label === 'string' ? j.label.slice(0, 256) : undefined,
stopped: !!j.stopped,
...v8m(pid)
}
})
const zombieJobs = jobs.filter((j) => j && j.done === true && typeof j.id === 'number')
const zombieProcs = zombieJobs.map((j) => {
const jid = typeof j.id === 'number' ? j.id : 0
const pid = 4200 + jid
const pgid = typeof j.pgid === 'number' ? j.pgid : pid
const session = typeof j.sid === 'number' ? j.sid : 1
const life = observeShellJobLifecycle(jid, 'zombie', now)
return {
pid,
ppid: 3,
pgid,
sid: session,
name: 'bare-os-shell-job',
state: 'zombie',
startedAtMs: life.createdAtMs,
createdAtMs: life.createdAtMs,
lastStateChangeAtMs: life.lastStateChangeAtMs,
completedAtMs: life.completedAtMs,
jobId: jid,
role: 'shell_job',
lifecyclePhase: 'closed',
label: typeof j.label === 'string' ? j.label.slice(0, 256) : undefined,
cwd: '/',
fds: [],
...v8m(pid)
}
})
const stdioTable = [
{ fd: 0, kind: 'stdio', target: '/dev/stdin' },
{ fd: 1, kind: 'stdio', target: '/dev/stdout' },
{ fd: 2, kind: 'stdio', target: '/dev/stderr' }
]
const extraFdTable = logicalRows.map((r) => ({
fd: r.fd | 0,
kind: 'logical',
target: String(r.target || '').slice(0, 1024)
}))
const sigHandlers =
opts.sigactionHandlers && typeof opts.sigactionHandlers === 'object'
? opts.sigactionHandlers
: null
const env = opts.env && typeof opts.env === 'object' ? opts.env : {}
const pipefailOn =
env.BARE_OS_SHELL_PIPEFAIL === '1' ||
env.BARE_OS_SHELL_PIPEFAIL === 'true'
const posixModeOn =
env.BARE_OS_SHELL_POSIX_MODE === '1' ||
env.BARE_OS_SHELL_POSIX_MODE === 'true'
const initd = opts.initdReadiness
const initdUnitsRaw =
initd && Array.isArray(initd.units) ? initd.units : []
/** @type {Array<Record<string, unknown>>} */
const initdLogicalProcs = []
let i = 0
for (const u of initdUnitsRaw) {
if (!u || typeof u.name !== 'string') continue
const pid = syntheticInitdPidFor(u.name)
i++
const pgid = pid
const nmLower = u.name.toLowerCase()
const replicationHint =
u.phase === 'failed' &&
/repl|swarm|net|peer|dht|hdms|hyperswarm/.test(nmLower)
? 'net-related-initd-failure'
: undefined
const life = observeInitdLifecycle(
u.name,
String(u.phase || ''),
typeof u.startedAtMs === 'number' ? u.startedAtMs : now,
now
)
initdLogicalProcs.push({
pid,
ppid: 2,
pgid,
sid: 1,
name: `bare-initd:${u.name.slice(0, 64)}`,
state:
u.phase === 'active'
? 'running'
: u.phase === 'failed'
? 'signaled'
: u.phase === 'starting'
? 'running'
: u.phase === 'stopping'
? 'sleeping'
: u.phase === 'skipped'
? 'sleeping'
: 'sleeping',
startedAtMs: typeof u.startedAtMs === 'number' ? u.startedAtMs : now,
createdAtMs: life.createdAtMs,
lastStateChangeAtMs: life.lastStateChangeAtMs,
completedAtMs: life.completedAtMs,
role: 'initd_unit',
initdUnit: u.name.slice(0, 128),
initdPhase: String(u.phase || '').slice(0, 32),
lastSignal: u.error ? 'error' : undefined,
cwd: '/',
fds: [],
fdTable: [],
...v8m(pid),
...(replicationHint ? { replicationHint } : {})
})
}
const baseProcesses = [
{ ...mk(1, 0, 'bare-os-kernel', boot || now, 1, 1), cwd: '/', fds: [], fdTable: [] },
{ ...mk(2, 1, 'bare-os-booter', boot || now, 2, 1), cwd: '/', fds: [], fdTable: [] },
{
...mk(3, 2, 'bare-os-shell', now, 3, 1),
sessionId: sid || undefined,
cwd: '/',
fds: [0, 1, 2],
fdTable: [...stdioTable, ...extraFdTable],
role: 'session_shell'
},
...initdLogicalProcs,
...jobProcs.map((p) => ({
...p,
cwd: typeof p.cwd === 'string' ? p.cwd : '/',
fds: Array.isArray(p.fds) ? p.fds : [0, 1, 2],
fdTable: [...stdioTable],
threads: p.threads ?? 1
})),
...zombieProcs
]
const pidToName = new Map()
for (const p of baseProcesses) {
if (p && typeof p.pid === 'number' && typeof p.name === 'string')
pidToName.set(p.pid, p.name)
}
const processes = baseProcesses.map((p) => ({
...p,
parentName:
typeof p.ppid === 'number' && pidToName.has(p.ppid)
? pidToName.get(p.ppid)
: undefined
}))
return {
schema: 9,
schemaVersion: 10,
note: 'Synthetic rows; Bare OS guests do not expose host OS processes. v10 adds lifecycle timestamps (createdAtMs/lastStateChangeAtMs/completedAtMs) for shell jobs and initd units. v9 adds per-row nice (-20..19) via ctx.bareOsRenice, accountingSource on accounting fields, and companions process_maps.json / process_threads.json (logical models). v8 optional accounting + initd replicationHint retained.',
signalModel: {
schema: 1,
note: 'Virtual deliveries via ctx.bareOsSendSignal (not full sigaction/sigprocmask).',
sigpendingAnalog: virtualSignalDeliveries,
sigprocmaskAnalog: []
},
signalRouting: {
schema: 1,
bareOsSendSignal:
'Delivers to synthetic pids 13 (kernel, booter, shell), shell job rows (41xx), and initd logical rows (51xx) when keyed by pid; see bareOsSendSignal implementation.',
killUtility:
'/bin/kill maps names kernel|booter|shell, numeric pids, and **negative pgid** (e.g. `kill -TERM -300` for logical group 300) to the same routing.',
trap:
'Shell builtins use ctx.shellTrapHandlers; signals are synthetic (no host kill).',
sessionShellPgid: 3
},
sigactionSurface: {
schema: 1,
note: 'Logical dispositions via ctx.bareOsSigaction(signal, IGNORE|DEFAULT); IGNORE suppresses delivery and session exit side effects.',
handlers:
sigHandlers && Object.keys(sigHandlers).length
? { ...sigHandlers }
: {}
},
exitStatusModel: {
schema: 1,
note: 'POSIX-like: pipeline exit is last stage exitCode; && stops on non-zero prior; || stops on zero prior.',
timeoutExit124:
'When BARE_OS_FEATURE_ABORT_TIMEOUT is advertised, /bin/timeout uses exit 124 on wall timeout.',
signalTerminated:
'Synthetic kills set high-bit style exit (128+n) only where shell documents; see shell + kernel-runner tests.'
},
session: {
sessionId: sid || undefined,
controllingTty: undefined,
foregroundPgid: 3,
sid: 1
},
processGroups: {
schema: 1,
note: 'Synthetic pgid/sid on shell jobs approximate POSIX process groups; guests have no host PIDs.',
setpgidAnalog:
'Background job start assigns pgid on shellBackgroundJobs rows where implemented.',
killpgAnalog:
'Negative numeric targets index **`shellBackgroundJobs`** by **`pgid`**; active jobs only (zombies excluded except **`kill -0`** existence checks). Not a full killpg(2) emulation.'
},
jobControlSemantics: {
schema: 1,
pipefail: pipefailOn,
posixShellMode: posixModeOn,
pipelineExitRule: pipefailOn
? 'first_failing_stage_in_pipeline'
: 'last_stage_posix_default',
waitReapsCompletedJobs: true,
waitExitCodeRule:
'When background jobs record lastExitCode, wait uses last non-zero exit among waited jobs (all-target wait); wait -n uses the first completed job exit.',
note: 'Mirrors BARE_OS_SHELL_PIPEFAIL and BARE_OS_SHELL_POSIX_MODE; see handbook ch.9 §3.'
},
ipcProcessGroups:
opts.ipcStats &&
opts.ipcStats.processGroups &&
typeof opts.ipcStats.processGroups === 'object'
? opts.ipcStats.processGroups
: undefined,
initdBinding: {
schema: 1,
note: 'initd units mirrored as logical pids 5100+ for POSIX-like ps/kill introspection.',
readinessSchema: initd?.schema,
unitCount: initdLogicalProcs.length
},
rlimits: {
note: 'Logical defaults; host rlimits may differ when subprocess bridge is active.',
nofile: { soft: 1024, hard: 4096 },
stack: { soft: 8388608, hard: 8388608 }
},
cwd: '/',
fdSummary: {
nextFd: 3 + extraFdTable.length,
reservedStdio: [0, 1, 2],
note: 'Additional FDs may appear when pseudo-files or IPC channels are opened.'
},
jobStats: {
running: jobProcs.filter((j) => !j.stopped).length,
stopped: jobProcs.filter((j) => j.stopped).length,
zombie: zombieJobs.length
},
subprocessBridgeMeta: subprocessBridgeMetaFromEnv(env),
processes,
atMs: now
}
}
/**
* Best-effort orphan detector for logical worker/subprocess rows.
* @param {ReturnType<typeof bareOsProcessTableSnapshot>} snap
*/
export function bareOsFindOrphanProcessRows(snap) {
const rows = Array.isArray(snap?.processes) ? snap.processes : []
const pids = new Set(rows.map((r) => Number(r && r.pid)).filter(Number.isFinite))
return rows.filter((r) => {
const ppid = Number(r && r.ppid)
if (!Number.isFinite(ppid) || ppid <= 0) return false
if (ppid === 1 || ppid === 2 || ppid === 3) return false
return !pids.has(ppid)
})
}