/** * Bare init daemon — lightweight service supervisor (systemd-like registration). * Services start after the session console exists; failures are logged, not fatal. * Drop-ins: `SocketActivationIpc=`, optional `IdleSec=` (with `stop`) for idle FIFO stop, * `ReadinessPath=` (VFS or `exec:`), `ConditionPathExists=` / `AssertPathExists=`, * `~/.config/bare-os/units.d//*.conf` fragments. */ import { appendVarLog, ensureBareOsVarLogTree, INITD_LOG, KERNEL_CONSOLE_LOG } from './bare-os-var-log.js' import { appendBareInitdJournal } from './bare-initd-journal.js' import { BARE_INITD_DEFAULT_AFTER, emptyUnitDropIn, loadInitdUnitDropIns, readInitdDisabledSet, readInitdMaskedSet, readUnitDropIn, sortServicesForBoot } from './bare-initd-user.js' import { bareOsIpcLogicalToActualFifoName } from './bare-os-ipc-namespace.js' /** * @typedef {{ * name: string, * description?: string, * logPath?: string, * start: (ctx: Record) => void | Promise, * stop?: (ctx: Record) => void | Promise * }} BareService */ /** * Supervisor unit states (transition invariants documented in handbook). * Runtime `phase` today maps: active→active, failed→failed, else→inactive. */ export const BARE_INITD_UNIT_STATES = Object.freeze([ 'inactive', 'starting', 'active', 'stopping', 'skipped', 'failed', 'dead' ]) /** @typedef {{ phase: 'active'|'failed'|'inactive'|'starting'|'stopping'|'skipped'|'dead', startedAtMs: number, error?: string }} BareServiceRuntime */ /** @type {BareService[]} */ const registry = [] /** @type {Map} */ const runtime = new Map() /** @type {Map>} */ const unitHealthTimers = new Map() /** @type {(() => void)[]} */ const disposers = [] /** @type {Set} */ const socketLoopAbortControllers = new Set() /** @type {(() => void | Promise)[]} */ const kernelShutdownHooks = [] /** Last DAG snapshot JSON (set after {@link startBareInitd} completes ordering). */ let lastBareInitdDagJson = 'null\n' /** Boot order used for suspend (reverse stop) and resume (forward start). */ let lastBareInitdBootOrder = [] /** Units stopped by {@link bareInitdSuspendForBareMobile} (resume in boot order). */ const bareInitdMobileSuspended = new Set() let bareInitdMobileSuspendArmed = false /** * @param {Record} ctx * @param {string} rel */ async function logicalPathExists(ctx, rel) { const p = String(rel || '').trim() if (!p) return false const vfs = ctx.vfs if (!vfs) return false try { if (typeof vfs.lstat === 'function') { const st = await vfs.lstat(p) if (st) return true } } catch { /* fall through */ } try { const b = await vfs.readFile(p) return !!(b && (b.length ?? 0) > 0) } catch { return false } } /** * @param {Record} ctx * @param {string} rel */ async function logicalPathIsDirectory(ctx, rel) { const p = String(rel || '').trim() if (!p) return false const vfs = ctx.vfs if (!vfs || typeof vfs.lstat !== 'function') return false try { const st = await vfs.lstat(p) return !!(st && typeof st === 'object' && st.isDirectory()) } catch { return false } } /** * @param {Record} ctx * @param {import('./bare-initd-user.js').BareInitdUnitDropIn} di * @returns {Promise<'ok'|'skip'>} */ async function evaluateUnitPathConditions(ctx, di) { const maxLen = 512 const ass = di.assertPathExists?.trim() if (ass) { if (ass.length > maxLen) throw new Error('AssertPathExists path too long (max 512)') if (!(await logicalPathExists(ctx, ass))) { throw new Error('AssertPathExists not satisfied: ' + ass) } } const cond = di.conditionPathExists?.trim() if (cond) { if (cond.length > maxLen) return 'ok' if (!(await logicalPathExists(ctx, cond))) return 'skip' } const assDir = di.assertPathIsDirectory?.trim() if (assDir) { if (assDir.length > maxLen) throw new Error('AssertPathIsDirectory path too long (max 512)') if (!(await logicalPathIsDirectory(ctx, assDir))) { throw new Error('AssertPathIsDirectory not satisfied: ' + assDir) } } const condDir = di.conditionPathIsDirectory?.trim() if (condDir) { if (condDir.length > maxLen) return 'ok' if (!(await logicalPathIsDirectory(ctx, condDir))) return 'skip' } return 'ok' } /** Kernel logger: restore console on stop/restart */ let kernelLoggerWrapped = false /** @type {((...args: unknown[]) => void) | null} */ let kernelLoggerOrigLog = null /** @type {((...args: unknown[]) => void) | null} */ let kernelLoggerOrigErr = null /** * Register cleanup (e.g. clearInterval). Invoked on every {@link stopBareInitd} * (e.g. each REPL session teardown); callbacks should be idempotent. * @param {() => void} fn */ export function registerBareInitdDisposer(fn) { if (typeof fn === 'function') disposers.push(fn) } /** * Register a function to run when the REPL/kernel session ends (before initd disposers). * Use for async teardown (flush buffers, close handles). Errors are swallowed. * @param {() => void | Promise} fn */ export function registerKernelShutdownHook(fn) { if (typeof fn === 'function') kernelShutdownHooks.push(fn) } /** * Runs shutdown hooks in reverse registration order (LIFO), then clears the queue. */ export async function runKernelShutdownHooks() { while (kernelShutdownHooks.length) { const fn = kernelShutdownHooks.pop() try { await fn() } catch { /* ignore */ } } } /** * Stop every active unit that defines `stop`, in reverse boot-DAG order (like mobile suspend). * Swallows per-unit errors; clears health timers via {@link stopBareService}. * @param {Record} ctx */ export async function bareInitdShutdownActiveUnitsReverse(ctx) { const order = lastBareInitdBootOrder.length > 0 ? [...lastBareInitdBootOrder] : [...runtime.entries()] .filter(([, rt]) => rt.phase === 'active') .map(([n]) => n) for (const name of [...order].reverse()) { const rt = runtime.get(name) if (!rt || rt.phase !== 'active') continue const s = findBareServiceDefinition(name) if (s && typeof s.stop === 'function') { try { await stopBareService(ctx, name) } catch { /* ignore */ } } } } export function stopBareInitd() { for (const ac of socketLoopAbortControllers) { try { ac.abort() } catch { /* ignore */ } } socketLoopAbortControllers.clear() for (const t of unitHealthTimers.values()) clearInterval(t) unitHealthTimers.clear() for (const fn of disposers) { try { fn() } catch { /* ignore */ } } } /** * @param {BareService} service */ export function registerBareService(service) { if (!service?.name || typeof service.start !== 'function') return registry.push(service) } /** * @param {string} name * @returns {BareService | undefined} */ export function findBareServiceDefinition(name) { return registry.find((s) => s.name === name) } /** * @returns {{ name: string, description: string, logPath?: string, hasStop: boolean }[]} */ export function listBareServices() { return registry.map((s) => ({ name: s.name, description: s.description || '', logPath: s.logPath, hasStop: typeof s.stop === 'function' })) } /** * @param {string} name * @returns {BareServiceRuntime | undefined} */ export function getBareServiceRuntime(name) { return runtime.get(name) } /** * Live initd unit phases for readiness / observability (mirrors supervisor runtime map). * @returns {{ schema: 1, units: { name: string, phase: string, startedAtMs: number, error?: string }[], note: string, atMs: number }} */ export function bareInitdReadinessSnapshot() { const now = Date.now() /** @type {{ name: string, phase: string, startedAtMs: number, error?: string }[]} */ const units = [] for (const [name, rt] of runtime) { const row = { name, phase: rt.phase, startedAtMs: rt.startedAtMs } if (rt.error) row.error = rt.error units.push(row) } units.sort((a, b) => a.name.localeCompare(b.name)) return { schema: 2, units, supervisionTelemetry: { schema: 1, restartJournalEvents: [ 'start_scheduled', 'restart_attempt', 'start_error', 'failed_final', 'on_failure_ran', 'active' ], journalPathPattern: '/run/bare-os/unit-journal/*.ndjson', note: 'restart_attempt rows include backoffMsScheduled and restartPolicy when Restart= is set.' }, note: 'Supervisor phases; per-unit file readiness uses ReadinessPath= in unit drop-ins when set.', atMs: now } } /** * @returns {string} JSON string for `/proc/bare_os/initd_dag.json` (or `null` line if unavailable). */ export function getLastBareInitdDagSnapshotJson() { return lastBareInitdDagJson } /** * Stop active initd units that expose `stop` (for Bare suspend / mobile background). * @param {Record} ctx */ export async function bareInitdSuspendForBareMobile(ctx) { if (bareInitdMobileSuspendArmed) return bareInitdMobileSuspendArmed = true const order = lastBareInitdBootOrder.length > 0 ? [...lastBareInitdBootOrder] : [...runtime.entries()] .filter(([, rt]) => rt.phase === 'active') .map(([n]) => n) for (const name of [...order].reverse()) { const rt = runtime.get(name) if (!rt || rt.phase !== 'active') continue const s = findBareServiceDefinition(name) if (s && typeof s.stop === 'function') { try { await stopBareService(ctx, name) bareInitdMobileSuspended.add(name) } catch { /* ignore */ } } } } /** * Restart units stopped by {@link bareInitdSuspendForBareMobile}. * @param {Record} ctx */ export async function bareInitdResumeAfterBareMobile(ctx) { const order = lastBareInitdBootOrder.length > 0 ? [...lastBareInitdBootOrder] : [...bareInitdMobileSuspended] for (const name of order) { if (!bareInitdMobileSuspended.has(name)) continue bareInitdMobileSuspended.delete(name) try { await startBareService(ctx, name) } catch { /* ignore */ } } bareInitdMobileSuspendArmed = false } /** * Poll until all listed units are `active` or `timeoutMs` elapses. * @param {Record} ctx * @param {string[]} names * @param {number} [timeoutMs] * @returns {Promise} true if every unit became active */ export async function waitForBareInitdUnits(ctx, names, timeoutMs = 60000) { void ctx const need = [...names].filter(Boolean) if (!need.length) return true const ms = Number(timeoutMs) > 0 ? timeoutMs : 60000 const deadline = Date.now() + ms while (Date.now() < deadline) { let ok = true for (const n of need) { const rt = runtime.get(n) if (!rt || rt.phase !== 'active') { ok = false break } } if (ok) return true await new Promise((r) => setTimeout(r, 25)) } return false } /** * @param {Record} ctx */ export async function startBareService(ctx, name) { const s = findBareServiceDefinition(name) if (!s) throw new Error(`Unknown unit: ${name}`) const vfs = ctx.vfs if (vfs && typeof vfs.readFile === 'function') { const masked = await readInitdMaskedSet(vfs) if (masked.has(name)) throw new Error(`Unit is masked: ${name}`) } const rt = runtime.get(name) if (rt?.phase === 'active') { return { noop: true, message: `${name} is already active` } } const di = vfs && typeof vfs.readFile === 'function' ? await readUnitDropIn(vfs, name) : emptyUnitDropIn() const startSec = di.timeoutStartSec const t0 = Date.now() try { const cond = await evaluateUnitPathConditions(ctx, di) if (cond === 'skip') { runtime.set(name, { phase: 'skipped', startedAtMs: t0 }) return { skipped: true } } } catch (e) { const msg = e?.message || String(e) runtime.set(name, { phase: 'failed', startedAtMs: t0, error: msg }) try { ctx.console?.error?.(`[bare-initd] ${name}: ${msg}`) } catch { /* ignore */ } void appendVarLog(ctx, INITD_LOG, name, msg) throw e } try { await withTimeoutSec(s.start(ctx), startSec, `start ${name}`) if (di.readinessPath) { const rsec = di.readinessTimeoutSec ?? 30 await waitForReadinessPath(ctx, di.readinessPath, rsec) } runtime.set(name, { phase: 'active', startedAtMs: t0 }) const post = di.execStartPost if (post && typeof ctx.execLine === 'function' && post.trim()) { try { await ctx.execLine(post.trim()) } catch (e) { const msg = e?.message || String(e) try { ctx.console?.error?.(`[bare-initd] ${name} ExecStartPost: ${msg}`) } catch { /* ignore */ } void appendVarLog(ctx, INITD_LOG, name, 'ExecStartPost: ' + msg) } } return { ok: true } } catch (e) { const msg = e?.message || String(e) runtime.set(name, { phase: 'failed', startedAtMs: t0, error: msg }) try { ctx.console?.error?.(`[bare-initd] ${name}: ${msg}`) } catch { /* ignore */ } void appendVarLog(ctx, INITD_LOG, name, msg) throw e } } /** * @param {Promise} promise * @param {number | null | undefined} sec * @param {string} label */ async function withTimeoutSec(promise, sec, label) { if (sec == null || !Number.isFinite(sec) || sec <= 0) return promise const ms = Math.round(sec * 1000) return await new Promise((resolve, reject) => { const t = setTimeout(() => { reject(new Error(`${label} timed out after ${sec}s`)) }, ms) Promise.resolve(promise).then( () => { clearTimeout(t) resolve(undefined) }, (e) => { clearTimeout(t) reject(e) } ) }) } /** * Poll VFS until `path` exists or timeout. * @param {Record} ctx * @param {string} logicalPath * @param {number} timeoutSec */ /** * @param {Record} ctx * @param {string} name * @param {import('./bare-initd-user.js').BareInitdUnitDropIn} dropIn */ function scheduleUnitHealth(ctx, name, dropIn) { if (!dropIn.execHealthCmd || !dropIn.healthIntervalSec) return if (typeof ctx.execLine !== 'function') return const intervalMs = Math.max(5, dropIn.healthIntervalSec) * 1000 const threshold = Math.max(1, dropIn.healthFailureThreshold ?? 3) let fails = 0 const t = setInterval(() => { void (async () => { const rt = runtime.get(name) if (!rt || rt.phase !== 'active') return const env = ctx.vfs?.env const saved = env ? env.BARE_OS_EXEC_MAX_DEPTH : undefined if (dropIn.bareMaxExecDepth != null && env) { env.BARE_OS_EXEC_MAX_DEPTH = String(dropIn.bareMaxExecDepth) } try { await ctx.execLine(dropIn.execHealthCmd.trim()) if ((Number(ctx.exitCode) || 0) === 0) fails = 0 else fails++ } catch { fails++ } finally { if (env && saved !== undefined) env.BARE_OS_EXEC_MAX_DEPTH = saved } if (fails >= threshold) { clearInterval(t) unitHealthTimers.delete(name) const msg = 'health check failed' runtime.set(name, { phase: 'failed', startedAtMs: Date.now(), error: msg }) try { ctx.console?.error?.(`[bare-initd] ${name}: ${msg}`) } catch { /* ignore */ } void appendVarLog(ctx, INITD_LOG, name, msg) appendBareInitdJournal(name, { event: 'health_failed', error: msg }) } })() }, intervalMs) unitHealthTimers.set(name, t) } async function waitForReadinessPath(ctx, logicalPath, timeoutSec) { const raw = String(logicalPath || '').trim() if (raw.toLowerCase().startsWith('exec:')) { const cmd = raw.slice(5).trim() if (!cmd || typeof ctx.execLine !== 'function') return const sec = Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : 30 await withTimeoutSec(ctx.execLine(cmd), sec, 'readiness exec') if ((Number(ctx.exitCode) || 0) !== 0) { throw new Error( `readiness exec failed (exit ${ctx.exitCode}): ${cmd.slice(0, 200)}` ) } return } const vfs = ctx.vfs if (!vfs || typeof vfs.exists !== 'function') return const sec = Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : 30 const deadline = Date.now() + Math.round(sec * 1000) while (Date.now() < deadline) { try { if (await vfs.exists(logicalPath)) return } catch { /* ignore */ } await new Promise((r) => setTimeout(r, 100)) } throw new Error(`readiness path not ready: ${logicalPath} (${sec}s)`) } export async function stopBareService(ctx, name) { const s = findBareServiceDefinition(name) if (!s) throw new Error(`Unknown unit: ${name}`) const ht = unitHealthTimers.get(name) if (ht) { clearInterval(ht) unitHealthTimers.delete(name) } if (typeof s.stop !== 'function') { throw new Error(`Unit ${name} does not support stop (no stop handler)`) } const vfs = ctx.vfs let stopSec = null if (vfs && typeof vfs.readFile === 'function') { const di = await readUnitDropIn(vfs, name) stopSec = di.timeoutStopSec } await withTimeoutSec(s.stop(ctx), stopSec, `stop ${name}`) const prev = runtime.get(name) runtime.set(name, { phase: 'inactive', startedAtMs: prev?.startedAtMs ?? Date.now() }) } /** * @param {Record} ctx * @param {string} name */ export async function restartBareService(ctx, name) { const s = findBareServiceDefinition(name) if (!s) throw new Error(`Unknown unit: ${name}`) if (typeof s.stop !== 'function') { throw new Error( `Unit ${name} does not support restart (no stop handler; session teardown only)` ) } await stopBareService(ctx, name) const vfs = ctx.vfs const di = vfs && typeof vfs.readFile === 'function' ? await readUnitDropIn(vfs, name) : emptyUnitDropIn() const startSec = di.timeoutStartSec const t0 = Date.now() try { const pathEv = await evaluateUnitPathConditions(ctx, di) if (pathEv === 'skip') { runtime.set(name, { phase: 'skipped', startedAtMs: t0 }) return } } catch (e) { const msg = e?.message || String(e) runtime.set(name, { phase: 'failed', startedAtMs: t0, error: msg }) try { ctx.console?.error?.(`[bare-initd] ${name}: ${msg}`) } catch { /* ignore */ } void appendVarLog(ctx, INITD_LOG, name, msg) throw e } try { await withTimeoutSec(s.start(ctx), startSec, `start ${name}`) runtime.set(name, { phase: 'active', startedAtMs: t0 }) } catch (e) { const msg = e?.message || String(e) runtime.set(name, { phase: 'failed', startedAtMs: t0, error: msg }) try { ctx.console?.error?.(`[bare-initd] ${name}: ${msg}`) } catch { /* ignore */ } void appendVarLog(ctx, INITD_LOG, name, msg) throw e } } /** * @param {BareService} s * @param {import('./bare-initd-user.js').BareInitdUnitDropIn} dropIn */ async function runOnFailureHookForUnit(ctx, s, dropIn) { if (!dropIn.onFailure?.trim() || dropIn.failureAction === 'none') return if (typeof ctx.execLine !== 'function') return try { await ctx.execLine(dropIn.onFailure.trim()) appendBareInitdJournal(s.name, { event: 'on_failure_ran' }) } catch (e) { const msg = e?.message || String(e) try { ctx.console?.error?.(`[bare-initd] ${s.name} OnFailure: ${msg}`) } catch { /* ignore */ } } } /** * @param {Record} ctx * @param {BareService} s * @param {import('./bare-initd-user.js').BareInitdUnitDropIn} dropIn */ async function startNormalBareInitdUnit(ctx, s, dropIn) { const t0 = Date.now() const startSec = dropIn.timeoutStartSec let defaultRestartAttempts = 3 const envD = ctx.env?.BARE_OS_INITD_RESTART_MAX_DEFAULT if (envD) { const n = Number.parseInt(String(envD), 10) if (Number.isFinite(n) && n >= 1 && n <= 32) defaultRestartAttempts = n } const maxAttempts = dropIn.restartMaxAttempts != null && dropIn.restartMaxAttempts > 0 ? Math.min(Math.floor(dropIn.restartMaxAttempts), 32) : dropIn.restart === 'on-failure' || dropIn.restart === 'always' ? defaultRestartAttempts : 1 const restartDelayMs = dropIn.restartSec != null && dropIn.restartSec >= 0 ? Math.round(dropIn.restartSec * 1000) : 1000 appendBareInitdJournal(s.name, { event: 'start_scheduled' }) for (let attempt = 0; attempt < maxAttempts; attempt++) { try { if (attempt > 0) { const backoffMs = Math.min( 120000, Math.round(restartDelayMs * Math.pow(2, attempt - 1)) ) appendBareInitdJournal(s.name, { event: 'restart_attempt', attempt, backoffMsScheduled: backoffMs, restartPolicy: dropIn.restart, restartMaxAttempts: maxAttempts, telemetrySchema: 1 }) await new Promise((r) => setTimeout(r, backoffMs)) } const pathEv = await evaluateUnitPathConditions(ctx, dropIn) if (pathEv === 'skip') { runtime.set(s.name, { phase: 'skipped', startedAtMs: Date.now() }) appendBareInitdJournal(s.name, { event: 'skipped_condition_path' }) return } await withTimeoutSec(s.start(ctx), startSec, `start ${s.name}`) if (dropIn.readinessPath) { const rsec = dropIn.readinessTimeoutSec ?? 30 await waitForReadinessPath(ctx, dropIn.readinessPath, rsec) } runtime.set(s.name, { phase: 'active', startedAtMs: t0 }) appendBareInitdJournal(s.name, { event: 'active', attempt }) const post = dropIn.execStartPost if (post && typeof ctx.execLine === 'function' && post.trim()) { try { await ctx.execLine(post.trim()) } catch (e) { const msg = e?.message || String(e) try { ctx.console?.error?.( `[bare-initd] ${s.name} ExecStartPost: ${msg}` ) } catch { /* ignore */ } void appendVarLog(ctx, INITD_LOG, s.name, 'ExecStartPost: ' + msg) } } scheduleUnitHealth(ctx, s.name, dropIn) return } catch (e) { const msg = e?.message || String(e) appendBareInitdJournal(s.name, { event: 'start_error', attempt, error: msg }) if (attempt === maxAttempts - 1) { runtime.set(s.name, { phase: 'failed', startedAtMs: t0, error: msg }) try { ctx.console?.error?.(`[bare-initd] ${s.name}: ${msg}`) } catch { /* ignore */ } void appendVarLog(ctx, INITD_LOG, s.name, msg) await runOnFailureHookForUnit(ctx, s, dropIn) appendBareInitdJournal(s.name, { event: 'failed_final', error: msg }) } } } } /** * @param {Record} ctx */ export async function startBareInitd(ctx) { runtime.clear() await ensureBareOsVarLogTree(ctx) const vfs = ctx.vfs /** @type {Map} */ let dropInsMap = new Map() let ordered = registry if (vfs && typeof vfs.readFile === 'function') { const disabled = await readInitdDisabledSet(vfs) const masked = await readInitdMaskedSet(vfs) const disabledForBoot = new Set([...disabled, ...masked]) dropInsMap = await loadInitdUnitDropIns( vfs, registry, BARE_INITD_DEFAULT_AFTER ) /** @type {Map} */ const requiresMap = new Map() /** @type {Map} */ const wantsMap = new Map() /** @type {Map} */ const afterMap = new Map() const nameSet = new Set(registry.map((s) => s.name)) for (const [name, di] of dropInsMap) { afterMap.set(name, [...(di.after || [])]) requiresMap.set(name, di.requires) wantsMap.set(name, di.wants) } for (const [name, di] of dropInsMap) { for (const b of di.before || []) { if (!nameSet.has(b)) continue const cur = afterMap.get(b) || [] if (!cur.includes(name)) cur.push(name) afterMap.set(b, cur) } } ordered = sortServicesForBoot( registry, disabledForBoot, afterMap, requiresMap, wantsMap ) } lastBareInitdBootOrder = ordered.map((s) => s.name) const rawPar = ctx.env && ctx.env.BARE_OS_INITD_MAX_PARALLEL const maxP = Math.max( 1, Math.min(32, Number.parseInt(String(rawPar ?? '1'), 10) || 1) ) const activeNames = new Set(ordered.map((s) => s.name)) /** @type {Map>} */ const prereq = new Map() for (const s of ordered) { const di = dropInsMap.get(s.name) || emptyUnitDropIn() const inc = new Set() for (const a of di.after) if (activeNames.has(a)) inc.add(a) for (const r of di.requires) if (activeNames.has(r)) inc.add(r) for (const w of di.wants) if (activeNames.has(w)) inc.add(w) prereq.set(s.name, inc) } for (const s of ordered) { const di = dropInsMap.get(s.name) || emptyUnitDropIn() for (const b of di.before || []) { if (!activeNames.has(b)) continue const inc = prereq.get(b) || new Set() inc.add(s.name) prereq.set(b, inc) } } /** @param {string[]} names @returns {string[][]} */ function computeLevels(names) { const remaining = new Set(names) /** @type {string[][]} */ const levels = [] while (remaining.size) { const ready = [...remaining].filter((n) => { for (const p of prereq.get(n) || []) { if (remaining.has(p)) return false } return true }) if (!ready.length) { const n = [...remaining].sort()[0] levels.push([n]) remaining.delete(n) continue } ready.sort() levels.push(ready) for (const n of ready) remaining.delete(n) } return levels } const levels = computeLevels(ordered.map((s) => s.name)) try { const nodes = ordered.map((s) => s.name) /** @type {[string, string][]} */ const edges = [] for (const name of nodes) { for (const p of prereq.get(name) || []) { edges.push([p, name]) } } const escDot = (s) => String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"') const dot = 'digraph bare_initd {\n' + nodes.map((n) => ` "${escDot(n)}";\n`).join('') + edges.map(([a, b]) => ` "${escDot(a)}" -> "${escDot(b)}";\n`).join('') + '}\n' lastBareInitdDagJson = `${JSON.stringify({ schema: 1, nodes, levels, edges, dot, supervision: { schema: 1, restartKeys: [ 'Restart', 'RestartSec', 'RestartMaxAttempts', 'OnFailure' ], reference: 'packages/bare-os-booter/lib/bare-initd-user.js', note: 'systemd-inspired subset parsed from unit drop-ins; backoff is host/runtime specific.' }, atMs: Date.now() })}\n` } catch { lastBareInitdDagJson = 'null\n' } for (const level of levels) { /** @type {{ s: BareService, di: import('./bare-initd-user.js').BareInitdUnitDropIn }[]} */ const sockets = [] /** @type {{ s: BareService, di: import('./bare-initd-user.js').BareInitdUnitDropIn }[]} */ const normals = [] for (const name of level) { const s = registry.find((x) => x.name === name) if (!s) continue const di = dropInsMap.get(s.name) || emptyUnitDropIn() if (di.socketActivationIpc && ctx.bareOsIpc) sockets.push({ s, di }) else normals.push({ s, di }) } for (const { s, di } of sockets) { const t0 = Date.now() const env = ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} const ipcActual = bareOsIpcLogicalToActualFifoName( di.socketActivationIpc, env ) try { ctx.bareOsIpc.create(ipcActual) } catch { /* exists */ } const ipcName = ipcActual const startSec = di.timeoutStartSec const idleSec = di.idleSec != null && di.idleSec > 0 ? Math.min(di.idleSec, 86400) : 0 const idleStopOk = idleSec > 0 && typeof s.stop === 'function' void (async () => { for (;;) { appendBareInitdJournal(s.name, { event: 'socket_wait' }) runtime.set(s.name, { phase: 'inactive', startedAtMs: Date.now() }) const outerWaitAc = new AbortController() socketLoopAbortControllers.add(outerWaitAc) try { await ctx.bareOsIpc.take(ipcName, { signal: outerWaitAc.signal }) } catch (e) { socketLoopAbortControllers.delete(outerWaitAc) if ( e && typeof e === 'object' && /** @type {Error} */ (e).name === 'AbortError' ) { runtime.set(s.name, { phase: 'inactive', startedAtMs: Date.now() }) appendBareInitdJournal(s.name, { event: 'socket_loop_stopped' }) return } const msg = e?.message || String(e) runtime.set(s.name, { phase: 'failed', startedAtMs: t0, error: msg }) try { ctx.console?.error?.(`[bare-initd] ${s.name} (socket): ${msg}`) } catch { /* ignore */ } void appendVarLog(ctx, INITD_LOG, s.name, msg) appendBareInitdJournal(s.name, { event: 'failed_socket', error: msg }) await runOnFailureHookForUnit(ctx, s, di) return } finally { socketLoopAbortControllers.delete(outerWaitAc) } const t1 = Date.now() try { const pe = await evaluateUnitPathConditions(ctx, di) if (pe === 'skip') { runtime.set(s.name, { phase: 'skipped', startedAtMs: t1 }) appendBareInitdJournal(s.name, { event: 'skipped_condition_path_socket' }) continue } await withTimeoutSec(s.start(ctx), startSec, `start ${s.name}`) if (di.readinessPath) { const rsec = di.readinessTimeoutSec ?? 30 await waitForReadinessPath(ctx, di.readinessPath, rsec) } runtime.set(s.name, { phase: 'active', startedAtMs: t1 }) appendBareInitdJournal(s.name, { event: 'active', socket: true }) const post = di.execStartPost if (post && typeof ctx.execLine === 'function' && post.trim()) { try { await ctx.execLine(post.trim()) } catch (e) { const msg = e?.message || String(e) try { ctx.console?.error?.( `[bare-initd] ${s.name} ExecStartPost: ${msg}` ) } catch { /* ignore */ } void appendVarLog( ctx, INITD_LOG, s.name, 'ExecStartPost: ' + msg ) } } scheduleUnitHealth(ctx, s.name, di) if (!idleStopOk) return while (runtime.get(s.name)?.phase === 'active') { const ac = new AbortController() socketLoopAbortControllers.add(ac) const takeP = ctx.bareOsIpc.take(ipcName, { signal: ac.signal }) const idleMs = idleSec * 1000 const race = await Promise.race([ takeP.then(() => 'ipc'), new Promise((r) => setTimeout(() => r('idle'), idleMs)) ]) if (race === 'idle') { ac.abort() socketLoopAbortControllers.delete(ac) try { await takeP } catch { /* AbortError */ } try { await stopBareService(ctx, s.name) } catch (e) { const msg = e?.message || String(e) try { ctx.console?.error?.( `[bare-initd] ${s.name} idle stop: ${msg}` ) } catch { /* ignore */ } } appendBareInitdJournal(s.name, { event: 'idle_stop', socket: true }) break } socketLoopAbortControllers.delete(ac) } } catch (e) { const msg = e?.message || String(e) runtime.set(s.name, { phase: 'failed', startedAtMs: t0, error: msg }) try { ctx.console?.error?.(`[bare-initd] ${s.name} (socket): ${msg}`) } catch { /* ignore */ } void appendVarLog(ctx, INITD_LOG, s.name, msg) appendBareInitdJournal(s.name, { event: 'failed_socket', error: msg }) await runOnFailureHookForUnit(ctx, s, di) return } } })() } for (let i = 0; i < normals.length; i += maxP) { const chunk = normals.slice(i, i + maxP) await Promise.all( chunk.map(({ s, di }) => startNormalBareInitdUnit(ctx, s, di)) ) } } } function stopKernelLogger(ctx) { const c = ctx.console if (!kernelLoggerWrapped || !c) return if (kernelLoggerOrigLog) c.log = kernelLoggerOrigLog if (kernelLoggerOrigErr) c.error = kernelLoggerOrigErr kernelLoggerWrapped = false kernelLoggerOrigLog = null kernelLoggerOrigErr = null } function startKernelLogger(ctx) { const c = ctx.console if (!c || typeof c.log !== 'function') return if (kernelLoggerWrapped) return kernelLoggerOrigLog = c.log.bind(c) kernelLoggerOrigErr = typeof c.error === 'function' ? c.error.bind(c) : kernelLoggerOrigLog c.log = (...args) => { const text = args.map(String).join(' ') void appendVarLog(ctx, KERNEL_CONSOLE_LOG, 'log', text) return kernelLoggerOrigLog(...args) } c.error = (...args) => { const text = args.map(String).join(' ') void appendVarLog(ctx, KERNEL_CONSOLE_LOG, 'error', text) return kernelLoggerOrigErr(...args) } kernelLoggerWrapped = true } function registerKernelLoggerService() { registerBareService({ name: 'kernel-logger', description: 'Mirrors console.log / console.error to /var/log/bare-os/kernel-console.log', logPath: KERNEL_CONSOLE_LOG, start: startKernelLogger, stop: stopKernelLogger }) } registerKernelLoggerService()