This commit is contained in:
Raven Scott
2026-04-03 19:13:20 -04:00
parent 8002a66f6e
commit 04d903beb8
24 changed files with 564 additions and 74 deletions
+23 -1
View File
@@ -34,7 +34,9 @@ import {
import { HdmsController, runHdmsCli } from './lib/hdms-manager.js'
import {
startBareInitd,
registerKernelShutdownHook
registerKernelShutdownHook,
listBareServices,
getBareServiceRuntime
} from './lib/bare-initd.js'
import { BARE_OS_CTX_API_VERSION } from './lib/bare-os-ctx-api.js'
import './lib/bare-cron.js'
@@ -228,10 +230,26 @@ async function executeKernel(disk, store, swarm, initSource) {
}
/** @type {{ getMounts: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> }} */
const vfsMountRef = { getMounts: () => new Map() }
const bootStartedMs = Date.now()
const vfs = createVfs(disk.drive, disk.personalDrive, shellEnv, vfsMountRef, {
procSnapshot: {
version: BARE_OS_CTX_API_VERSION,
cmdline: 'bare-os-booter'
},
bootStartedMs,
initdRunText() {
const lines = [
'# bare-initd units (name<TAB>phase<TAB>startedAtMs<TAB>description)',
''
]
for (const s of listBareServices()) {
const rt = getBareServiceRuntime(s.name)
const phase = rt?.phase ?? 'inactive'
const started = rt?.startedAtMs ?? 0
const desc = (s.description || '').replace(/\t/g, ' ').replace(/\n/g, ' ')
lines.push(`${s.name}\t${phase}\t${started}\t${desc}`)
}
return lines.join('\n') + '\n'
}
})
@@ -244,6 +262,10 @@ async function executeKernel(disk, store, swarm, initSource) {
const ctx = {
/** Documented `ctx` contract version; bump in lib/bare-os-ctx-api.js when the surface changes. */
bareOsCtxApiVersion: BARE_OS_CTX_API_VERSION,
/** Milliseconds since Unix epoch when this session started VFS construction (for `/proc/uptime`). */
bareOsBootStartedMs: bootStartedMs,
/** True when `BARE_OS_SKIP_REPL=1` — stdin is non-interactive; `readLine` yields EOF immediately after boot. */
bareOsSkipRepl: skipInteractive,
disk,
drive: disk.drive,
personalDrive: disk.personalDrive,
@@ -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.0.0'
export const BARE_OS_CTX_API_VERSION = '1.1.0'
+58
View File
@@ -47,6 +47,38 @@ export function syncBareOsExitStatusEnv(ctx) {
/** Max alias indirections (prevents cycles). */
const MAX_ALIAS_DEPTH = 16
/** Default caps for simulated pipeline capture (`console.log` between stages). */
export const DEFAULT_PIPELINE_MAX_STAGES = 32
export const DEFAULT_PIPELINE_MAX_CAPTURE_BYTES = 2 * 1024 * 1024
export const DEFAULT_PIPELINE_MAX_CAPTURE_LINES = 50000
/**
* @param {Record<string, string> | null | undefined} env
*/
function pipelineLimitsFromEnv(env) {
const o = env && typeof env === 'object' ? env : {}
const parse = (key, def) => {
const v = o[key]
if (v == null || v === '') return def
const n = Number.parseInt(String(v), 10)
return Number.isFinite(n) && n > 0 ? n : def
}
return {
maxStages: parse(
'BARE_OS_PIPELINE_MAX_STAGES',
DEFAULT_PIPELINE_MAX_STAGES
),
maxBytes: parse(
'BARE_OS_PIPELINE_MAX_BYTES',
DEFAULT_PIPELINE_MAX_CAPTURE_BYTES
),
maxLines: parse(
'BARE_OS_PIPELINE_MAX_LINES',
DEFAULT_PIPELINE_MAX_CAPTURE_LINES
)
}
}
/**
* Baseline aliases; `~/.barerc` and `unalias -a` merge/reset from this table.
* @returns {Record<string, string>}
@@ -546,9 +578,18 @@ function segmentHasCommand(seg) {
async function execParsedPipeline(ctx, pipeline) {
const vfs = ctx.vfs
const env = vfs.env
const lim = pipelineLimitsFromEnv(env)
if (pipeline.length > lim.maxStages) {
ctx.console.error(
`shell: pipeline exceeds BARE_OS_PIPELINE_MAX_STAGES (${lim.maxStages})`
)
ctx.exitCode = 1
return 'ok'
}
let stdinText = typeof ctx.shellStdin === 'string' ? ctx.shellStdin : null
try {
for (let pi = 0; pi < pipeline.length; pi++) {
const cmd = pipeline[pi]
const isLast = pi === pipeline.length - 1
@@ -599,6 +640,18 @@ async function execParsedPipeline(ctx, pipeline) {
if (!isLast || cmd.redirOut) {
ctx.console.log = (...args) => {
outChunks.push(args.map(String).join(' ') + '\n')
const joined = outChunks.join('')
if (joined.length > lim.maxBytes) {
throw new Error(
`shell: pipeline output exceeds BARE_OS_PIPELINE_MAX_BYTES (${lim.maxBytes})`
)
}
const lineCount = joined.split('\n').length - 1
if (lineCount > lim.maxLines) {
throw new Error(
`shell: pipeline output exceeds BARE_OS_PIPELINE_MAX_LINES (${lim.maxLines})`
)
}
}
ctx.console.error = ctx.console.log
}
@@ -828,6 +881,11 @@ async function execParsedPipeline(ctx, pipeline) {
}
return 'ok'
} catch (e) {
ctx.console.error((e && e.message) || String(e))
ctx.exitCode = 1
return 'ok'
}
}
/**
@@ -330,7 +330,10 @@ export function isVirtualMountPoint(abs) {
n === '/proc/self' ||
n === '/sys' ||
n === '/sys/fs' ||
n === '/sys/fs/bare_os'
n === '/sys/fs/bare_os' ||
n === '/run' ||
n === '/run/bare-os' ||
n === '/dev'
)
}
+103 -6
View File
@@ -23,12 +23,16 @@ const DIR_MARKER = '.bareos_empty'
* Unified path view: system Hyperdrive for OS paths, personal Hyperdrive under $HOME,
* optional HDMS mounts under /mnt/<label>/…, virtual /var with writable /var/log/…
* on the personal drive under /.bare-os/var/log/<home-seg>/… (session-isolated).
* Read-only pseudo `proc` and `sys` under `/`; session `tmp` maps to `/.bare-os/tmp/<seg>/` on the personal drive.
* Read-only pseudo `proc`, `sys`, `run`, `dev` under `/`; session `tmp` maps to `/.bare-os/tmp/<seg>/` on the personal drive.
* @param {import('hyperdrive').default} systemDrive
* @param {import('hyperdrive').default} personalDrive
* @param {Record<string, string>} env
* @param {{ getMounts?: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> } | null} [mntRef]
* @param {{ procSnapshot?: { version?: string, cmdline?: string } }} [vfsOptions]
* @param {{
* procSnapshot?: { version?: string, cmdline?: string },
* bootStartedMs?: number,
* initdRunText?: () => string
* }} [vfsOptions]
*/
export function createVfs(
systemDrive,
@@ -38,6 +42,14 @@ export function createVfs(
vfsOptions = {}
) {
const procSnapshot = vfsOptions.procSnapshot || null
const bootStartedMs =
typeof vfsOptions.bootStartedMs === 'number'
? vfsOptions.bootStartedMs
: null
const initdRunText =
typeof vfsOptions.initdRunText === 'function'
? vfsOptions.initdRunText
: null
const HOME = () => env.HOME || '/home/guest'
let cwd = env.PWD || HOME()
@@ -132,11 +144,43 @@ export function createVfs(
return utf8Encode(parts.join(''))
}
function pseudoUptimeText() {
const start = bootStartedMs != null ? bootStartedMs : Date.now()
const up = Math.max(0, (Date.now() - start) / 1000)
return `${up.toFixed(2)} ${up.toFixed(2)}\n`
}
function pseudoMeminfoText() {
return [
'MemTotal: 524288 kB',
'MemFree: 262144 kB',
'MemAvailable: 262144 kB',
'SwapTotal: 0 kB',
'SwapFree: 0 kB',
''
].join('\n')
}
function pseudoFileBytes(routePseudo) {
const f = routePseudo.file
if (f === 'version') return utf8Encode(pseudoVersionText())
if (f === 'cmdline') return utf8Encode(pseudoCmdlineText())
if (f === 'environ') return pseudoEnvironBytes()
const k = routePseudo.kind
if (f === 'version' && (k === 'proc' || k === 'sys')) {
return utf8Encode(pseudoVersionText())
}
if (k === 'proc') {
if (f === 'cmdline') return utf8Encode(pseudoCmdlineText())
if (f === 'environ') return pseudoEnvironBytes()
if (f === 'uptime') return utf8Encode(pseudoUptimeText())
if (f === 'meminfo') return utf8Encode(pseudoMeminfoText())
}
if (k === 'run' && f === 'units') {
const t = initdRunText
? initdRunText()
: '# bare-initd: no snapshot provider\n'
return utf8Encode(t)
}
if (k === 'dev' && f === 'null') return utf8Encode('')
if (k === 'dev' && f === 'zero') return new Uint8Array(65536)
return utf8Encode('')
}
@@ -163,8 +207,38 @@ export function createVfs(
if (sub === 'self/cmdline') {
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'cmdline' }
}
if (sub === 'uptime') {
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'uptime' }
}
if (sub === 'meminfo') {
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'meminfo' }
}
return { virtualPseudo: true, kind: 'proc', node: 'enoent' }
}
if (n === '/run' || n.startsWith('/run/')) {
if (n === '/run') {
return { virtualPseudo: true, kind: 'run', node: 'root' }
}
if (n === '/run/bare-os') {
return { virtualPseudo: true, kind: 'run', node: 'dir', dir: 'bare_os' }
}
if (n === '/run/bare-os/units') {
return { virtualPseudo: true, kind: 'run', node: 'file', file: 'units' }
}
return { virtualPseudo: true, kind: 'run', node: 'enoent' }
}
if (n === '/dev' || n.startsWith('/dev/')) {
if (n === '/dev') {
return { virtualPseudo: true, kind: 'dev', node: 'root' }
}
if (n === '/dev/null') {
return { virtualPseudo: true, kind: 'dev', node: 'file', file: 'null' }
}
if (n === '/dev/zero') {
return { virtualPseudo: true, kind: 'dev', node: 'file', file: 'zero' }
}
return { virtualPseudo: true, kind: 'dev', node: 'enoent' }
}
if (n === '/sys' || n.startsWith('/sys/')) {
if (n === '/sys') {
return { virtualPseudo: true, kind: 'sys', node: 'root' }
@@ -653,7 +727,7 @@ export function createVfs(
throw new Error('Not a directory: ' + abs)
}
if (pr.kind === 'proc' && pr.node === 'root') {
return ['bare_os_version', 'self', 'version']
return ['bare_os_version', 'meminfo', 'self', 'uptime', 'version']
}
if (pr.kind === 'proc' && pr.node === 'dir' && pr.dir === 'self') {
return ['cmdline', 'environ']
@@ -667,6 +741,15 @@ export function createVfs(
if (pr.kind === 'sys' && pr.node === 'dir' && pr.dir === 'bare_os') {
return ['version']
}
if (pr.kind === 'run' && pr.node === 'root') {
return ['bare-os']
}
if (pr.kind === 'run' && pr.node === 'dir' && pr.dir === 'bare_os') {
return ['units']
}
if (pr.kind === 'dev' && pr.node === 'root') {
return ['null', 'zero']
}
}
const r = route(abs)
if (r.virtualMntRoot) {
@@ -697,6 +780,12 @@ export function createVfs(
if (abs === '/' && !names.includes('tmp')) {
names.push('tmp')
}
if (abs === '/' && !names.includes('run')) {
names.push('run')
}
if (abs === '/' && !names.includes('dev')) {
names.push('dev')
}
return names.sort()
}
@@ -773,6 +862,14 @@ export function createVfs(
*/
async function writeFileAtAbs(abs, buf, opts = {}) {
const r = route(abs)
if (
r.virtualPseudo &&
r.kind === 'dev' &&
r.node === 'file' &&
(r.file === 'null' || r.file === 'zero')
) {
return
}
if (
r.virtualHomeDir ||
r.virtualMntRoot ||
+70 -1
View File
@@ -450,15 +450,21 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
BARE_OS_CTX_API_VERSION: '9.9.9-test'
}
const vfs = createVfs(sys, personal, env, null, {
procSnapshot: { version: '1.2.3-test', cmdline: 'unit-test' }
procSnapshot: { version: '1.2.3-test', cmdline: 'unit-test' },
bootStartedMs: Date.now() - 4000,
initdRunText: () => 'demo-unit\tactive\t1\tdemo\n'
})
const root = await vfs.readdir('/')
t.ok(root.includes('proc'))
t.ok(root.includes('sys'))
t.ok(root.includes('tmp'))
t.ok(root.includes('run'))
t.ok(root.includes('dev'))
t.alike(await vfs.readdir('/proc').then((a) => [...a].sort()), [
'bare_os_version',
'meminfo',
'self',
'uptime',
'version'
])
t.alike(await vfs.readdir('/proc/self').then((a) => [...a].sort()), [
@@ -474,6 +480,20 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
t.ok(sysVer.includes('1.2.3-test'))
const cmd = b4a.toString(await vfs.readFile('/proc/self/cmdline'))
t.ok(cmd.includes('unit-test'))
const mem = b4a.toString(await vfs.readFile('/proc/meminfo'))
t.ok(mem.includes('MemTotal:'))
const up = b4a.toString(await vfs.readFile('/proc/uptime'))
t.ok(/^\d+\.\d+\s+\d+\.\d+/.test(up.trim()))
t.alike(await vfs.readdir('/dev').then((a) => [...a].sort()), [
'null',
'zero'
])
await vfs.writeFile('/dev/null', b4a.from('gone'))
t.is((await vfs.readFile('/dev/null'))?.byteLength ?? 0, 0)
const z = await vfs.readFile('/dev/zero')
t.ok(z && z.byteLength === 65536)
const units = b4a.toString(await vfs.readFile('/run/bare-os/units'))
t.ok(units.includes('demo-unit'))
let writeErr = null
try {
await vfs.writeFile('/proc/version', b4a.from('x'))
@@ -784,6 +804,51 @@ test('loadBarerc createSkeletonIfMissing writes ~/.barerc when absent', async (t
rmSync(dir, { recursive: true, force: true })
})
test('execShellLine pipeline stage and byte limits', async (t) => {
const dir = testCorestoreDir('shpipe')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('spipe'))
await drive.ready()
await personal.ready()
const echo = `
async function run(ctx, argv) {
ctx.console.log(argv.slice(1).join(' '))
}
`
const cat = `
async function run(ctx) {
ctx.console.log(bareStdin(ctx).replace(/\\n$/, ''))
}
`
await drive.put('/bin/echo', b4a.from(echo))
await drive.put('/bin/cat', b4a.from(cat))
const spam = `
async function run(ctx) {
ctx.console.log('z'.repeat(200))
}
`
await drive.put('/bin/spam', b4a.from(spam))
const logs = []
const ctx = testCtx(drive, personal)
ctx.console = {
log: (...a) => logs.push(a.join(' ')),
error: (...a) => logs.push(a.join(' '))
}
ctx.env.BARE_OS_PIPELINE_MAX_STAGES = '2'
await execShellLine(ctx, 'echo a | echo b | echo c')
t.is(ctx.exitCode, 1)
t.ok(logs.some((l) => l.includes('BARE_OS_PIPELINE_MAX_STAGES')))
logs.length = 0
ctx.env.BARE_OS_PIPELINE_MAX_STAGES = '32'
ctx.env.BARE_OS_PIPELINE_MAX_BYTES = '80'
await execShellLine(ctx, 'spam | cat')
t.is(ctx.exitCode, 1)
t.ok(logs.some((l) => l.includes('BARE_OS_PIPELINE_MAX_BYTES')))
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('execShellLine runs cd and external', async (t) => {
const dir = testCorestoreDir('sh')
const store = new Corestore(dir)
@@ -1872,6 +1937,10 @@ test('runBinCommand man ls prints manual text', async (t) => {
t.ok(text.includes('SYNOPSIS'))
t.ok(text.includes('EXAMPLES'))
t.ok(text.includes('ls'))
logs.length = 0
await runBinCommand(ctx, ['man', '-w'])
t.is(ctx.exitCode, 0)
t.is(logs.join('\n').trim(), '/share/man/man.json')
await store.close()
rmSync(dir, { recursive: true, force: true })
})