Updates
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
| Version | Booter (workspace) | Notes |
|
||||
| ------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 1.9.0 | 0.1.0 | **`bareOsVerifyBootManifestSignature`**, signed manifest (**`BARE_OS_BOOT_MANIFEST_SIGN`**, **`/etc/bare-os/boot.manifest.sig`**), **`/proc/bare_os_replication`**, **`/proc/bare_os_capabilities`** (+ **`.json`**), **`/proc/bare_os_bootstrap`**, **`/run/bare-os/unit-journal/*.ndjson`**, initd **`BARE_OS_INITD_MAX_PARALLEL`**, unit journal + **`OnFailure=`** / **`FailureAction=`** / **`Before=`**, **`bareOsIpc.duplexJsonRoundTrip`**, **`BARE_OS_VFS_BIN_CACHE`**, **`bareOsRequestMirror`**, **`bareOsExportPersonalSnapshot`**, **`bareOsPearIpcEmit`**, sandbox worker hint (**`BARE_OS_SANDBOX_WORKER`**). Seeder: optional **`BARE_OS_HYPER_MULTISIG_VERIFY`** + **`hyper-multisig`**. Coreutils: **`grep -r`** **`--include`/`--exclude`/`--exclude-dir`**, **`sed -z`**. |
|
||||
| 1.8.0 | 0.1.0 | **`ctx.bareOsSandboxRunScript`**, **`bareOsBootFileSha256Hex`**, boot phase hooks (**`bareOsRegisterBootPhaseHook`** / **`bareOsInvokeBootPhaseHooks`**), virtual file meta (**`bareOsInvalidateVirtualFile`**, **`bareOsUpdateVirtualFileMeta`**), **`bareOsIpc.createDuplexBridge`**, **`/proc/bare_os_swarm`**, VFS union read (**`BARE_OS_VFS_UNION_PREFIXES`**), **`BARE_OS_URANDOM_CRYPTO=0`**, async **`bareOsRequestPearReload`**, **`bareOsPublishBootReady.subsystems`**, **`verifyBareModuleLockfile`**. Kernel: **`BARE_OS_BOOT_MANIFEST`**, **`/etc/bare-os/selftest.d/`**, stock init hooks. |
|
||||
| 1.7.0 | 0.1.0 | **`ctx.bare`**: frozen map of Holepunch-style npm modules for in-image scripts (manifest-driven host `import()` + optional trusted **`/lib/bare/bundles/*.js`** merge). Caps **`bareCtxModules`**, **`bareDriveBundles`**. Env **`BARE_OS_BARE_MODULES`**, **`BARE_OS_BARE_DRIVE_BUNDLES`**. Workspace **`bare-os-bare-libs`** builds seeded bundles. |
|
||||
| 1.6.0 | 0.1.0 | Abort/timeout on `execLine`, `readLine`, `runBinCommand`, VFS `readFile`/`writeFile`; IPC fan-out + JSON-RPC token/line limits; HTTP allow/deny + audit; `/proc/bare_os_resources`, `/proc/bare_os_features`; `/run/bare-os/virtual/*`; booter boot phases in `boot.json` (`booterPhases`); optional `ctx.bareOsHostStats`, `ctx.httpFetch` policy wrapper; initd `ReadinessPath` / `ReadinessTimeoutSec`; Pear/sandbox stubs. |
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
runBinCommand,
|
||||
runUserScriptFromSource
|
||||
} from './lib/kernel-runner.js'
|
||||
import { verifyBootManifestEd25519 } from '#bare-os-boot-manifest-sig'
|
||||
import { getBareInitdJournalNdjson } from './lib/bare-initd-journal.js'
|
||||
import { createBareOsSandboxContext } from './lib/bare-os-sandbox.js'
|
||||
import { createVfs } from './lib/vfs.js'
|
||||
import { createBareOsIpc } from './lib/bare-os-ipc.js'
|
||||
@@ -76,7 +78,7 @@ import {
|
||||
bareOsListThemeNames
|
||||
} from './lib/bare-os-theme-presets.js'
|
||||
import './lib/bare-cron.js'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createHash } from 'bare-crypto'
|
||||
|
||||
const { randomUUID, randomBytes } = bareCrypto
|
||||
|
||||
@@ -319,7 +321,19 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
'BARE_OS_YES_MAX_LINES',
|
||||
'BARE_OS_SHUF_MAX_LINES',
|
||||
'BARE_OS_SPLIT_MAX_FILES',
|
||||
'BARE_OS_NPROC'
|
||||
'BARE_OS_NPROC',
|
||||
'BARE_OS_BOOT_MANIFEST',
|
||||
'BARE_OS_BOOT_MANIFEST_SIGN',
|
||||
'BARE_OS_BOOT_MANIFEST_PUBKEY_HEX',
|
||||
'BARE_OS_SANDBOX_SCRIPT',
|
||||
'BARE_OS_VFS_UNION_PREFIXES',
|
||||
'BARE_OS_URANDOM_CRYPTO',
|
||||
'BARE_OS_INITD_MAX_PARALLEL',
|
||||
'BARE_OS_VFS_BIN_CACHE',
|
||||
'BARE_OS_SANDBOX_WORKER',
|
||||
'BARE_OS_BLIND_BOOTSTRAP_URL',
|
||||
'BARE_OS_BLIND_BOOTSTRAP_JSON',
|
||||
'BARE_OS_MIRROR_READ_KEY'
|
||||
]) {
|
||||
const v = hostEnv[k]
|
||||
if (v != null && v !== '') shellEnv[k] = v
|
||||
@@ -538,6 +552,57 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
protocol: 'bare-os-v1'
|
||||
})}\n`
|
||||
},
|
||||
procBareOsReplicationText() {
|
||||
const tk = topicKey()
|
||||
const peers = disk.peers?.size ?? 0
|
||||
const mk = shellEnv.BARE_OS_MIRROR_READ_KEY
|
||||
return `${JSON.stringify({
|
||||
topicHex: b4a.toString(tk, 'hex'),
|
||||
peerCount: peers,
|
||||
session: sessionStatsRef,
|
||||
mirrorHint: !!(mk && String(mk).trim()),
|
||||
atMs: Date.now()
|
||||
})}\n`
|
||||
},
|
||||
procBareOsCapabilitiesText() {
|
||||
const c = buildBareOsRuntimeCaps(shellEnv)
|
||||
const paths = (c.pseudoFsPaths || []).join('\n ')
|
||||
return [
|
||||
'# bare_os_capabilities (summary; see .json for machine-readable)',
|
||||
`ctxApiVersion: ${c.ctxApiVersion}`,
|
||||
'pipeline: ' + JSON.stringify(c.pipeline),
|
||||
'quotas: ' + JSON.stringify(c.quotas),
|
||||
'pseudoFsPaths:',
|
||||
' ' + paths,
|
||||
'features: ' + JSON.stringify(c.features),
|
||||
''
|
||||
].join('\n')
|
||||
},
|
||||
procBareOsCapabilitiesJsonText() {
|
||||
return `${JSON.stringify(buildBareOsRuntimeCaps(shellEnv), null, 2)}\n`
|
||||
},
|
||||
procBareOsBootstrapText() {
|
||||
const url = shellEnv.BARE_OS_BLIND_BOOTSTRAP_URL
|
||||
const j = shellEnv.BARE_OS_BLIND_BOOTSTRAP_JSON
|
||||
if (url && String(url).trim()) {
|
||||
return `${JSON.stringify({
|
||||
blindBootstrapUrl: String(url).trim(),
|
||||
source: 'BARE_OS_BLIND_BOOTSTRAP_URL'
|
||||
})}\n`
|
||||
}
|
||||
if (j && String(j).trim()) {
|
||||
try {
|
||||
return `${JSON.stringify({
|
||||
blindBootstrap: JSON.parse(String(j)),
|
||||
source: 'BARE_OS_BLIND_BOOTSTRAP_JSON'
|
||||
})}\n`
|
||||
} catch {
|
||||
return `${JSON.stringify({ error: 'invalid BARE_OS_BLIND_BOOTSTRAP_JSON' })}\n`
|
||||
}
|
||||
}
|
||||
return '{}\n'
|
||||
},
|
||||
getUnitJournalNdjson: (unit) => getBareInitdJournalNdjson(unit),
|
||||
getVirtualReaders: () => virtualReaderEntries,
|
||||
unionReadPrefixes: parseUnionReadPrefixes(shellEnv),
|
||||
sysClassNetLoText() {
|
||||
@@ -888,6 +953,18 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
if (shellEnv.BARE_OS_SANDBOX_SCRIPT === '0') {
|
||||
throw new Error('bareOsSandboxRunScript: disabled by BARE_OS_SANDBOX_SCRIPT=0')
|
||||
}
|
||||
if (
|
||||
shellEnv.BARE_OS_SANDBOX_WORKER === '1' ||
|
||||
shellEnv.BARE_OS_SANDBOX_WORKER === 'true'
|
||||
) {
|
||||
try {
|
||||
this.console?.log?.(
|
||||
'[bare-os] BARE_OS_SANDBOX_WORKER: running inline (bare-thread needs a file entrypoint)'
|
||||
)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
const sb = createBareOsSandboxContext(this)
|
||||
const { raceWithAbortAndTimeout } = await import('./lib/bare-os-abort.js')
|
||||
return raceWithAbortAndTimeout(
|
||||
@@ -957,6 +1034,86 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
bareOsBootFileSha256Hex(buf) {
|
||||
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
|
||||
return createHash('sha256').update(u8).digest('hex')
|
||||
},
|
||||
/**
|
||||
* Ed25519 verify for `/etc/bare-os/boot.manifest.json` when `BARE_OS_BOOT_MANIFEST_SIGN=1`.
|
||||
* @param {Uint8Array | ArrayBuffer | null | undefined} manifestBytes
|
||||
* @param {Uint8Array | ArrayBuffer | null | undefined} signatureBytes
|
||||
* @param {string} [publicKeyHex] optional override; else `BARE_OS_BOOT_MANIFEST_PUBKEY_HEX`
|
||||
*/
|
||||
bareOsVerifyBootManifestSignature(manifestBytes, signatureBytes, publicKeyHex) {
|
||||
const pub =
|
||||
publicKeyHex != null && String(publicKeyHex).trim()
|
||||
? String(publicKeyHex).trim()
|
||||
: shellEnv.BARE_OS_BOOT_MANIFEST_PUBKEY_HEX || ''
|
||||
const m =
|
||||
manifestBytes instanceof Uint8Array
|
||||
? manifestBytes
|
||||
: manifestBytes
|
||||
? new Uint8Array(manifestBytes)
|
||||
: null
|
||||
const s =
|
||||
signatureBytes instanceof Uint8Array
|
||||
? signatureBytes
|
||||
: signatureBytes
|
||||
? new Uint8Array(signatureBytes)
|
||||
: null
|
||||
return verifyBootManifestEd25519(m, s, pub)
|
||||
},
|
||||
/**
|
||||
* Ask host to attach a mirror read key (Pear / Holepunch); emits process event when available.
|
||||
* @param {{ key?: string, label?: string }} [opts]
|
||||
*/
|
||||
async bareOsRequestMirror(opts = {}) {
|
||||
if (typeof globalThis.process?.emit === 'function') {
|
||||
try {
|
||||
globalThis.process.emit('bare-os:mirror-request', {
|
||||
key: opts.key != null ? String(opts.key) : '',
|
||||
label: opts.label != null ? String(opts.label) : ''
|
||||
})
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return { ok: true, hint: 'Host should listen for process "bare-os:mirror-request".' }
|
||||
},
|
||||
/**
|
||||
* Hint host to export a personal-drive snapshot (corestore-snapshot style).
|
||||
* @param {{ label?: string }} [opts]
|
||||
*/
|
||||
async bareOsExportPersonalSnapshot(opts = {}) {
|
||||
if (typeof globalThis.process?.emit === 'function') {
|
||||
try {
|
||||
globalThis.process.emit('bare-os:export-personal-snapshot', {
|
||||
label: opts.label != null ? String(opts.label) : 'default',
|
||||
ts: Date.now()
|
||||
})
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
hint: 'Host listens for process "bare-os:export-personal-snapshot".'
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Passthrough for Pear IPC–style host bridges (see developer guide).
|
||||
* @param {string} channel
|
||||
* @param {Record<string, unknown>} payload
|
||||
*/
|
||||
bareOsPearIpcEmit(channel, payload) {
|
||||
if (typeof globalThis.process?.emit !== 'function') return false
|
||||
try {
|
||||
globalThis.process.emit('bare-os:pear-ipc', {
|
||||
channel: String(channel || ''),
|
||||
payload: payload && typeof payload === 'object' ? payload : {},
|
||||
ts: Date.now()
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* In-memory structured initd journal (NDJSON lines) exposed under /run/bare-os/unit-journal/.
|
||||
*/
|
||||
|
||||
const MAX_LINES_PER_UNIT = 400
|
||||
const MAX_LINE_UTF8 = 4096
|
||||
|
||||
/** @type {Map<string, string[]>} */
|
||||
const linesByUnit = new Map()
|
||||
|
||||
/**
|
||||
* @param {string} unit
|
||||
* @param {Record<string, unknown>} rec
|
||||
*/
|
||||
export function appendBareInitdJournal(unit, rec) {
|
||||
if (!unit || !/^[a-zA-Z0-9._-]+$/.test(unit)) return
|
||||
const payload = { ts: Date.now(), unit, ...rec }
|
||||
let line
|
||||
try {
|
||||
line = JSON.stringify(payload) + '\n'
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (line.length > MAX_LINE_UTF8) return
|
||||
let arr = linesByUnit.get(unit)
|
||||
if (!arr) {
|
||||
arr = []
|
||||
linesByUnit.set(unit, arr)
|
||||
}
|
||||
arr.push(line)
|
||||
while (arr.length > MAX_LINES_PER_UNIT) arr.shift()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} unit
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getBareInitdJournalNdjson(unit) {
|
||||
const arr = linesByUnit.get(unit)
|
||||
return arr ? arr.join('') : ''
|
||||
}
|
||||
|
||||
/** @returns {string[]} */
|
||||
export function listBareInitdJournalUnits() {
|
||||
return [...linesByUnit.keys()].sort()
|
||||
}
|
||||
|
||||
export function clearBareInitdJournalForTests() {
|
||||
linesByUnit.clear()
|
||||
}
|
||||
@@ -12,7 +12,9 @@ export const BARE_INITD_DISABLED_FILE = '~/.config/bare-os/initd/disabled.txt'
|
||||
* Optional `name.unit` files under this directory. Supports `[Unit]` keys:
|
||||
* After=, Requires=, Wants=, TimeoutStartSec=, TimeoutStopSec=, Restart=, RestartSec=, ExecStartPost=, SocketActivationIpc=,
|
||||
* ReadinessPath= (VFS path until exists), ReadinessTimeoutSec=,
|
||||
* ExecHealthCmd=, HealthIntervalSec=, HealthFailureThreshold=, BareMaxExecDepth=.
|
||||
* ExecHealthCmd=, HealthIntervalSec=, HealthFailureThreshold=, BareMaxExecDepth=,
|
||||
* Before= (reverse edge: listed units start after this one), OnFailure= (execLine after restart exhausted),
|
||||
* FailureAction=exec|none (default exec when OnFailure is set).
|
||||
*/
|
||||
export const BARE_INITD_UNITS_DIR = '~/.config/bare-os/units'
|
||||
|
||||
@@ -43,7 +45,10 @@ export const BARE_INITD_DEFAULT_AFTER = Object.freeze({
|
||||
* execHealthCmd: string | null,
|
||||
* healthIntervalSec: number | null,
|
||||
* healthFailureThreshold: number | null,
|
||||
* bareMaxExecDepth: number | null
|
||||
* bareMaxExecDepth: number | null,
|
||||
* before: string[],
|
||||
* onFailure: string | null,
|
||||
* failureAction: 'exec' | 'none'
|
||||
* }} BareInitdUnitDropIn
|
||||
*/
|
||||
|
||||
@@ -64,7 +69,10 @@ export function emptyUnitDropIn() {
|
||||
execHealthCmd: null,
|
||||
healthIntervalSec: null,
|
||||
healthFailureThreshold: null,
|
||||
bareMaxExecDepth: null
|
||||
bareMaxExecDepth: null,
|
||||
before: [],
|
||||
onFailure: null,
|
||||
failureAction: 'exec'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,11 +141,20 @@ export function parseUnitDropInText(text) {
|
||||
} else if (key === 'baremaxexecdepth') {
|
||||
const n = Number.parseInt(val, 10)
|
||||
if (Number.isFinite(n) && n > 0) out.bareMaxExecDepth = n
|
||||
} else if (key === 'before') {
|
||||
out.before.push(...parseList(val))
|
||||
} else if (key === 'onfailure') {
|
||||
if (val.trim()) out.onFailure = val
|
||||
} else if (key === 'failureaction') {
|
||||
const v = val.toLowerCase()
|
||||
if (v === 'none') out.failureAction = 'none'
|
||||
else out.failureAction = 'exec'
|
||||
}
|
||||
}
|
||||
out.after = [...new Set(out.after)]
|
||||
out.requires = [...new Set(out.requires)]
|
||||
out.wants = [...new Set(out.wants)]
|
||||
out.before = [...new Set(out.before)]
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -166,7 +183,10 @@ export function mergeUnitDropIns(base, user) {
|
||||
healthIntervalSec: user.healthIntervalSec ?? base.healthIntervalSec,
|
||||
healthFailureThreshold:
|
||||
user.healthFailureThreshold ?? base.healthFailureThreshold,
|
||||
bareMaxExecDepth: user.bareMaxExecDepth ?? base.bareMaxExecDepth
|
||||
bareMaxExecDepth: user.bareMaxExecDepth ?? base.bareMaxExecDepth,
|
||||
before: pickArr(user.before, base.before),
|
||||
onFailure: user.onFailure ?? base.onFailure,
|
||||
failureAction: user.failureAction ?? base.failureAction
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +291,15 @@ export async function loadInitdUnitDropIns(vfs, services, defaultAfter) {
|
||||
fromFile.after = [...new Set([...def, ...fromFile.after])]
|
||||
map.set(s.name, fromFile)
|
||||
}
|
||||
for (const [name, di] of map) {
|
||||
for (const b of di.before) {
|
||||
if (!/^[a-zA-Z0-9._-]+$/.test(b)) continue
|
||||
const target = map.get(b)
|
||||
if (target) {
|
||||
target.after = [...new Set([...target.after, name])]
|
||||
}
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
INITD_LOG,
|
||||
KERNEL_CONSOLE_LOG
|
||||
} from './bare-os-var-log.js'
|
||||
import { appendBareInitdJournal } from './bare-initd-journal.js'
|
||||
import {
|
||||
BARE_INITD_DEFAULT_AFTER,
|
||||
emptyUnitDropIn,
|
||||
@@ -287,6 +288,7 @@ function scheduleUnitHealth(ctx, name, dropIn) {
|
||||
/* ignore */
|
||||
}
|
||||
void appendVarLog(ctx, INITD_LOG, name, msg)
|
||||
appendBareInitdJournal(name, { event: 'health_failed', error: msg })
|
||||
}
|
||||
})()
|
||||
}, intervalMs)
|
||||
@@ -370,6 +372,94 @@ export async function restartBareService(ctx, name) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
const maxAttempts =
|
||||
dropIn.restart === 'on-failure' || dropIn.restart === 'always' ? 3 : 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) {
|
||||
appendBareInitdJournal(s.name, { event: 'restart_attempt', attempt })
|
||||
await new Promise((r) => setTimeout(r, restartDelayMs))
|
||||
}
|
||||
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
|
||||
*/
|
||||
@@ -377,10 +467,12 @@ 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 dropIns = await loadInitdUnitDropIns(
|
||||
dropInsMap = await loadInitdUnitDropIns(
|
||||
vfs,
|
||||
registry,
|
||||
BARE_INITD_DEFAULT_AFTER
|
||||
@@ -391,7 +483,7 @@ export async function startBareInitd(ctx) {
|
||||
const wantsMap = new Map()
|
||||
/** @type {Map<string, string[]>} */
|
||||
const afterMap = new Map()
|
||||
for (const [name, di] of dropIns) {
|
||||
for (const [name, di] of dropInsMap) {
|
||||
afterMap.set(name, di.after)
|
||||
requiresMap.set(name, di.requires)
|
||||
wantsMap.set(name, di.wants)
|
||||
@@ -404,34 +496,88 @@ export async function startBareInitd(ctx) {
|
||||
wantsMap
|
||||
)
|
||||
}
|
||||
for (const s of ordered) {
|
||||
const t0 = Date.now()
|
||||
/** @type {import('./bare-initd-user.js').BareInitdUnitDropIn} */
|
||||
const dropIn =
|
||||
vfs && typeof vfs.readFile === 'function'
|
||||
? await readUnitDropIn(vfs, s.name)
|
||||
: emptyUnitDropIn()
|
||||
|
||||
if (dropIn.socketActivationIpc && ctx.bareOsIpc) {
|
||||
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)
|
||||
}
|
||||
|
||||
/** @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))
|
||||
|
||||
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()
|
||||
appendBareInitdJournal(s.name, { event: 'socket_wait' })
|
||||
try {
|
||||
ctx.bareOsIpc.create(dropIn.socketActivationIpc)
|
||||
ctx.bareOsIpc.create(di.socketActivationIpc)
|
||||
} catch {
|
||||
/* exists */
|
||||
}
|
||||
runtime.set(s.name, { phase: 'inactive', startedAtMs: t0 })
|
||||
const ipcName = dropIn.socketActivationIpc
|
||||
const startSec = dropIn.timeoutStartSec
|
||||
const ipcName = di.socketActivationIpc
|
||||
const startSec = di.timeoutStartSec
|
||||
void (async () => {
|
||||
try {
|
||||
await ctx.bareOsIpc.take(ipcName)
|
||||
const t1 = Date.now()
|
||||
await withTimeoutSec(s.start(ctx), startSec, `start ${s.name}`)
|
||||
if (dropIn.readinessPath) {
|
||||
const rsec = dropIn.readinessTimeoutSec ?? 30
|
||||
await waitForReadinessPath(ctx, dropIn.readinessPath, rsec)
|
||||
if (di.readinessPath) {
|
||||
const rsec = di.readinessTimeoutSec ?? 30
|
||||
await waitForReadinessPath(ctx, di.readinessPath, rsec)
|
||||
}
|
||||
runtime.set(s.name, { phase: 'active', startedAtMs: t1 })
|
||||
const post = dropIn.execStartPost
|
||||
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())
|
||||
@@ -447,7 +593,7 @@ export async function startBareInitd(ctx) {
|
||||
void appendVarLog(ctx, INITD_LOG, s.name, 'ExecStartPost: ' + msg)
|
||||
}
|
||||
}
|
||||
scheduleUnitHealth(ctx, s.name, dropIn)
|
||||
scheduleUnitHealth(ctx, s.name, di)
|
||||
} catch (e) {
|
||||
const msg = e?.message || String(e)
|
||||
runtime.set(s.name, { phase: 'failed', startedAtMs: t0, error: msg })
|
||||
@@ -457,59 +603,17 @@ export async function startBareInitd(ctx) {
|
||||
/* ignore */
|
||||
}
|
||||
void appendVarLog(ctx, INITD_LOG, s.name, msg)
|
||||
appendBareInitdJournal(s.name, { event: 'failed_socket', error: msg })
|
||||
await runOnFailureHookForUnit(ctx, s, di)
|
||||
}
|
||||
})()
|
||||
continue
|
||||
}
|
||||
|
||||
const startSec = dropIn.timeoutStartSec
|
||||
const maxAttempts =
|
||||
dropIn.restart === 'on-failure' || dropIn.restart === 'always' ? 3 : 1
|
||||
const restartDelayMs =
|
||||
dropIn.restartSec != null && dropIn.restartSec >= 0
|
||||
? Math.round(dropIn.restartSec * 1000)
|
||||
: 1000
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
try {
|
||||
if (attempt > 0) {
|
||||
await new Promise((r) => setTimeout(r, restartDelayMs))
|
||||
}
|
||||
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 })
|
||||
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)
|
||||
break
|
||||
} catch (e) {
|
||||
const msg = e?.message || String(e)
|
||||
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)
|
||||
}
|
||||
}
|
||||
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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
'use strict'
|
||||
/**
|
||||
* CJS bridge: `require('bare-crypto/lib/key.js')` bypasses package `exports` on Node and avoids
|
||||
* pulling Bare-only addon graphs into ESM static analysis. Pear/Bare executes this file with CJS
|
||||
* `require` (no Node `module` built-in needed from our ESM entry).
|
||||
*/
|
||||
const { verify } = require('bare-crypto')
|
||||
const { Ed25519PublicKey } = require('bare-crypto/lib/key.js')
|
||||
module.exports = { verify, Ed25519PublicKey }
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Pear / Bare: load crypto via CJS bridge (`require` provided by runtime). No `node:module`.
|
||||
*/
|
||||
|
||||
import b4a from 'b4a'
|
||||
import bridge from './bare-os-boot-manifest-sig-bridge.cjs'
|
||||
|
||||
const { verify, Ed25519PublicKey } = bridge
|
||||
|
||||
/**
|
||||
* @param {Uint8Array | null | undefined} manifestBytes
|
||||
* @param {Uint8Array | null | undefined} signatureBytes
|
||||
* @param {string} publicKeyHex 64 hex chars (32-byte Ed25519 public key)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function verifyBootManifestEd25519(manifestBytes, signatureBytes, publicKeyHex) {
|
||||
if (!manifestBytes?.length || !signatureBytes?.length) return false
|
||||
const hex = String(publicKeyHex || '').trim().toLowerCase().replace(/^0x/, '')
|
||||
if (!/^[0-9a-f]{64}$/.test(hex)) return false
|
||||
/** @type {Uint8Array} */
|
||||
let sig
|
||||
if (signatureBytes.length === 64) {
|
||||
sig = signatureBytes
|
||||
} else {
|
||||
const t = b4a.toString(signatureBytes, 'utf8').trim()
|
||||
if (/^[0-9a-f]{128}$/i.test(t)) {
|
||||
sig = b4a.from(t, 'hex')
|
||||
} else {
|
||||
try {
|
||||
sig = b4a.from(t, 'base64')
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sig.length !== 64) return false
|
||||
try {
|
||||
const pub = b4a.from(hex, 'hex')
|
||||
const key = new Ed25519PublicKey(pub)
|
||||
return verify('ed25519', manifestBytes, key, sig)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Node (brittle-node tests / `node index.js`): lazy `require('bare-crypto')` inside try/catch.
|
||||
* `bare-crypto` is Bare-oriented and typically throws under Node; callers still get a boolean.
|
||||
*/
|
||||
|
||||
import b4a from 'b4a'
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
/**
|
||||
* @param {Uint8Array | null | undefined} manifestBytes
|
||||
* @param {Uint8Array | null | undefined} signatureBytes
|
||||
* @param {string} publicKeyHex 64 hex chars (32-byte Ed25519 public key)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function verifyBootManifestEd25519(manifestBytes, signatureBytes, publicKeyHex) {
|
||||
if (!manifestBytes?.length || !signatureBytes?.length) return false
|
||||
const hex = String(publicKeyHex || '').trim().toLowerCase().replace(/^0x/, '')
|
||||
if (!/^[0-9a-f]{64}$/.test(hex)) return false
|
||||
/** @type {Uint8Array} */
|
||||
let sig
|
||||
if (signatureBytes.length === 64) {
|
||||
sig = signatureBytes
|
||||
} else {
|
||||
const t = b4a.toString(signatureBytes, 'utf8').trim()
|
||||
if (/^[0-9a-f]{128}$/i.test(t)) {
|
||||
sig = b4a.from(t, 'hex')
|
||||
} else {
|
||||
try {
|
||||
sig = b4a.from(t, 'base64')
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sig.length !== 64) return false
|
||||
try {
|
||||
const { verify } = require('bare-crypto')
|
||||
const { Ed25519PublicKey } = require('bare-crypto/lib/key.js')
|
||||
const pub = b4a.from(hex, 'hex')
|
||||
const key = new Ed25519PublicKey(pub)
|
||||
return verify('ed25519', manifestBytes, key, sig)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,4 @@
|
||||
* Semantic version of the booter `ctx` contract for custom kernels.
|
||||
* Bump when adding/removing/renaming documented `ctx` fields or changing behavior.
|
||||
*/
|
||||
export const BARE_OS_CTX_API_VERSION = '1.8.0'
|
||||
export const BARE_OS_CTX_API_VERSION = '1.9.0'
|
||||
|
||||
+18
@@ -56,6 +56,11 @@ export interface BareOsIpc {
|
||||
take: () => Promise<Uint8Array>
|
||||
}
|
||||
}
|
||||
duplexJsonRoundTrip(
|
||||
baseName: string,
|
||||
payload: Record<string, unknown>,
|
||||
opts?: { timeoutMs?: number }
|
||||
): Promise<Record<string, unknown>>
|
||||
}
|
||||
|
||||
export interface BareOsHostStats {
|
||||
@@ -120,6 +125,19 @@ export interface BareOsKernelContext {
|
||||
opts?: BareOsAbortOpts
|
||||
): Promise<void>
|
||||
bareOsBootFileSha256Hex(buf: Uint8Array | ArrayBuffer): string
|
||||
bareOsVerifyBootManifestSignature(
|
||||
manifestBytes: Uint8Array | ArrayBuffer | null | undefined,
|
||||
signatureBytes: Uint8Array | ArrayBuffer | null | undefined,
|
||||
publicKeyHex?: string
|
||||
): boolean
|
||||
bareOsRequestMirror(opts?: {
|
||||
key?: string
|
||||
label?: string
|
||||
}): Promise<{ ok: boolean; hint: string }>
|
||||
bareOsExportPersonalSnapshot(opts?: {
|
||||
label?: string
|
||||
}): Promise<{ ok: boolean; hint: string }>
|
||||
bareOsPearIpcEmit(channel: string, payload: Record<string, unknown>): boolean
|
||||
bareOsRequestPearReload(opts?: {
|
||||
persistRequest?: boolean
|
||||
}): Promise<{
|
||||
|
||||
@@ -295,6 +295,31 @@ export function createBareOsIpc(opts = {}) {
|
||||
take: () => ba.take()
|
||||
}
|
||||
return { left, right }
|
||||
},
|
||||
|
||||
/**
|
||||
* One JSON line request/response over a {@link createDuplexBridge} side (`bareOsRpc: "2"`).
|
||||
* @param {{ push: (buf: Uint8Array | ArrayBuffer) => void, take: () => Promise<Uint8Array> }} side
|
||||
* @param {Record<string, unknown>} request
|
||||
*/
|
||||
async duplexJsonRoundTrip(side, request) {
|
||||
if (!request || typeof request !== 'object')
|
||||
throw new Error('bare-os ipc: invalid duplex request')
|
||||
const line =
|
||||
JSON.stringify({
|
||||
bareOsRpc: '2',
|
||||
...request
|
||||
}) + '\n'
|
||||
if (line.length > maxJsonLine) {
|
||||
throw new Error('bare-os ipc: duplex JSON line exceeds max length')
|
||||
}
|
||||
side.push(b4a.from(line, 'utf8'))
|
||||
const u8 = await side.take()
|
||||
const res = b4a.toString(u8, 'utf8').trim()
|
||||
if (res.length > maxJsonLine) {
|
||||
throw new Error('bare-os ipc: duplex response too large')
|
||||
}
|
||||
return /** @type {Record<string, unknown>} */ (JSON.parse(res))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ export const BARE_OS_PSEUDO_FS_PATHS = Object.freeze([
|
||||
'/proc/bare_os_quotas',
|
||||
'/proc/bare_os_resources',
|
||||
'/proc/bare_os_swarm',
|
||||
'/proc/bare_os_replication',
|
||||
'/proc/bare_os_capabilities',
|
||||
'/proc/bare_os_capabilities.json',
|
||||
'/proc/bare_os_bootstrap',
|
||||
'/proc/bare_os_session_stats',
|
||||
'/proc/bare_os_version',
|
||||
'/proc/cpuinfo',
|
||||
@@ -52,6 +56,7 @@ export const BARE_OS_PSEUDO_FS_PATHS = Object.freeze([
|
||||
'/run/bare-os/ready',
|
||||
'/run/bare-os/session',
|
||||
'/run/bare-os/units',
|
||||
'/run/bare-os/unit-journal',
|
||||
'/run/bare-os/virtual',
|
||||
'/run/bare-os/ipc',
|
||||
'/sys',
|
||||
@@ -165,7 +170,29 @@ export function buildBareOsRuntimeCaps(shellEnv) {
|
||||
shellEnv.BARE_OS_BOOT_MANIFEST === '1' ||
|
||||
shellEnv.BARE_OS_BOOT_MANIFEST === 'true',
|
||||
bareModuleLockfile: true,
|
||||
oidcPublishHook: true
|
||||
oidcPublishHook: true,
|
||||
ipcDuplexJsonRpc: true,
|
||||
procReplicationSnapshot: true,
|
||||
procCapabilitiesExport: true,
|
||||
blindBootstrapProc: true,
|
||||
initdUnitJournal: true,
|
||||
initdParallelStart:
|
||||
Number.parseInt(
|
||||
String(shellEnv.BARE_OS_INITD_MAX_PARALLEL || '1'),
|
||||
10
|
||||
) > 1,
|
||||
vfsBinReadCache:
|
||||
shellEnv.BARE_OS_VFS_BIN_CACHE === '1' ||
|
||||
shellEnv.BARE_OS_VFS_BIN_CACHE === 'true',
|
||||
bootManifestEd25519:
|
||||
shellEnv.BARE_OS_BOOT_MANIFEST_SIGN === '1' ||
|
||||
shellEnv.BARE_OS_BOOT_MANIFEST_SIGN === 'true',
|
||||
sandboxWorkerHint:
|
||||
shellEnv.BARE_OS_SANDBOX_WORKER === '1' ||
|
||||
shellEnv.BARE_OS_SANDBOX_WORKER === 'true',
|
||||
mirrorRequestHook: true,
|
||||
personalSnapshotExportHook: true,
|
||||
pearIpcEmitHook: true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -367,6 +367,16 @@ export function tokenize(line) {
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (c === '(') {
|
||||
tokens.push({ type: 'op', value: '(' })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c === ')') {
|
||||
tokens.push({ type: 'op', value: ')' })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c === '<') {
|
||||
if (line[i + 1] === '<' && line[i + 2] === '<') {
|
||||
tokens.push({ type: 'op', value: '<<<' })
|
||||
@@ -418,7 +428,9 @@ export function tokenize(line) {
|
||||
ch === '>' ||
|
||||
ch === '<' ||
|
||||
ch === ';' ||
|
||||
ch === '&'
|
||||
ch === '&' ||
|
||||
ch === '(' ||
|
||||
ch === ')'
|
||||
)
|
||||
break
|
||||
word += ch
|
||||
@@ -1175,6 +1187,8 @@ function splitTopLevelStatements(tokens) {
|
||||
else if (t.value === 'fi') depth = Math.max(0, depth - 1)
|
||||
else if (t.value === 'while' || t.value === 'for') depth++
|
||||
else if (t.value === 'done') depth = Math.max(0, depth - 1)
|
||||
else if (t.value === 'case') depth++
|
||||
else if (t.value === 'esac') depth = Math.max(0, depth - 1)
|
||||
}
|
||||
if (t.type === 'op' && t.value === ';' && depth === 0) {
|
||||
if (cur.length) out.push(cur)
|
||||
@@ -1204,6 +1218,8 @@ function splitTopLevelByAmpersand(tokens) {
|
||||
else if (t.value === 'fi') depth = Math.max(0, depth - 1)
|
||||
else if (t.value === 'while' || t.value === 'for') depth++
|
||||
else if (t.value === 'done') depth = Math.max(0, depth - 1)
|
||||
else if (t.value === 'case') depth++
|
||||
else if (t.value === 'esac') depth = Math.max(0, depth - 1)
|
||||
}
|
||||
if (t.type === 'op' && t.value === '&' && depth === 0) {
|
||||
out.push(cur)
|
||||
@@ -1447,6 +1463,129 @@ async function execForConstruct(ctx, tokens) {
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Token[]} toks
|
||||
* @param {Record<string, string>} env
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function casePatternList(toks, env) {
|
||||
/** @type {string[]} */
|
||||
const out = []
|
||||
/** @type {Token[]} */
|
||||
let cur = []
|
||||
for (const t of toks) {
|
||||
if (t.type === 'op' && t.value === '|') {
|
||||
if (cur.length) {
|
||||
const s = cur.map((w) => w.value).join(' ')
|
||||
out.push(expandWord(s.trim(), env))
|
||||
cur = []
|
||||
}
|
||||
} else if (t.type === 'word') {
|
||||
cur.push(t)
|
||||
}
|
||||
}
|
||||
if (cur.length) {
|
||||
const s = cur.map((w) => w.value).join(' ')
|
||||
out.push(expandWord(s.trim(), env))
|
||||
}
|
||||
return out.filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} subject
|
||||
* @param {string} pat
|
||||
*/
|
||||
function casePatternMatches(subject, pat) {
|
||||
if (pat === '*') return true
|
||||
return subject === pat
|
||||
}
|
||||
|
||||
/**
|
||||
* `case WORD in pattern) list ;; … esac` — bounded branches; patterns support `|` alternation and `*`.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Token[]} tokens
|
||||
* @returns {Promise<'exit' | 'ok'>}
|
||||
*/
|
||||
async function execCaseConstruct(ctx, tokens) {
|
||||
const last = tokens[tokens.length - 1]
|
||||
if (last.type !== 'word' || last.value !== 'esac') {
|
||||
ctx.console.error('shell: case: missing esac')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
if (
|
||||
tokens.length < 5 ||
|
||||
tokens[1].type !== 'word' ||
|
||||
tokens[2].type !== 'word' ||
|
||||
tokens[2].value !== 'in'
|
||||
) {
|
||||
ctx.console.error('shell: case: expected `case WORD in`')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
const env = ctx.vfs.env
|
||||
const subj = expandWord(tokens[1].value, env)
|
||||
const maxBranches = Number.parseInt(
|
||||
ctx.vfs?.env?.BARE_OS_SHELL_CASE_MAX_BRANCHES || '32',
|
||||
10
|
||||
)
|
||||
const cap = Number.isFinite(maxBranches) && maxBranches > 0 ? maxBranches : 32
|
||||
let i = 3
|
||||
let branches = 0
|
||||
while (i < tokens.length - 1) {
|
||||
if (++branches > cap) {
|
||||
ctx.console.error('shell: case: too many branches (see BARE_OS_SHELL_CASE_MAX_BRANCHES)')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
let paren = -1
|
||||
for (let k = i; k < tokens.length - 1; k++) {
|
||||
const t = tokens[k]
|
||||
if (t.type === 'op' && t.value === ')') {
|
||||
paren = k
|
||||
break
|
||||
}
|
||||
}
|
||||
if (paren < 0) {
|
||||
ctx.console.error('shell: case: expected )')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
const patToks = tokens.slice(i, paren)
|
||||
let dsemi = -1
|
||||
for (let k = paren + 1; k < tokens.length - 1; k++) {
|
||||
const t = tokens[k]
|
||||
const n = tokens[k + 1]
|
||||
if (
|
||||
t.type === 'op' &&
|
||||
t.value === ';' &&
|
||||
n &&
|
||||
n.type === 'op' &&
|
||||
n.value === ';'
|
||||
) {
|
||||
dsemi = k
|
||||
break
|
||||
}
|
||||
}
|
||||
if (dsemi < 0) {
|
||||
ctx.console.error('shell: case: expected ;;')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
const bodyToks = tokens.slice(paren + 1, dsemi)
|
||||
const pats = casePatternList(patToks, env)
|
||||
const matched = pats.some((p) => casePatternMatches(subj, p))
|
||||
if (matched) {
|
||||
const r = await execSemicolonLists(ctx, bodyToks)
|
||||
if (r === 'exit') return 'exit'
|
||||
return 'ok'
|
||||
}
|
||||
i = dsemi + 2
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Token[]} stmt
|
||||
@@ -1460,6 +1599,8 @@ async function dispatchShellStatement(ctx, stmt) {
|
||||
return execWhileConstruct(ctx, stmt)
|
||||
if (head?.type === 'word' && head.value === 'for')
|
||||
return execForConstruct(ctx, stmt)
|
||||
if (head?.type === 'word' && head.value === 'case')
|
||||
return execCaseConstruct(ctx, stmt)
|
||||
return execAndOrList(ctx, stmt)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
readInitdDisabledSet,
|
||||
writeInitdDisabledSet
|
||||
} from './bare-initd-user.js'
|
||||
import { getBareInitdJournalNdjson } from './bare-initd-journal.js'
|
||||
|
||||
/** @param {Uint8Array | null} buf @param {number} maxLines */
|
||||
function tailUtf8Lines(buf, maxLines) {
|
||||
@@ -112,6 +113,13 @@ export async function runSystemctlCli(ctx, argv) {
|
||||
return
|
||||
}
|
||||
await printLogTail(ctx, def.logPath, lines, 'journalctl')
|
||||
const jtext = getBareInitdJournalNdjson(unit)
|
||||
if (jtext.trim()) {
|
||||
const jl = jtext.trimEnd().split(/\r?\n/)
|
||||
const tailJ = jl.slice(-lines).join('\n')
|
||||
ctx.console.log('--- unit journal (NDJSON tail) ---')
|
||||
ctx.console.log(tailJ)
|
||||
}
|
||||
ctx.exitCode = ctx.exitCode ?? 0
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import unixPathResolve from 'unix-path-resolve'
|
||||
import b4a from 'b4a'
|
||||
import { raceWithAbortAndTimeout } from './bare-os-abort.js'
|
||||
import { listBareInitdJournalUnits } from './bare-initd-journal.js'
|
||||
import {
|
||||
extractBareOs,
|
||||
identityNames,
|
||||
@@ -48,6 +49,11 @@ const DIR_MARKER = '.bareos_empty'
|
||||
* procBareOsResourcesText?: () => string,
|
||||
* procBareOsFeaturesText?: () => string,
|
||||
* procBareOsSwarmText?: () => string,
|
||||
* procBareOsReplicationText?: () => string,
|
||||
* procBareOsCapabilitiesText?: () => string,
|
||||
* procBareOsCapabilitiesJsonText?: () => string,
|
||||
* procBareOsBootstrapText?: () => string,
|
||||
* getUnitJournalNdjson?: (unit: string) => string,
|
||||
* getVirtualReaders?: () => Map<string, unknown>,
|
||||
* unionReadPrefixes?: readonly string[],
|
||||
* sysClassNetLoText?: () => string
|
||||
@@ -118,6 +124,26 @@ export function createVfs(
|
||||
typeof vfsOptions.procBareOsSwarmText === 'function'
|
||||
? vfsOptions.procBareOsSwarmText
|
||||
: null
|
||||
const procBareOsReplicationText =
|
||||
typeof vfsOptions.procBareOsReplicationText === 'function'
|
||||
? vfsOptions.procBareOsReplicationText
|
||||
: null
|
||||
const procBareOsCapabilitiesText =
|
||||
typeof vfsOptions.procBareOsCapabilitiesText === 'function'
|
||||
? vfsOptions.procBareOsCapabilitiesText
|
||||
: null
|
||||
const procBareOsCapabilitiesJsonText =
|
||||
typeof vfsOptions.procBareOsCapabilitiesJsonText === 'function'
|
||||
? vfsOptions.procBareOsCapabilitiesJsonText
|
||||
: null
|
||||
const procBareOsBootstrapText =
|
||||
typeof vfsOptions.procBareOsBootstrapText === 'function'
|
||||
? vfsOptions.procBareOsBootstrapText
|
||||
: null
|
||||
const getUnitJournalNdjson =
|
||||
typeof vfsOptions.getUnitJournalNdjson === 'function'
|
||||
? vfsOptions.getUnitJournalNdjson
|
||||
: null
|
||||
const getVirtualReaders =
|
||||
typeof vfsOptions.getVirtualReaders === 'function'
|
||||
? vfsOptions.getVirtualReaders
|
||||
@@ -127,6 +153,11 @@ export function createVfs(
|
||||
(s) => typeof s === 'string' && s.startsWith('/')
|
||||
)
|
||||
: []
|
||||
const binCacheEnabled =
|
||||
env.BARE_OS_VFS_BIN_CACHE === '1' || env.BARE_OS_VFS_BIN_CACHE === 'true'
|
||||
/** @type {Map<string, Uint8Array>} */
|
||||
const binReadCache = binCacheEnabled ? new Map() : null
|
||||
const BIN_READ_CACHE_MAX = 64
|
||||
const sysClassNetLoText =
|
||||
typeof vfsOptions.sysClassNetLoText === 'function'
|
||||
? vfsOptions.sysClassNetLoText
|
||||
@@ -321,6 +352,30 @@ export function createVfs(
|
||||
const t = procBareOsSwarmText ? procBareOsSwarmText() : '{}\n'
|
||||
return utf8Encode(t)
|
||||
}
|
||||
if (f === 'bare_os_replication') {
|
||||
const t = procBareOsReplicationText
|
||||
? procBareOsReplicationText()
|
||||
: '{}\n'
|
||||
return utf8Encode(t)
|
||||
}
|
||||
if (f === 'bare_os_capabilities') {
|
||||
const t = procBareOsCapabilitiesText
|
||||
? procBareOsCapabilitiesText()
|
||||
: '(no snapshot)\n'
|
||||
return utf8Encode(t)
|
||||
}
|
||||
if (f === 'bare_os_capabilities_json') {
|
||||
const t = procBareOsCapabilitiesJsonText
|
||||
? procBareOsCapabilitiesJsonText()
|
||||
: '{}\n'
|
||||
return utf8Encode(t)
|
||||
}
|
||||
if (f === 'bare_os_bootstrap') {
|
||||
const t = procBareOsBootstrapText
|
||||
? procBareOsBootstrapText()
|
||||
: '{}\n'
|
||||
return utf8Encode(t)
|
||||
}
|
||||
if (f === 'net_dev') {
|
||||
const t = procNetDevText
|
||||
? procNetDevText()
|
||||
@@ -338,6 +393,13 @@ export function createVfs(
|
||||
: '# bare-initd: no snapshot provider\n'
|
||||
return utf8Encode(t)
|
||||
}
|
||||
if (k === 'run' && f === 'unit_journal' && getUnitJournalNdjson) {
|
||||
const u =
|
||||
/** @type {{ unitJournalName?: string }} */ (routePseudo).unitJournalName ||
|
||||
''
|
||||
const t = getUnitJournalNdjson(u)
|
||||
return utf8Encode(t || '')
|
||||
}
|
||||
if (k === 'run' && f === 'boot_profile') {
|
||||
const t = bootProfileText ? bootProfileText() : '\n'
|
||||
return utf8Encode(t)
|
||||
@@ -489,6 +551,38 @@ export function createVfs(
|
||||
file: 'bare_os_swarm'
|
||||
}
|
||||
}
|
||||
if (sub === 'bare_os_replication') {
|
||||
return {
|
||||
virtualPseudo: true,
|
||||
kind: 'proc',
|
||||
node: 'file',
|
||||
file: 'bare_os_replication'
|
||||
}
|
||||
}
|
||||
if (sub === 'bare_os_capabilities') {
|
||||
return {
|
||||
virtualPseudo: true,
|
||||
kind: 'proc',
|
||||
node: 'file',
|
||||
file: 'bare_os_capabilities'
|
||||
}
|
||||
}
|
||||
if (sub === 'bare_os_capabilities.json') {
|
||||
return {
|
||||
virtualPseudo: true,
|
||||
kind: 'proc',
|
||||
node: 'file',
|
||||
file: 'bare_os_capabilities_json'
|
||||
}
|
||||
}
|
||||
if (sub === 'bare_os_bootstrap') {
|
||||
return {
|
||||
virtualPseudo: true,
|
||||
kind: 'proc',
|
||||
node: 'file',
|
||||
file: 'bare_os_bootstrap'
|
||||
}
|
||||
}
|
||||
if (sub === 'net' || sub === 'net/') {
|
||||
return { virtualPseudo: true, kind: 'proc', node: 'dir', dir: 'net' }
|
||||
}
|
||||
@@ -512,6 +606,29 @@ export function createVfs(
|
||||
if (n === '/run/bare-os/units') {
|
||||
return { virtualPseudo: true, kind: 'run', node: 'file', file: 'units' }
|
||||
}
|
||||
if (n === '/run/bare-os/unit-journal' || n === '/run/bare-os/unit-journal/') {
|
||||
return {
|
||||
virtualPseudo: true,
|
||||
kind: 'run',
|
||||
node: 'dir',
|
||||
dir: 'unit_journal_root'
|
||||
}
|
||||
}
|
||||
{
|
||||
const uj = '/run/bare-os/unit-journal/'
|
||||
if (n.startsWith(uj)) {
|
||||
const seg = n.slice(uj.length).replace(/\/+$/, '')
|
||||
if (/^[a-zA-Z0-9._-]+\.ndjson$/.test(seg)) {
|
||||
return {
|
||||
virtualPseudo: true,
|
||||
kind: 'run',
|
||||
node: 'file',
|
||||
file: 'unit_journal',
|
||||
unitJournalName: seg.replace(/\.ndjson$/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (n === '/run/bare-os/boot_profile') {
|
||||
return {
|
||||
virtualPseudo: true,
|
||||
@@ -1197,8 +1314,12 @@ export function createVfs(
|
||||
}
|
||||
if (pr.kind === 'proc' && pr.node === 'root') {
|
||||
return [
|
||||
'bare_os_bootstrap',
|
||||
'bare_os_capabilities',
|
||||
'bare_os_capabilities.json',
|
||||
'bare_os_features',
|
||||
'bare_os_quotas',
|
||||
'bare_os_replication',
|
||||
'bare_os_resources',
|
||||
'bare_os_session_stats',
|
||||
'bare_os_swarm',
|
||||
@@ -1245,11 +1366,20 @@ export function createVfs(
|
||||
'ipc',
|
||||
'ready',
|
||||
'session',
|
||||
'unit-journal',
|
||||
'units',
|
||||
'virtual'
|
||||
]
|
||||
return bareOsIpc ? base : base.filter((x) => x !== 'ipc')
|
||||
}
|
||||
if (
|
||||
pr.kind === 'run' &&
|
||||
pr.node === 'dir' &&
|
||||
pr.dir === 'unit_journal_root' &&
|
||||
getUnitJournalNdjson
|
||||
) {
|
||||
return listBareInitdJournalUnits().map((u) => `${u}.ndjson`)
|
||||
}
|
||||
if (
|
||||
pr.kind === 'run' &&
|
||||
pr.node === 'dir' &&
|
||||
@@ -1546,7 +1676,24 @@ export function createVfs(
|
||||
}
|
||||
}
|
||||
}
|
||||
return drive.get(p, { follow: true })
|
||||
if (binReadCache && drive === systemDrive && abs.startsWith('/bin/')) {
|
||||
const hit = binReadCache.get(abs)
|
||||
if (hit) return new Uint8Array(hit)
|
||||
}
|
||||
const got = await drive.get(p, { follow: true })
|
||||
if (
|
||||
binReadCache &&
|
||||
drive === systemDrive &&
|
||||
abs.startsWith('/bin/') &&
|
||||
got
|
||||
) {
|
||||
if (binReadCache.size >= BIN_READ_CACHE_MAX) {
|
||||
const first = binReadCache.keys().next().value
|
||||
binReadCache.delete(first)
|
||||
}
|
||||
binReadCache.set(abs, new Uint8Array(got))
|
||||
}
|
||||
return got
|
||||
})(),
|
||||
abortOpts,
|
||||
'vfs.readFile'
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"node:url": {
|
||||
"bare": "bare-url",
|
||||
"default": "node:url"
|
||||
},
|
||||
"#bare-os-boot-manifest-sig": {
|
||||
"bare": "./lib/bare-os-boot-manifest-sig.bare.js",
|
||||
"default": "./lib/bare-os-boot-manifest-sig.node.js"
|
||||
}
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -855,8 +855,12 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
|
||||
t.ok(root.includes('run'))
|
||||
t.ok(root.includes('dev'))
|
||||
t.alike(await vfs.readdir('/proc').then((a) => [...a].sort()), [
|
||||
'bare_os_bootstrap',
|
||||
'bare_os_capabilities',
|
||||
'bare_os_capabilities.json',
|
||||
'bare_os_features',
|
||||
'bare_os_quotas',
|
||||
'bare_os_replication',
|
||||
'bare_os_resources',
|
||||
'bare_os_session_stats',
|
||||
'bare_os_swarm',
|
||||
@@ -917,6 +921,7 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
|
||||
'ipc',
|
||||
'ready',
|
||||
'session',
|
||||
'unit-journal',
|
||||
'units',
|
||||
'virtual'
|
||||
])
|
||||
@@ -1199,6 +1204,7 @@ test('buildBareOsRuntimeCaps matches ctx API version and pipeline env', async (t
|
||||
t.ok(caps.pseudoFsPaths.includes('/proc/mounts'))
|
||||
t.ok(caps.pseudoFsPaths.includes('/proc/bare_os_resources'))
|
||||
t.ok(caps.pseudoFsPaths.includes('/run/bare-os/virtual'))
|
||||
t.ok(caps.pseudoFsPaths.includes('/run/bare-os/unit-journal'))
|
||||
t.is(caps.features.simulatedPipelines, true)
|
||||
t.is(caps.features.httpDelegate, true)
|
||||
t.is(caps.features.gitDelegate, true)
|
||||
@@ -3904,6 +3910,63 @@ test('coreutils gnu-gap batch: paste tac rev md5sum expr tsort numfmt truncate i
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('verifyBootManifestEd25519 rejects invalid inputs', async (t) => {
|
||||
const { verifyBootManifestEd25519 } = await import('#bare-os-boot-manifest-sig')
|
||||
const msg = b4a.from('manifest-bytes', 'utf8')
|
||||
t.absent(verifyBootManifestEd25519(msg, null, ''))
|
||||
t.absent(verifyBootManifestEd25519(msg, msg, '00ff'))
|
||||
const z64 = '0'.repeat(64)
|
||||
t.absent(verifyBootManifestEd25519(msg, new Uint8Array(64), z64))
|
||||
})
|
||||
|
||||
test('bareOsIpc.duplexJsonRoundTrip', async (t) => {
|
||||
const ipc = createBareOsIpc()
|
||||
const { left, right } = ipc.createDuplexBridge('dupjx')
|
||||
const respP = ipc.duplexJsonRoundTrip(right, { id: 7, method: 'ping' })
|
||||
const u8 = await left.take()
|
||||
const req = JSON.parse(b4a.toString(u8, 'utf8'))
|
||||
t.is(req.method, 'ping')
|
||||
left.push(
|
||||
b4a.from(
|
||||
JSON.stringify({ bareOsRpc: '2', id: 7, result: 'pong' }) + '\n',
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
const out = await respP
|
||||
t.is(out.result, 'pong')
|
||||
})
|
||||
|
||||
test('unit journal exposed under /run/bare-os/unit-journal', async (t) => {
|
||||
const {
|
||||
appendBareInitdJournal,
|
||||
clearBareInitdJournalForTests,
|
||||
getBareInitdJournalNdjson
|
||||
} = await import('./lib/bare-initd-journal.js')
|
||||
clearBareInitdJournalForTests()
|
||||
appendBareInitdJournal('demo', { event: 'unit_test' })
|
||||
const dir = testCorestoreDir('vj')
|
||||
const store = new Corestore(dir)
|
||||
const sys = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pvj'))
|
||||
await sys.ready()
|
||||
await personal.ready()
|
||||
const env = {
|
||||
HOME: '/home/guest',
|
||||
PWD: '/home/guest',
|
||||
PATH: '/bin',
|
||||
USER: 'guest'
|
||||
}
|
||||
const vfs = createVfs(sys, personal, env, null, {
|
||||
getUnitJournalNdjson: (u) => getBareInitdJournalNdjson(u)
|
||||
})
|
||||
t.ok((await vfs.readdir('/run/bare-os/unit-journal')).includes('demo.ndjson'))
|
||||
const j = b4a.toString(await vfs.readFile('/run/bare-os/unit-journal/demo.ndjson'))
|
||||
t.ok(j.includes('unit_test'))
|
||||
clearBareInitdJournalForTests()
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function readBuiltBin(name) {
|
||||
const fs = await import('node:fs/promises')
|
||||
const p = path.join(__dirname, '../../kernel/bin', name)
|
||||
|
||||
Reference in New Issue
Block a user