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
570 lines
19 KiB
JavaScript
570 lines
19 KiB
JavaScript
/**
|
||
* 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 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<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)
|
||
})
|
||
}
|