updates
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user