/** * 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} */ const initdSyntheticPidByName = new Map() let nextInitdSyntheticPid = INITD_SYNTH_PID_BASE /** @type {Map} */ const shellJobLifecycleById = new Map() /** @type {Map} */ 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 | 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, * shellJobs?: Array<{ id?: number, done?: boolean, stopped?: boolean, pgid?: number, sid?: number, label?: string }>, * ipcStats?: { processGroups?: Record } | null, * logicalFdRows?: Array<{ fd: number, target: string }>, * initdReadiness?: { units?: Array<{ name: string, phase: string, startedAtMs: number, error?: string }>, schema?: number } | null, * sigactionHandlers?: Record | null, * env?: Record | null, * niceByPid?: Map | 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 | 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} snap */ export function bareOsBuildLogicalMemoryMapsProc(snap) { const processes = Array.isArray(snap.processes) ? snap.processes : [] /** @type {Record[]} */ 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} snap * @param {unknown} bareLib ctx.bare when available */ export function bareOsBuildProcessThreadsProc(snap, bareLib) { const processes = Array.isArray(snap.processes) ? snap.processes : [] /** @type {Record[]} */ 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[]} */ 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>} */ 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 1–3 (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} 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) }) }