Files
bare-operating-system/packages/bare-os-booter/lib/bare-initd.js
T
2026-04-26 07:05:24 -04:00

1157 lines
35 KiB
JavaScript

/**
* 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/<name>/*.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<string, unknown>) => void | Promise<void>,
* stop?: (ctx: Record<string, unknown>) => void | Promise<void>
* }} 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<string, BareServiceRuntime>} */
const runtime = new Map()
/** @type {Map<string, ReturnType<typeof setInterval>>} */
const unitHealthTimers = new Map()
/** @type {(() => void)[]} */
const disposers = []
/** @type {Set<AbortController>} */
const socketLoopAbortControllers = new Set()
/** @type {(() => void | Promise<void>)[]} */
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<string, unknown>} 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<string, unknown>} 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<string, unknown>} 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<void>} 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<string, unknown>} 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<string, unknown>} 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<string, unknown>} 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<string, unknown>} ctx
* @param {string[]} names
* @param {number} [timeoutMs]
* @returns {Promise<boolean>} 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<string, unknown>} 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<void>} 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<string, unknown>} ctx
* @param {string} logicalPath
* @param {number} timeoutSec
*/
/**
* @param {Record<string, unknown>} 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<string, unknown>} 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<string, unknown>} 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<string, unknown>} ctx
*/
export async function startBareInitd(ctx) {
runtime.clear()
await ensureBareOsVarLogTree(ctx)
const vfs = ctx.vfs
/** @type {Map<string, import('./bare-initd-user.js').BareInitdUnitDropIn>} */
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<string, string[]>} */
const requiresMap = new Map()
/** @type {Map<string, string[]>} */
const wantsMap = new Map()
/** @type {Map<string, string[]>} */
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<string, Set<string>>} */
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<string, string | undefined>} */ (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()