This commit is contained in:
2026-08-18 18:11:28 -04:00
parent 0e9a650fa2
commit bbaf47028f
259 changed files with 0 additions and 0 deletions
@@ -0,0 +1,569 @@
/**
* 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)
})
}