Another pass again
This commit is contained in:
@@ -161,6 +161,10 @@ import {
|
||||
bareInitdResumeAfterBareMobile
|
||||
} from './lib/bare-initd.js'
|
||||
import { BARE_OS_LIFECYCLE_SCHEMA_VERSION } from './lib/bare-os-lifecycle-schema.js'
|
||||
import { createBooterBootEmitter } from './lib/bare-os-boot-phases.js'
|
||||
import { createBooterProcHelpers } from './lib/bare-os-booter-proc-helpers.js'
|
||||
import { createBareOsVirtualSignalDeliverer } from './lib/bare-os-virtual-signal.js'
|
||||
import { createKernelLoaderAuditAppend } from './lib/bare-os-loader-audit.js'
|
||||
import { createBareOsQvacBridge } from './lib/bare-os-qvac-host.mjs'
|
||||
import { bareOsQvacEnsureModelsHdms } from './lib/bare-os-qvac-models-store.mjs'
|
||||
import { buildPearIpcRegistryJson } from './lib/bare-os-pear-ipc-registry.js'
|
||||
@@ -181,9 +185,7 @@ import {
|
||||
import { buildBareOsSyscallsProcJson } from './lib/bare-os-syscalls-proc-json.js'
|
||||
import {
|
||||
bareOsIsPosixSignalName,
|
||||
bareOsNormalizeSignalName,
|
||||
BARE_OS_SIGNAL_EXIT,
|
||||
BARE_OS_SESSION_EXIT_SIGNALS
|
||||
bareOsNormalizeSignalName
|
||||
} from './lib/bare-os-posix-signals.js'
|
||||
import {
|
||||
bareOsWasmKernelCompile as compileBareOsWasmKernelModule,
|
||||
@@ -191,10 +193,6 @@ import {
|
||||
} from './lib/bare-os-wasm-kernel.js'
|
||||
import { BARE_OS_CTX_API_VERSION } from './lib/bare-os-ctx-api.js'
|
||||
import { buildBareOsPearCorestoreHrpcProcJson } from './lib/bare-os-proc-pear-corestore-hrpc.js'
|
||||
import {
|
||||
bareOsProcBlindPeerRelayHintsExposed,
|
||||
buildBareOsBlindRelaySwarmRuntime
|
||||
} from './lib/bare-os-proc-blind-peer-relay-gate.js'
|
||||
import { buildBareOsRuntimeCaps } from './lib/bare-os-runtime-caps.js'
|
||||
import {
|
||||
bareOsBareModulesEnabled,
|
||||
@@ -739,31 +737,15 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
/** Live logical→actual map; updated by {@link protomuxAliasRegistry}. */
|
||||
const bareOsProtomuxAliasMap = protomuxAliasRegistry.legacyMapView()
|
||||
|
||||
function emitBooterBootPhase(phase) {
|
||||
return emitBooterBootStep(phase)
|
||||
}
|
||||
function emitBooterBootStep(step) {
|
||||
const p = String(step)
|
||||
const arr = bootReadyStateRef.booterPhases
|
||||
const arrSt = bootReadyStateRef.booterStages
|
||||
if (!arr.includes(p)) arr.push(p)
|
||||
if (!arrSt.includes(p)) arrSt.push(p)
|
||||
const ev = {
|
||||
type: 'boot',
|
||||
phase: 'booter:' + p,
|
||||
ms: Date.now() - bootStartedMs,
|
||||
ts: Date.now(),
|
||||
sessionId: bareOsSessionId,
|
||||
lifecycleSchemaVersion: BARE_OS_LIFECYCLE_SCHEMA_VERSION,
|
||||
...(bootHrtimeNowNs ? { monotonicNs: String(bootHrtimeNowNs()) } : {})
|
||||
}
|
||||
for (const fn of bootEventSubs) {
|
||||
Promise.resolve(fn(ev)).catch(() => {})
|
||||
}
|
||||
for (const fn of diagnosticsSubs) {
|
||||
Promise.resolve(fn({ ...ev, source: 'booter' })).catch(() => {})
|
||||
}
|
||||
}
|
||||
const { emitBooterBootPhase, emitBooterBootStep } = createBooterBootEmitter({
|
||||
bootReadyStateRef,
|
||||
bootStartedMs,
|
||||
bootHrtimeNowNs,
|
||||
bootEventSubs,
|
||||
diagnosticsSubs,
|
||||
sessionId: bareOsSessionId,
|
||||
lifecycleSchemaVersion: BARE_OS_LIFECYCLE_SCHEMA_VERSION
|
||||
})
|
||||
/** @type {{ stats: unknown }} */
|
||||
const hostProcStatsRef = { stats: null }
|
||||
/** Filled after `ctx` is constructed; used for `/proc` readers that need live shell job rows. */
|
||||
@@ -771,39 +753,18 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
/** @type {Record<string, unknown> | null} */
|
||||
ctx: null
|
||||
}
|
||||
function bareOsCollectLogicalFdRows() {
|
||||
const c = bareOsInteractiveCtxRef.ctx
|
||||
const o = c?.bareOsLogicalFds
|
||||
if (!o || typeof o !== 'object') return []
|
||||
return Object.keys(o)
|
||||
.filter((k) => /^[0-9]+$/.test(k) && k !== '0' && k !== '1' && k !== '2')
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
.map((fdNum) => ({
|
||||
fd: Number(fdNum),
|
||||
target: String(/** @type {Record<string, string>} */ (o)[fdNum] ?? '')
|
||||
}))
|
||||
}
|
||||
|
||||
function bareOsLiveShellJobsAndIpcStats() {
|
||||
const live = bareOsInteractiveCtxRef.ctx
|
||||
const shellJobs =
|
||||
live &&
|
||||
live.shellBackgroundJobs &&
|
||||
typeof live.shellBackgroundJobs === 'object' &&
|
||||
Array.isArray(live.shellBackgroundJobs.list)
|
||||
? live.shellBackgroundJobs.list
|
||||
: []
|
||||
let ipcStats
|
||||
try {
|
||||
ipcStats =
|
||||
live && live.bareOsIpc && typeof live.bareOsIpc.stats === 'function'
|
||||
? live.bareOsIpc.stats()
|
||||
: undefined
|
||||
} catch {
|
||||
ipcStats = undefined
|
||||
}
|
||||
return { shellJobs, ipcStats }
|
||||
}
|
||||
const {
|
||||
bareOsCollectLogicalFdRows,
|
||||
bareOsLiveShellJobsAndIpcStats,
|
||||
bareOsBlindRelayRuntimeHintsForProc,
|
||||
bareOsChatProcSnapshotRecord,
|
||||
bareOsMeshdropProcSnapshotRecord
|
||||
} = createBooterProcHelpers({
|
||||
interactiveCtxRef: bareOsInteractiveCtxRef,
|
||||
disk,
|
||||
shellEnv,
|
||||
hostEnv
|
||||
})
|
||||
/** @type {{ current: Record<string, unknown> | null }} */
|
||||
const warmReadCacheStatsRef = { current: null }
|
||||
/** Prior Hypercore lengths for optional replication-driven warm-cache prefix eviction. */
|
||||
@@ -817,46 +778,6 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
const bootBudgetSummaryProcRef = {
|
||||
text: '{"schema":1,"note":"boot_budget_summary_pending"}\n'
|
||||
}
|
||||
function bareOsBlindRelayRuntimeHintsForProc() {
|
||||
if (!bareOsProcBlindPeerRelayHintsExposed(shellEnv)) return undefined
|
||||
const peers = disk.peers ? [...disk.peers] : []
|
||||
return {
|
||||
swarm: buildBareOsBlindRelaySwarmRuntime(shellEnv, peers, {
|
||||
hasSystemDrive: !!disk.drive,
|
||||
hasPersonalDrive: !!disk.personalDrive,
|
||||
auxiliaryDriveCount: Array.isArray(disk.auxiliaryDrives)
|
||||
? disk.auxiliaryDrives.length
|
||||
: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Same object as `/proc/bare_os/chat.json` (single source for VFS + ctx hook).
|
||||
* @returns {Record<string, unknown>}
|
||||
*/
|
||||
function bareOsChatProcSnapshotRecord() {
|
||||
const svc = disk.bareOsChatService
|
||||
if (!svc) {
|
||||
return {
|
||||
schema: 1,
|
||||
note: 'Swarm chat is off when BARE_OS_PROTOMUX_CHAT_CHANNEL=0/false/off (stock default is on).'
|
||||
}
|
||||
}
|
||||
return {
|
||||
schema: 1,
|
||||
atMs: Date.now(),
|
||||
protocol: svc.PROTOCOL_CHAT_CHANNEL_NAME,
|
||||
metrics: svc.snapshotMetrics(),
|
||||
protomuxChatRxTotal:
|
||||
typeof disk.protomuxChatChannelRxTotal === 'number'
|
||||
? disk.protomuxChatChannelRxTotal
|
||||
: 0,
|
||||
swarmPeers:
|
||||
disk.peers && typeof disk.peers.size === 'number' ? disk.peers.size : 0,
|
||||
muxEnabled:
|
||||
bareOsChatMuxEnabled(shellEnv) || bareOsChatMuxEnabled(hostEnv || {})
|
||||
}
|
||||
}
|
||||
const vfs = createVfs(disk.drive, disk.personalDrive, shellEnv, vfsMountRef, {
|
||||
procBareOsSwarmSubsystemStatusJsonText(fileKey) {
|
||||
return bareOsFormatSwarmSubsystemProcJson(fileKey, {
|
||||
@@ -897,31 +818,7 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
return `${JSON.stringify(bareOsChatProcSnapshotRecord())}\n`
|
||||
},
|
||||
procBareOsMeshdropText() {
|
||||
const svc = disk.bareOsMeshdropService
|
||||
/** @type {Record<string, unknown>} */
|
||||
const o = svc
|
||||
? {
|
||||
schema: 1,
|
||||
atMs: Date.now(),
|
||||
protocol: svc.PROTOCOL_MESHDROP_CHANNEL_NAME,
|
||||
metrics: svc.snapshotMetrics(),
|
||||
protomuxMeshdropRxTotal:
|
||||
typeof disk.protomuxMeshdropChannelRxTotal === 'number'
|
||||
? disk.protomuxMeshdropChannelRxTotal
|
||||
: 0,
|
||||
swarmPeers:
|
||||
disk.peers && typeof disk.peers.size === 'number'
|
||||
? disk.peers.size
|
||||
: 0,
|
||||
muxEnabled:
|
||||
bareOsMeshdropMuxEnabled(shellEnv) ||
|
||||
bareOsMeshdropMuxEnabled(hostEnv || {})
|
||||
}
|
||||
: {
|
||||
schema: 1,
|
||||
note: 'Meshdrop channel is off when BARE_OS_PROTOMUX_MESHDROP_CHANNEL=0/false/off (stock default is on).'
|
||||
}
|
||||
return `${JSON.stringify(o)}\n`
|
||||
return `${JSON.stringify(bareOsMeshdropProcSnapshotRecord())}\n`
|
||||
},
|
||||
procBareOsDhtScanText() {
|
||||
const peers = disk.peers?.size ?? 0
|
||||
@@ -3498,45 +3395,11 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
return redactAuditShellLineFromEnv(line, shellEnv)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader audit NDJSON under /run/bare-os/loader-audit.ndjson (cap-gated via BARE_OS_LOADER_AUDIT).
|
||||
* @param {Record<string, unknown>} rec
|
||||
*/
|
||||
async function appendKernelLoaderAuditLine(rec) {
|
||||
if (
|
||||
shellEnv.BARE_OS_LOADER_AUDIT !== '1' &&
|
||||
shellEnv.BARE_OS_LOADER_AUDIT !== 'true'
|
||||
)
|
||||
return
|
||||
if (
|
||||
!vfs ||
|
||||
typeof vfs.readFile !== 'function' ||
|
||||
typeof vfs.writeFile !== 'function'
|
||||
)
|
||||
return
|
||||
const line =
|
||||
JSON.stringify({
|
||||
type: 'loader_audit',
|
||||
ts: Date.now(),
|
||||
sessionId: bareOsSessionId,
|
||||
...rec
|
||||
}) + '\n'
|
||||
try {
|
||||
let prev = ''
|
||||
try {
|
||||
const buf = await vfs.readFile('/run/bare-os/loader-audit.ndjson')
|
||||
prev = b4a.toString(buf)
|
||||
} catch {
|
||||
/* new */
|
||||
}
|
||||
await vfs.writeFile(
|
||||
'/run/bare-os/loader-audit.ndjson',
|
||||
b4a.from(prev + line)
|
||||
)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
const appendKernelLoaderAuditLine = createKernelLoaderAuditAppend({
|
||||
env: shellEnv,
|
||||
vfs,
|
||||
sessionId: bareOsSessionId
|
||||
})
|
||||
|
||||
let bareOsAdvertisedPrimaryResolved =
|
||||
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PRIMARY >>> 0
|
||||
@@ -3555,50 +3418,10 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
let sessionExitCode = 0
|
||||
let forceSessionEnd = false
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {number} pid
|
||||
* @param {string} sig normalized signal short name (e.g. TERM)
|
||||
*/
|
||||
function deliverBareOsVirtualSignalToPid(ctx, pid, sig) {
|
||||
const extendedSynthetic = Number.isFinite(pid) && pid >= 4100 && pid < 9000
|
||||
if (sig === '0') {
|
||||
return { ok: true, delivered: false, exists: true }
|
||||
}
|
||||
if (
|
||||
ctx.bareOsLogicalSigaction &&
|
||||
ctx.bareOsLogicalSigaction[sig] === 'IGNORE'
|
||||
) {
|
||||
return {
|
||||
ok: true,
|
||||
delivered: false,
|
||||
ignored: true,
|
||||
atMs: Date.now()
|
||||
}
|
||||
}
|
||||
const atMs = Date.now()
|
||||
if (
|
||||
pid === 3 &&
|
||||
(sig === 'INT' || sig === 'TERM' || sig === 'USR1' || sig === 'USR2')
|
||||
) {
|
||||
Promise.resolve(dispatchShellTrapSignal(ctx, sig)).catch(() => {})
|
||||
}
|
||||
bareOsVirtualSignalState.set(pid, { signal: sig, atMs })
|
||||
if (typeof globalThis.process?.emit === 'function') {
|
||||
try {
|
||||
globalThis.process.emit('bare-os:signal', { pid, signal: sig, atMs })
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
const noSessionExit =
|
||||
extendedSynthetic || !BARE_OS_SESSION_EXIT_SIGNALS.has(sig)
|
||||
if (!noSessionExit) {
|
||||
const signum = BARE_OS_SIGNAL_EXIT[sig] || 15
|
||||
ctx.requestBooterExit(128 + signum)
|
||||
}
|
||||
return { ok: true, delivered: true, atMs }
|
||||
}
|
||||
const deliverBareOsVirtualSignalToPid = createBareOsVirtualSignalDeliverer({
|
||||
dispatchShellTrapSignal,
|
||||
virtualSignalState: bareOsVirtualSignalState
|
||||
})
|
||||
|
||||
const stockBareOsHrpcRequest = createStockBareOsHrpcRequest({
|
||||
env: shellEnv,
|
||||
|
||||
@@ -17,3 +17,55 @@ export const BARE_OS_BOOTER_BOOT_STEPS = Object.freeze([
|
||||
export function isKnownBareOsBooterBootStep(step) {
|
||||
return BARE_OS_BOOTER_BOOT_STEPS.includes(String(step))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* bootReadyStateRef: { booterPhases: string[], booterStages: string[] },
|
||||
* bootStartedMs: number,
|
||||
* bootHrtimeNowNs?: (() => bigint) | null,
|
||||
* bootEventSubs: Iterable<(ev: Record<string, unknown>) => unknown>,
|
||||
* diagnosticsSubs: Iterable<(ev: Record<string, unknown>) => unknown>,
|
||||
* sessionId: string,
|
||||
* lifecycleSchemaVersion: string | number
|
||||
* }} deps
|
||||
*/
|
||||
export function createBooterBootEmitter(deps) {
|
||||
const {
|
||||
bootReadyStateRef,
|
||||
bootStartedMs,
|
||||
bootHrtimeNowNs = null,
|
||||
bootEventSubs,
|
||||
diagnosticsSubs,
|
||||
sessionId,
|
||||
lifecycleSchemaVersion
|
||||
} = deps
|
||||
|
||||
function emitBooterBootStep(step) {
|
||||
const p = String(step)
|
||||
const arr = bootReadyStateRef.booterPhases
|
||||
const arrSt = bootReadyStateRef.booterStages
|
||||
if (!arr.includes(p)) arr.push(p)
|
||||
if (!arrSt.includes(p)) arrSt.push(p)
|
||||
const ev = {
|
||||
type: 'boot',
|
||||
phase: 'booter:' + p,
|
||||
ms: Date.now() - bootStartedMs,
|
||||
ts: Date.now(),
|
||||
sessionId,
|
||||
lifecycleSchemaVersion,
|
||||
...(bootHrtimeNowNs ? { monotonicNs: String(bootHrtimeNowNs()) } : {})
|
||||
}
|
||||
for (const fn of bootEventSubs) {
|
||||
Promise.resolve(fn(ev)).catch(() => {})
|
||||
}
|
||||
for (const fn of diagnosticsSubs) {
|
||||
Promise.resolve(fn({ ...ev, source: 'booter' })).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
function emitBooterBootPhase(phase) {
|
||||
return emitBooterBootStep(phase)
|
||||
}
|
||||
|
||||
return { emitBooterBootStep, emitBooterBootPhase }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Booter /proc snapshot helpers used while constructing executeKernel ctx.
|
||||
*/
|
||||
import {
|
||||
bareOsProcBlindPeerRelayHintsExposed,
|
||||
buildBareOsBlindRelaySwarmRuntime
|
||||
} from './bare-os-proc-blind-peer-relay-gate.js'
|
||||
import { bareOsChatMuxEnabled } from './bare-os-chat-service.js'
|
||||
import { bareOsMeshdropMuxEnabled } from './bare-os-meshdrop-service.js'
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* interactiveCtxRef: { ctx: Record<string, unknown> | null },
|
||||
* disk: Record<string, unknown>,
|
||||
* shellEnv: Record<string, string | undefined>,
|
||||
* hostEnv?: Record<string, string | undefined> | null
|
||||
* }} deps
|
||||
*/
|
||||
export function createBooterProcHelpers(deps) {
|
||||
const { interactiveCtxRef, disk, shellEnv, hostEnv = null } = deps
|
||||
|
||||
function bareOsCollectLogicalFdRows() {
|
||||
const c = interactiveCtxRef.ctx
|
||||
const o = c?.bareOsLogicalFds
|
||||
if (!o || typeof o !== 'object') return []
|
||||
return Object.keys(o)
|
||||
.filter((k) => /^[0-9]+$/.test(k) && k !== '0' && k !== '1' && k !== '2')
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
.map((fdNum) => ({
|
||||
fd: Number(fdNum),
|
||||
target: String(/** @type {Record<string, string>} */ (o)[fdNum] ?? '')
|
||||
}))
|
||||
}
|
||||
|
||||
function bareOsLiveShellJobsAndIpcStats() {
|
||||
const live = interactiveCtxRef.ctx
|
||||
const shellJobs =
|
||||
live &&
|
||||
live.shellBackgroundJobs &&
|
||||
typeof live.shellBackgroundJobs === 'object' &&
|
||||
Array.isArray(live.shellBackgroundJobs.list)
|
||||
? live.shellBackgroundJobs.list
|
||||
: []
|
||||
let ipcStats
|
||||
try {
|
||||
ipcStats =
|
||||
live && live.bareOsIpc && typeof live.bareOsIpc.stats === 'function'
|
||||
? live.bareOsIpc.stats()
|
||||
: undefined
|
||||
} catch {
|
||||
ipcStats = undefined
|
||||
}
|
||||
return { shellJobs, ipcStats }
|
||||
}
|
||||
|
||||
function bareOsBlindRelayRuntimeHintsForProc() {
|
||||
if (!bareOsProcBlindPeerRelayHintsExposed(shellEnv)) return undefined
|
||||
const peers = disk.peers ? [...disk.peers] : []
|
||||
return {
|
||||
swarm: buildBareOsBlindRelaySwarmRuntime(shellEnv, peers, {
|
||||
hasSystemDrive: !!disk.drive,
|
||||
hasPersonalDrive: !!disk.personalDrive,
|
||||
auxiliaryDriveCount: Array.isArray(disk.auxiliaryDrives)
|
||||
? disk.auxiliaryDrives.length
|
||||
: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same object as `/proc/bare_os/chat.json` (single source for VFS + ctx hook).
|
||||
* @returns {Record<string, unknown>}
|
||||
*/
|
||||
function bareOsChatProcSnapshotRecord() {
|
||||
const svc = disk.bareOsChatService
|
||||
if (!svc) {
|
||||
return {
|
||||
schema: 1,
|
||||
note: 'Swarm chat is off when BARE_OS_PROTOMUX_CHAT_CHANNEL=0/false/off (stock default is on).'
|
||||
}
|
||||
}
|
||||
return {
|
||||
schema: 1,
|
||||
atMs: Date.now(),
|
||||
protocol: svc.PROTOCOL_CHAT_CHANNEL_NAME,
|
||||
metrics: svc.snapshotMetrics(),
|
||||
protomuxChatRxTotal:
|
||||
typeof disk.protomuxChatChannelRxTotal === 'number'
|
||||
? disk.protomuxChatChannelRxTotal
|
||||
: 0,
|
||||
swarmPeers:
|
||||
disk.peers && typeof disk.peers.size === 'number' ? disk.peers.size : 0,
|
||||
muxEnabled:
|
||||
bareOsChatMuxEnabled(shellEnv) || bareOsChatMuxEnabled(hostEnv || {})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same object as `/proc/bare_os/meshdrop.json`.
|
||||
* @returns {Record<string, unknown>}
|
||||
*/
|
||||
function bareOsMeshdropProcSnapshotRecord() {
|
||||
const svc = disk.bareOsMeshdropService
|
||||
if (!svc) {
|
||||
return {
|
||||
schema: 1,
|
||||
note: 'Meshdrop channel is off when BARE_OS_PROTOMUX_MESHDROP_CHANNEL=0/false/off (stock default is on).'
|
||||
}
|
||||
}
|
||||
return {
|
||||
schema: 1,
|
||||
atMs: Date.now(),
|
||||
protocol: svc.PROTOCOL_MESHDROP_CHANNEL_NAME,
|
||||
metrics: svc.snapshotMetrics(),
|
||||
protomuxMeshdropRxTotal:
|
||||
typeof disk.protomuxMeshdropChannelRxTotal === 'number'
|
||||
? disk.protomuxMeshdropChannelRxTotal
|
||||
: 0,
|
||||
swarmPeers:
|
||||
disk.peers && typeof disk.peers.size === 'number' ? disk.peers.size : 0,
|
||||
muxEnabled:
|
||||
bareOsMeshdropMuxEnabled(shellEnv) ||
|
||||
bareOsMeshdropMuxEnabled(hostEnv || {})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
bareOsCollectLogicalFdRows,
|
||||
bareOsLiveShellJobsAndIpcStats,
|
||||
bareOsBlindRelayRuntimeHintsForProc,
|
||||
bareOsChatProcSnapshotRecord,
|
||||
bareOsMeshdropProcSnapshotRecord
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Loader audit NDJSON under /run/bare-os/loader-audit.ndjson.
|
||||
*/
|
||||
import b4a from 'b4a'
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* env: Record<string, string | undefined>,
|
||||
* vfs: { readFile?: Function, writeFile?: Function } | null | undefined,
|
||||
* sessionId: string
|
||||
* }} deps
|
||||
*/
|
||||
export function createKernelLoaderAuditAppend(deps) {
|
||||
const { env, vfs, sessionId } = deps
|
||||
|
||||
/**
|
||||
* Cap-gated via BARE_OS_LOADER_AUDIT.
|
||||
* @param {Record<string, unknown>} rec
|
||||
*/
|
||||
return async function appendKernelLoaderAuditLine(rec) {
|
||||
if (
|
||||
env.BARE_OS_LOADER_AUDIT !== '1' &&
|
||||
env.BARE_OS_LOADER_AUDIT !== 'true'
|
||||
)
|
||||
return
|
||||
if (
|
||||
!vfs ||
|
||||
typeof vfs.readFile !== 'function' ||
|
||||
typeof vfs.writeFile !== 'function'
|
||||
)
|
||||
return
|
||||
const line =
|
||||
JSON.stringify({
|
||||
type: 'loader_audit',
|
||||
ts: Date.now(),
|
||||
sessionId,
|
||||
...rec
|
||||
}) + '\n'
|
||||
try {
|
||||
let prev = ''
|
||||
try {
|
||||
const buf = await vfs.readFile('/run/bare-os/loader-audit.ndjson')
|
||||
prev = b4a.toString(buf)
|
||||
} catch {
|
||||
/* new */
|
||||
}
|
||||
await vfs.writeFile(
|
||||
'/run/bare-os/loader-audit.ndjson',
|
||||
b4a.from(prev + line)
|
||||
)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Logical signal delivery for synthetic PIDs (guest has no host signals).
|
||||
*/
|
||||
import {
|
||||
BARE_OS_SIGNAL_EXIT,
|
||||
BARE_OS_SESSION_EXIT_SIGNALS
|
||||
} from './bare-os-posix-signals.js'
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* dispatchShellTrapSignal: (ctx: Record<string, unknown>, sig: string) => unknown,
|
||||
* virtualSignalState: Map<number, { signal: string, atMs: number }>
|
||||
* }} deps
|
||||
*/
|
||||
export function createBareOsVirtualSignalDeliverer(deps) {
|
||||
const { dispatchShellTrapSignal, virtualSignalState } = deps
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {number} pid
|
||||
* @param {string} sig normalized signal short name (e.g. TERM)
|
||||
*/
|
||||
return function deliverBareOsVirtualSignalToPid(ctx, pid, sig) {
|
||||
const extendedSynthetic = Number.isFinite(pid) && pid >= 4100 && pid < 9000
|
||||
if (sig === '0') {
|
||||
return { ok: true, delivered: false, exists: true }
|
||||
}
|
||||
if (
|
||||
ctx.bareOsLogicalSigaction &&
|
||||
ctx.bareOsLogicalSigaction[sig] === 'IGNORE'
|
||||
) {
|
||||
return {
|
||||
ok: true,
|
||||
delivered: false,
|
||||
ignored: true,
|
||||
atMs: Date.now()
|
||||
}
|
||||
}
|
||||
const atMs = Date.now()
|
||||
if (
|
||||
pid === 3 &&
|
||||
(sig === 'INT' || sig === 'TERM' || sig === 'USR1' || sig === 'USR2')
|
||||
) {
|
||||
Promise.resolve(dispatchShellTrapSignal(ctx, sig)).catch(() => {})
|
||||
}
|
||||
virtualSignalState.set(pid, { signal: sig, atMs })
|
||||
if (typeof globalThis.process?.emit === 'function') {
|
||||
try {
|
||||
globalThis.process.emit('bare-os:signal', { pid, signal: sig, atMs })
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
const noSessionExit =
|
||||
extendedSynthetic || !BARE_OS_SESSION_EXIT_SIGNALS.has(sig)
|
||||
if (!noSessionExit) {
|
||||
const signum = BARE_OS_SIGNAL_EXIT[sig] || 15
|
||||
ctx.requestBooterExit(128 + signum)
|
||||
}
|
||||
return { ok: true, delivered: true, atMs }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Shell builtin name table and optional POSIX `read` builtin.
|
||||
*/
|
||||
|
||||
export const SHELL_BUILTINS = new Set([
|
||||
'alias',
|
||||
'unalias',
|
||||
'barerc',
|
||||
'cd',
|
||||
'export',
|
||||
'unset',
|
||||
'readonly',
|
||||
'umask',
|
||||
'set',
|
||||
':',
|
||||
'command',
|
||||
'type',
|
||||
'logout',
|
||||
'exit',
|
||||
'jobs',
|
||||
'fg',
|
||||
'bg',
|
||||
'wait',
|
||||
'suspend-job',
|
||||
'disown',
|
||||
'trap',
|
||||
'test',
|
||||
'['
|
||||
])
|
||||
|
||||
/**
|
||||
* Optional POSIX-style **`read`** builtin (bounded line, IFS split). Off by default.
|
||||
* @param {Record<string, string | undefined> | null | undefined} env
|
||||
*/
|
||||
export function bareOsShellReadBuiltinEnabled(env) {
|
||||
const o = env && typeof env === 'object' ? env : {}
|
||||
return o.BARE_OS_SHELL_READ_BUILTIN === '1' || o.BARE_OS_SHELL_READ_BUILTIN === 'true'
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorted list of shell builtin command names for completion / UX.
|
||||
* Includes **`read`** only when {@link bareOsShellReadBuiltinEnabled} is true.
|
||||
* @param {Record<string, string | undefined> | null | undefined} env
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function listBareOsShellBuiltins(env) {
|
||||
const out = [...SHELL_BUILTINS]
|
||||
if (bareOsShellReadBuiltinEnabled(env)) out.push('read')
|
||||
out.sort()
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} cmd
|
||||
* @param {Record<string, string | undefined> | null | undefined} env
|
||||
*/
|
||||
export function isShellBuiltin(cmd, env) {
|
||||
if (SHELL_BUILTINS.has(cmd)) return true
|
||||
if (cmd === 'read' && bareOsShellReadBuiltinEnabled(env)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
* @param {string} ifs
|
||||
* @param {number} nNames
|
||||
*/
|
||||
export function bareOsShellReadSplitFields(line, ifs, nNames) {
|
||||
const sep = ifs.length ? ifs[0] : ' '
|
||||
if (nNames <= 1) return [line]
|
||||
const out = []
|
||||
let rest = line
|
||||
for (let i = 0; i < nNames - 1; i++) {
|
||||
const idx = rest.indexOf(sep)
|
||||
if (idx === -1) {
|
||||
out.push(rest)
|
||||
rest = ''
|
||||
break
|
||||
}
|
||||
out.push(rest.slice(0, idx))
|
||||
rest = rest.slice(idx + sep.length)
|
||||
}
|
||||
while (out.length < nNames - 1) out.push('')
|
||||
out.push(rest)
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {number} maxBytes
|
||||
* @param {(m: string) => void} errFn
|
||||
* @returns {Promise<string | null>} null = EOF / error
|
||||
*/
|
||||
export async function bareOsShellReadOneLine(ctx, maxBytes, errFn, opts = {}) {
|
||||
const delimiter = typeof opts.delimiter === 'string' ? opts.delimiter : '\n'
|
||||
const timeoutMs = Number.isFinite(opts.timeoutMs) ? Number(opts.timeoutMs) : 0
|
||||
if (typeof ctx.shellStdin === 'string') {
|
||||
const raw = ctx.shellStdin
|
||||
const idx = delimiter ? raw.indexOf(delimiter) : -1
|
||||
const line = idx === -1 ? raw : raw.slice(0, idx)
|
||||
ctx.shellStdin = idx === -1 ? '' : raw.slice(idx + delimiter.length)
|
||||
if (line.length > maxBytes) {
|
||||
errFn(`read: line exceeds BARE_OS_SHELL_READ_MAX_BYTES (${maxBytes})`)
|
||||
return null
|
||||
}
|
||||
return line
|
||||
}
|
||||
const rl = ctx.readLine
|
||||
if (typeof rl === 'function') {
|
||||
const readP = rl('')
|
||||
const ln =
|
||||
timeoutMs > 0
|
||||
? await Promise.race([
|
||||
readP,
|
||||
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
|
||||
])
|
||||
: await readP
|
||||
if (ln == null) return null
|
||||
if (ln.length > maxBytes) {
|
||||
errFn(`read: line exceeds BARE_OS_SHELL_READ_MAX_BYTES (${maxBytes})`)
|
||||
return null
|
||||
}
|
||||
return ln
|
||||
}
|
||||
errFn(
|
||||
'read: no input (redirect stdin, use a pipeline, or interactive readLine)'
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string[]} argv
|
||||
* @param {Record<string, string>} env
|
||||
* @param {(m: string) => void} origErr
|
||||
*/
|
||||
export async function runShellReadBuiltin(ctx, argv, env, origErr) {
|
||||
let i = 1
|
||||
let rawMode = false
|
||||
let delimiter = '\n'
|
||||
let timeoutMs = 0
|
||||
while (i < argv.length && argv[i].startsWith('-')) {
|
||||
const a = argv[i]
|
||||
if (a === '-r') {
|
||||
rawMode = true
|
||||
}
|
||||
else if (a === '-d') {
|
||||
const d = argv[i + 1]
|
||||
if (d == null) {
|
||||
origErr.call(ctx.console, 'read: option requires an argument -- d')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
delimiter = String(d).slice(0, 1)
|
||||
i++
|
||||
}
|
||||
else if (a === '-t') {
|
||||
const v = argv[i + 1]
|
||||
if (v == null) {
|
||||
origErr.call(ctx.console, 'read: option requires an argument -- t')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
const n = Number.parseFloat(String(v))
|
||||
if (!Number.isFinite(n) || n < 0) {
|
||||
origErr.call(ctx.console, 'read: invalid timeout: ' + String(v))
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
timeoutMs = Math.min(Math.floor(n * 1000), 120000)
|
||||
i++
|
||||
}
|
||||
else if (a === '--') {
|
||||
i++
|
||||
break
|
||||
} else {
|
||||
origErr.call(ctx.console, 'read: unsupported option: ' + a)
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
i++
|
||||
}
|
||||
const names = argv.slice(i).filter(Boolean)
|
||||
if (!names.length) names.push('REPLY')
|
||||
const maxRaw = env.BARE_OS_SHELL_READ_MAX_BYTES
|
||||
const maxParsed =
|
||||
maxRaw != null && String(maxRaw).trim() !== ''
|
||||
? Number.parseInt(String(maxRaw), 10)
|
||||
: 65536
|
||||
const maxBytes =
|
||||
Number.isFinite(maxParsed) && maxParsed > 0
|
||||
? Math.min(maxParsed, 2_000_000)
|
||||
: 65536
|
||||
let line = await bareOsShellReadOneLine(
|
||||
ctx,
|
||||
maxBytes,
|
||||
(m) => origErr.call(ctx.console, m),
|
||||
{ delimiter, timeoutMs }
|
||||
)
|
||||
if (line === null) {
|
||||
for (const n of names) env[n] = ''
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (!rawMode) {
|
||||
line = line.replace(/\\(.)/g, '$1')
|
||||
}
|
||||
const ifs =
|
||||
env.IFS !== undefined && env.IFS !== null ? String(env.IFS) : ' \t\n'
|
||||
const fields = bareOsShellReadSplitFields(line, ifs, names.length)
|
||||
for (let j = 0; j < names.length; j++) {
|
||||
const k = names[j]
|
||||
if (
|
||||
ctx.shellReadonlyVars instanceof Set &&
|
||||
ctx.shellReadonlyVars.has(k)
|
||||
) {
|
||||
origErr.call(ctx.console, k + ': readonly variable')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
env[k] = fields[j] ?? ''
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* Shell parameter / arithmetic word expansion (`$VAR`, `${…}`, `$((…))`).
|
||||
*/
|
||||
import { findArithmeticClose } from './shell-lex.js'
|
||||
import { shellCheckUnboundParam } from './shell-nounset.js'
|
||||
import { bareOsEvalArithmeticExpr } from './shell-arithmetic.js'
|
||||
import { BARE_OS_EXIT_STATUS_ENV } from './shell-runtime.js'
|
||||
|
||||
/**
|
||||
* @param {string} inner
|
||||
* @param {Record<string, string>} env
|
||||
*/
|
||||
function expandParamBracedInner(inner, env, depth = 0) {
|
||||
const expandNested = (s) => {
|
||||
const txt = String(s ?? '')
|
||||
if (!txt.includes('$')) return txt
|
||||
return expandWord(txt, env, depth + 1)
|
||||
}
|
||||
const tr = inner.trim()
|
||||
const lenParam = /^#([A-Za-z_][A-Za-z0-9_]*)$/.exec(tr)
|
||||
if (lenParam) {
|
||||
shellCheckUnboundParam(lenParam[1], env)
|
||||
return String(String(env[lenParam[1]] ?? '').length)
|
||||
}
|
||||
const indirectOn =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_INDIRECT_EXPANSION === '1' ||
|
||||
env.BARE_OS_SHELL_INDIRECT_EXPANSION === 'true')
|
||||
const indirectName = /^!([A-Za-z_][A-Za-z0-9_]*)$/.exec(tr)
|
||||
if (indirectOn && indirectName) {
|
||||
const ref = String(env[indirectName[1]] ?? '')
|
||||
shellCheckUnboundParam(ref, env)
|
||||
return String(env[ref] ?? '')
|
||||
}
|
||||
const paramV2 =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === '1' ||
|
||||
env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === 'true')
|
||||
const paramV3 =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === '1' ||
|
||||
env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === 'true')
|
||||
|
||||
const errIdx = inner.indexOf(':?')
|
||||
if (paramV3 && errIdx > 0) {
|
||||
const name = inner.slice(0, errIdx).trim()
|
||||
const msg = inner.slice(errIdx + 2)
|
||||
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
||||
const v = env[name]
|
||||
if (v == null || String(v) === '') {
|
||||
throw new Error(expandNested(msg) || 'parameter null or unset')
|
||||
}
|
||||
return String(v)
|
||||
}
|
||||
}
|
||||
|
||||
const assignIdx = inner.indexOf(':=')
|
||||
if (paramV2 && assignIdx > 0) {
|
||||
const name = inner.slice(0, assignIdx).trim()
|
||||
const alt = inner.slice(assignIdx + 2)
|
||||
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
||||
let v = env[name]
|
||||
if (v == null || String(v) === '') {
|
||||
const ex = expandNested(alt)
|
||||
env[name] = ex
|
||||
v = ex
|
||||
}
|
||||
return String(v ?? '')
|
||||
}
|
||||
}
|
||||
|
||||
const posixUnsetOnly =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_POSIX_UNSET_ONLY_DEFAULT === '1' ||
|
||||
env.BARE_OS_SHELL_POSIX_UNSET_ONLY_DEFAULT === 'true')
|
||||
if (posixUnsetOnly) {
|
||||
const hy = inner.indexOf('-')
|
||||
if (
|
||||
hy > 0 &&
|
||||
inner.slice(hy - 1, hy + 1) !== ':-' &&
|
||||
!inner.includes(':')
|
||||
) {
|
||||
const m = /^([A-Za-z_][A-Za-z0-9_]*)-(.+)$/.exec(inner)
|
||||
if (m && m[1] && m[2] != null) {
|
||||
const name = m[1]
|
||||
const alt = m[2]
|
||||
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(env, name))
|
||||
return expandNested(alt)
|
||||
return String(env[name] ?? '')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const idx = inner.indexOf(':-')
|
||||
if (idx > 0) {
|
||||
const name = inner.slice(0, idx).trim()
|
||||
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
||||
const alt = inner.slice(idx + 2)
|
||||
const v = env[name]
|
||||
if (v != null && String(v) !== '') return String(v)
|
||||
return expandNested(alt)
|
||||
}
|
||||
}
|
||||
|
||||
const plusIdx = inner.indexOf(':+')
|
||||
if (paramV3 && plusIdx > 0) {
|
||||
const name = inner.slice(0, plusIdx).trim()
|
||||
const alt = inner.slice(plusIdx + 2)
|
||||
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
||||
const v = env[name]
|
||||
if (v != null && String(v) !== '') return expandNested(alt)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
if (paramV3) {
|
||||
const sliceRe = /^([A-Za-z_][A-Za-z0-9_]*):(\d+)(?::(\d+))?$/.exec(tr)
|
||||
if (sliceRe) {
|
||||
const name = sliceRe[1]
|
||||
const off = Number.parseInt(sliceRe[2], 10)
|
||||
const ln =
|
||||
sliceRe[3] != null ? Number.parseInt(sliceRe[3], 10) : undefined
|
||||
const v = String(env[name] ?? '')
|
||||
let out = Number.isFinite(off) ? v.slice(off) : v
|
||||
if (ln != null && Number.isFinite(ln)) out = out.slice(0, ln)
|
||||
return out
|
||||
}
|
||||
const globalRepl = /^([A-Za-z_][A-Za-z0-9_]*)\/\/(.*)\/(.*)$/.exec(tr)
|
||||
if (globalRepl && globalRepl[2].length <= 256 && globalRepl[3].length <= 512) {
|
||||
const name = globalRepl[1]
|
||||
let v = String(env[name] ?? '')
|
||||
const pat = globalRepl[2]
|
||||
const rep = expandNested(globalRepl[3])
|
||||
try {
|
||||
const re = new RegExp(pat, 'g')
|
||||
v = v.replace(re, rep)
|
||||
} catch {
|
||||
/* invalid regex — leave value */
|
||||
}
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
if (paramV2) {
|
||||
const longPref = /^([A-Za-z_][A-Za-z0-9_]*)##(.+)$/.exec(inner)
|
||||
if (longPref && longPref[2].length > 0 && longPref[2].length <= 128) {
|
||||
const v = String(env[longPref[1]] ?? '')
|
||||
const pat = longPref[2]
|
||||
if (pat === '*/') {
|
||||
const i = v.lastIndexOf('/')
|
||||
return i >= 0 ? v.slice(i + 1) : v
|
||||
}
|
||||
let end = -1
|
||||
for (let i = 0; i <= v.length - pat.length; i++) {
|
||||
if (v.slice(i, i + pat.length) === pat) end = i + pat.length
|
||||
}
|
||||
return end >= 0 ? v.slice(end) : v
|
||||
}
|
||||
const shortPref = /^([A-Za-z_][A-Za-z0-9_]*)#(.+)$/.exec(inner)
|
||||
if (shortPref && shortPref[2].length > 0 && shortPref[2].length <= 128) {
|
||||
const v = String(env[shortPref[1]] ?? '')
|
||||
const pat = shortPref[2]
|
||||
if (pat === '*/') {
|
||||
const i = v.indexOf('/')
|
||||
return i >= 0 ? v.slice(i + 1) : v
|
||||
}
|
||||
const i = v.indexOf(pat)
|
||||
return i >= 0 ? v.slice(i + pat.length) : v
|
||||
}
|
||||
const longSuf = /^([A-Za-z_][A-Za-z0-9_]*)%%(.+)$/.exec(inner)
|
||||
if (longSuf && longSuf[2].length > 0 && longSuf[2].length <= 128) {
|
||||
const v = String(env[longSuf[1]] ?? '')
|
||||
const pat = longSuf[2]
|
||||
if (!/[?*[]/.test(pat) && v.endsWith(pat))
|
||||
return v.slice(0, v.length - pat.length)
|
||||
if (pat.includes('*') && !pat.includes('[') && !pat.includes('?')) {
|
||||
const parts = pat.split('*')
|
||||
if (parts.length === 2) {
|
||||
const a = parts[0]
|
||||
const b = parts[1]
|
||||
let best = -1
|
||||
for (let len = 1; len <= v.length; len++) {
|
||||
const suf = v.slice(v.length - len)
|
||||
if (
|
||||
suf.startsWith(a) &&
|
||||
suf.endsWith(b) &&
|
||||
suf.length >= a.length + b.length
|
||||
) {
|
||||
if (best < 0 || len > best) best = len
|
||||
}
|
||||
}
|
||||
if (best > 0) return v.slice(0, v.length - best)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
const shortSuf = /^([A-Za-z_][A-Za-z0-9_]*)%(.+)$/.exec(inner)
|
||||
if (shortSuf && shortSuf[2].length > 0 && shortSuf[2].length <= 128) {
|
||||
const v = String(env[shortSuf[1]] ?? '')
|
||||
const pat = shortSuf[2]
|
||||
if (!/[?*[]/.test(pat) && v.endsWith(pat))
|
||||
return v.slice(0, v.length - pat.length)
|
||||
if (pat.includes('*') && !pat.includes('[') && !pat.includes('?')) {
|
||||
const parts = pat.split('*')
|
||||
if (parts.length === 2) {
|
||||
const a = parts[0]
|
||||
const b = parts[1]
|
||||
let best = -1
|
||||
for (let len = 1; len <= v.length; len++) {
|
||||
const suf = v.slice(v.length - len)
|
||||
if (
|
||||
suf.startsWith(a) &&
|
||||
suf.endsWith(b) &&
|
||||
suf.length >= a.length + b.length
|
||||
) {
|
||||
if (best < 0 || len < best) best = len
|
||||
}
|
||||
}
|
||||
if (best > 0) return v.slice(0, v.length - best)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
const hash = inner.indexOf('#')
|
||||
if (hash > 0) {
|
||||
const name = inner.slice(0, hash).trim()
|
||||
const pref = inner.slice(hash + 1)
|
||||
if (
|
||||
/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) &&
|
||||
pref.length > 0 &&
|
||||
pref.length <= 128
|
||||
) {
|
||||
const v = String(env[name] ?? '')
|
||||
return v.startsWith(pref) ? v.slice(pref.length) : v
|
||||
}
|
||||
}
|
||||
const keySimple = inner.trim()
|
||||
shellCheckUnboundParam(keySimple, env)
|
||||
return env[keySimple] ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} s
|
||||
* @param {Record<string, string>} env
|
||||
*/
|
||||
export function expandWord(s, env, depth = 0) {
|
||||
const maxDepthRaw = Number.parseInt(
|
||||
String(env?.BARE_OS_SHELL_EXPANSION_MAX_DEPTH || '32'),
|
||||
10
|
||||
)
|
||||
const maxDepth =
|
||||
Number.isFinite(maxDepthRaw) && maxDepthRaw > 0 ? Math.min(maxDepthRaw, 256) : 32
|
||||
if (depth > maxDepth) {
|
||||
throw new Error(`shell: expansion recursion too deep (max ${maxDepth})`)
|
||||
}
|
||||
const paramExpOn =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_PARAM_EXPANSION === '1' ||
|
||||
env.BARE_OS_SHELL_PARAM_EXPANSION === 'true')
|
||||
const paramV2 =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === '1' ||
|
||||
env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === 'true')
|
||||
const paramV3 =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === '1' ||
|
||||
env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === 'true')
|
||||
let out = ''
|
||||
let j = 0
|
||||
while (j < s.length) {
|
||||
if (s[j] === '$') {
|
||||
if (s[j + 1] === '(' && s[j + 2] === '(') {
|
||||
const close = findArithmeticClose(s, j + 3)
|
||||
if (close < 0) {
|
||||
out += s.slice(j)
|
||||
break
|
||||
}
|
||||
const inner = s.slice(j + 3, close)
|
||||
try {
|
||||
out += bareOsEvalArithmeticExpr(inner, env)
|
||||
} catch (e) {
|
||||
if (
|
||||
env?.BARE_OS_SHELL_POSIX_MODE === '1' ||
|
||||
env?.BARE_OS_SHELL_POSIX_MODE === 'true'
|
||||
) {
|
||||
throw new Error(
|
||||
'shell: arithmetic: invalid token (POSIX mode strict arithmetic)'
|
||||
)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
j = close + 2
|
||||
continue
|
||||
}
|
||||
if (s[j + 1] === '{') {
|
||||
const end = s.indexOf('}', j + 2)
|
||||
if (end === -1) {
|
||||
out += s.slice(j)
|
||||
break
|
||||
}
|
||||
const inner = s.slice(j + 2, end)
|
||||
const tr0 = inner.trim()
|
||||
const indirectOnBr =
|
||||
env?.BARE_OS_SHELL_INDIRECT_EXPANSION === '1' ||
|
||||
env?.BARE_OS_SHELL_INDIRECT_EXPANSION === 'true'
|
||||
if (inner === '?') {
|
||||
out += env[BARE_OS_EXIT_STATUS_ENV] ?? '0'
|
||||
} else if (
|
||||
/^#[A-Za-z_][A-Za-z0-9_]*$/.test(tr0) ||
|
||||
(indirectOnBr && /^![A-Za-z_][A-Za-z0-9_]*$/.test(tr0)) ||
|
||||
(paramExpOn &&
|
||||
(inner.includes(':-') ||
|
||||
(paramV3 && (inner.includes(':+') || inner.includes(':?'))) ||
|
||||
(paramV3 &&
|
||||
(/^[A-Za-z_][A-Za-z0-9_]*:\d/.test(tr0) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*\/\//.test(inner))) ||
|
||||
(paramV2 &&
|
||||
(inner.includes(':=') ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*##/.test(inner) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*%%/.test(inner) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*%[^%]/.test(inner) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*#[^#]/.test(inner))) ||
|
||||
(/^[A-Za-z_][A-Za-z0-9_]*#/.test(inner) && inner.includes('#'))))
|
||||
) {
|
||||
out += expandParamBracedInner(inner, env, depth + 1)
|
||||
} else {
|
||||
const ik = inner.trim()
|
||||
shellCheckUnboundParam(ik, env)
|
||||
out += env[ik] ?? ''
|
||||
}
|
||||
j = end + 1
|
||||
continue
|
||||
}
|
||||
if (s[j + 1] === '?') {
|
||||
out += env[BARE_OS_EXIT_STATUS_ENV] ?? '0'
|
||||
j += 2
|
||||
continue
|
||||
}
|
||||
if (/[0-9]/.test(s[j + 1] ?? '')) {
|
||||
const pn = s[j + 1]
|
||||
shellCheckUnboundParam(pn, env)
|
||||
out += env[pn] ?? ''
|
||||
j += 2
|
||||
continue
|
||||
}
|
||||
let k = j + 1
|
||||
while (k < s.length && /[A-Za-z0-9_]/.test(s[k])) k++
|
||||
const name = s.slice(j + 1, k)
|
||||
if (name) {
|
||||
shellCheckUnboundParam(name, env)
|
||||
out += env[name] ?? ''
|
||||
j = k
|
||||
} else {
|
||||
out += '$'
|
||||
j++
|
||||
}
|
||||
continue
|
||||
}
|
||||
out += s[j++]
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Shell runtime helpers: exit-status env, pipeline caps, child merge, audit.
|
||||
*/
|
||||
|
||||
/** Env key mirroring last command exit status (POSIX `$?` parity). */
|
||||
export const BARE_OS_EXIT_STATUS_ENV = 'BARE_OS_EXIT_STATUS'
|
||||
|
||||
/**
|
||||
* Mirror `ctx.exitCode` into `ctx.vfs.env` so kernels and `echo $?` see last status.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
export function syncBareOsExitStatusEnv(ctx) {
|
||||
const env = ctx.vfs?.env
|
||||
if (!env || typeof env !== 'object') return
|
||||
const n = Number(ctx.exitCode)
|
||||
env[BARE_OS_EXIT_STATUS_ENV] = String(Number.isFinite(n) ? n : 0)
|
||||
}
|
||||
|
||||
/** 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
|
||||
|
||||
/**
|
||||
* Resolved simulated pipeline limits for the current `vfs.env` / session env.
|
||||
* @param {Record<string, string> | null | undefined} env
|
||||
*/
|
||||
export function getBareOsPipelineLimits(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
|
||||
}
|
||||
/** Upper bounds on simulated capture (after streaming multiplier); tunable for high-RAM hosts. */
|
||||
const absMaxBytes = parse(
|
||||
'BARE_OS_PIPELINE_ABS_MAX_BYTES',
|
||||
512 * 1024 * 1024
|
||||
)
|
||||
const absMaxLines = parse('BARE_OS_PIPELINE_ABS_MAX_LINES', 2_000_000)
|
||||
const streamOn =
|
||||
o.BARE_OS_SHELL_STREAMING === '1' || o.BARE_OS_SHELL_STREAMING === 'true'
|
||||
const multRaw = Number.parseFloat(
|
||||
String(o.BARE_OS_SHELL_STREAMING_MULT || '4')
|
||||
)
|
||||
const mult =
|
||||
streamOn && Number.isFinite(multRaw) && multRaw > 1
|
||||
? Math.min(multRaw, 16)
|
||||
: 1
|
||||
const baseBytes = parse(
|
||||
'BARE_OS_PIPELINE_MAX_BYTES',
|
||||
DEFAULT_PIPELINE_MAX_CAPTURE_BYTES
|
||||
)
|
||||
const baseLines = parse(
|
||||
'BARE_OS_PIPELINE_MAX_LINES',
|
||||
DEFAULT_PIPELINE_MAX_CAPTURE_LINES
|
||||
)
|
||||
const effectiveBytes = Math.floor(baseBytes * mult)
|
||||
const effectiveLines = Math.floor(baseLines * mult)
|
||||
return {
|
||||
maxStages: parse(
|
||||
'BARE_OS_PIPELINE_MAX_STAGES',
|
||||
DEFAULT_PIPELINE_MAX_STAGES
|
||||
),
|
||||
maxBytes: Math.min(effectiveBytes, absMaxBytes),
|
||||
maxLines: Math.min(effectiveLines, absMaxLines),
|
||||
streamingMultiplier: mult,
|
||||
/** True when `BARE_OS_SHELL_STREAMING` relaxes caps via multiplier. */
|
||||
streamingEnabled: streamOn,
|
||||
/** Parsed `BARE_OS_PIPELINE_MAX_*` before multiplier (for operator snapshots). */
|
||||
baseMaxBytes: baseBytes,
|
||||
baseMaxLines: baseLines,
|
||||
/** Hard ceilings after multiplier (`BARE_OS_PIPELINE_ABS_MAX_*`; defaults 512 MiB / 2 M lines). */
|
||||
absCapBytes: absMaxBytes,
|
||||
absCapLines: absMaxLines
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When `suspend-job` sets `stopped` on a background entry, yield between statements
|
||||
* until `fg` / `bg` clears it (cooperative logical job control; no host SIGTSTP).
|
||||
* @param {{ stopped?: boolean }} entry
|
||||
*/
|
||||
export function waitWhileShellJobStopped(entry) {
|
||||
if (!entry || !entry.stopped) return Promise.resolve()
|
||||
return new Promise((resolve) => {
|
||||
const id = setInterval(() => {
|
||||
if (!entry.stopped) {
|
||||
clearInterval(id)
|
||||
resolve(undefined)
|
||||
}
|
||||
}, 10)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} event
|
||||
* @param {Record<string, unknown>} payload
|
||||
*/
|
||||
export function appendShellAuditEvent(ctx, event, payload = {}) {
|
||||
if (!Array.isArray(ctx.shellAuditEvents)) ctx.shellAuditEvents = []
|
||||
ctx.shellAuditEvents.push({
|
||||
schema: 1,
|
||||
ts: Date.now(),
|
||||
event,
|
||||
...payload
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, unknown>} childCtx
|
||||
*/
|
||||
export function mergeChildExitCode(ctx, childCtx) {
|
||||
if (childCtx.exitCode !== undefined && childCtx.exitCode !== null) {
|
||||
ctx.exitCode = childCtx.exitCode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} identity
|
||||
* @returns {identity is { state: unknown, publicKey: unknown, secretKey: unknown }}
|
||||
*/
|
||||
export function isMergeableIdentitySession(identity) {
|
||||
if (!identity || typeof identity !== 'object') return false
|
||||
return (
|
||||
Object.prototype.hasOwnProperty.call(identity, 'state') &&
|
||||
Object.prototype.hasOwnProperty.call(identity, 'publicKey') &&
|
||||
Object.prototype.hasOwnProperty.call(identity, 'secretKey')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge shell child-command side effects we intentionally allow to flow back.
|
||||
* Today this includes exit status and identity session state.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, unknown>} childCtx
|
||||
*/
|
||||
export function mergePipelineChildCtx(ctx, childCtx) {
|
||||
mergeChildExitCode(ctx, childCtx)
|
||||
if (isMergeableIdentitySession(childCtx.identity)) {
|
||||
ctx.identity = /** @type {Record<string, unknown>} */ (childCtx.identity)
|
||||
}
|
||||
}
|
||||
@@ -38,376 +38,37 @@ import {
|
||||
parseShellFunctionDeclaration,
|
||||
casePatternMatches
|
||||
} from './shell-syntax.js'
|
||||
import {
|
||||
isShellBuiltin,
|
||||
runShellReadBuiltin
|
||||
} from './shell-builtins.js'
|
||||
import {
|
||||
syncBareOsExitStatusEnv,
|
||||
getBareOsPipelineLimits,
|
||||
waitWhileShellJobStopped,
|
||||
appendShellAuditEvent,
|
||||
mergePipelineChildCtx
|
||||
} from './shell-runtime.js'
|
||||
import { expandWord } from './shell-expand.js'
|
||||
|
||||
export { BARE_OS_SHELL_NOUNSET_ERROR }
|
||||
|
||||
const SHELL_BUILTINS = new Set([
|
||||
'alias',
|
||||
'unalias',
|
||||
'barerc',
|
||||
'cd',
|
||||
'export',
|
||||
'unset',
|
||||
'readonly',
|
||||
'umask',
|
||||
'set',
|
||||
':',
|
||||
'command',
|
||||
'type',
|
||||
'logout',
|
||||
'exit',
|
||||
'jobs',
|
||||
'fg',
|
||||
'bg',
|
||||
'wait',
|
||||
'suspend-job',
|
||||
'disown',
|
||||
'trap',
|
||||
'test',
|
||||
'['
|
||||
])
|
||||
|
||||
/**
|
||||
* Optional POSIX-style **`read`** builtin (bounded line, IFS split). Off by default.
|
||||
* @param {Record<string, string | undefined> | null | undefined} env
|
||||
*/
|
||||
export function bareOsShellReadBuiltinEnabled(env) {
|
||||
const o = env && typeof env === 'object' ? env : {}
|
||||
return o.BARE_OS_SHELL_READ_BUILTIN === '1' || o.BARE_OS_SHELL_READ_BUILTIN === 'true'
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorted list of shell builtin command names for completion / UX.
|
||||
* Includes **`read`** only when {@link bareOsShellReadBuiltinEnabled} is true.
|
||||
* @param {Record<string, string | undefined> | null | undefined} env
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function listBareOsShellBuiltins(env) {
|
||||
const out = [...SHELL_BUILTINS]
|
||||
if (bareOsShellReadBuiltinEnabled(env)) out.push('read')
|
||||
out.sort()
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} cmd
|
||||
* @param {Record<string, string | undefined> | null | undefined} env
|
||||
*/
|
||||
function isShellBuiltin(cmd, env) {
|
||||
if (SHELL_BUILTINS.has(cmd)) return true
|
||||
if (cmd === 'read' && bareOsShellReadBuiltinEnabled(env)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
* @param {string} ifs
|
||||
* @param {number} nNames
|
||||
*/
|
||||
function bareOsShellReadSplitFields(line, ifs, nNames) {
|
||||
const sep = ifs.length ? ifs[0] : ' '
|
||||
if (nNames <= 1) return [line]
|
||||
const out = []
|
||||
let rest = line
|
||||
for (let i = 0; i < nNames - 1; i++) {
|
||||
const idx = rest.indexOf(sep)
|
||||
if (idx === -1) {
|
||||
out.push(rest)
|
||||
rest = ''
|
||||
break
|
||||
}
|
||||
out.push(rest.slice(0, idx))
|
||||
rest = rest.slice(idx + sep.length)
|
||||
}
|
||||
while (out.length < nNames - 1) out.push('')
|
||||
out.push(rest)
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {number} maxBytes
|
||||
* @param {(m: string) => void} errFn
|
||||
* @returns {Promise<string | null>} null = EOF / error
|
||||
*/
|
||||
async function bareOsShellReadOneLine(ctx, maxBytes, errFn, opts = {}) {
|
||||
const delimiter = typeof opts.delimiter === 'string' ? opts.delimiter : '\n'
|
||||
const timeoutMs = Number.isFinite(opts.timeoutMs) ? Number(opts.timeoutMs) : 0
|
||||
if (typeof ctx.shellStdin === 'string') {
|
||||
const raw = ctx.shellStdin
|
||||
const idx = delimiter ? raw.indexOf(delimiter) : -1
|
||||
const line = idx === -1 ? raw : raw.slice(0, idx)
|
||||
ctx.shellStdin = idx === -1 ? '' : raw.slice(idx + delimiter.length)
|
||||
if (line.length > maxBytes) {
|
||||
errFn(`read: line exceeds BARE_OS_SHELL_READ_MAX_BYTES (${maxBytes})`)
|
||||
return null
|
||||
}
|
||||
return line
|
||||
}
|
||||
const rl = ctx.readLine
|
||||
if (typeof rl === 'function') {
|
||||
const readP = rl('')
|
||||
const ln =
|
||||
timeoutMs > 0
|
||||
? await Promise.race([
|
||||
readP,
|
||||
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
|
||||
])
|
||||
: await readP
|
||||
if (ln == null) return null
|
||||
if (ln.length > maxBytes) {
|
||||
errFn(`read: line exceeds BARE_OS_SHELL_READ_MAX_BYTES (${maxBytes})`)
|
||||
return null
|
||||
}
|
||||
return ln
|
||||
}
|
||||
errFn(
|
||||
'read: no input (redirect stdin, use a pipeline, or interactive readLine)'
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string[]} argv
|
||||
* @param {Record<string, string>} env
|
||||
* @param {(m: string) => void} origErr
|
||||
*/
|
||||
async function runShellReadBuiltin(ctx, argv, env, origErr) {
|
||||
let i = 1
|
||||
let rawMode = false
|
||||
let delimiter = '\n'
|
||||
let timeoutMs = 0
|
||||
while (i < argv.length && argv[i].startsWith('-')) {
|
||||
const a = argv[i]
|
||||
if (a === '-r') {
|
||||
rawMode = true
|
||||
}
|
||||
else if (a === '-d') {
|
||||
const d = argv[i + 1]
|
||||
if (d == null) {
|
||||
origErr.call(ctx.console, 'read: option requires an argument -- d')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
delimiter = String(d).slice(0, 1)
|
||||
i++
|
||||
}
|
||||
else if (a === '-t') {
|
||||
const v = argv[i + 1]
|
||||
if (v == null) {
|
||||
origErr.call(ctx.console, 'read: option requires an argument -- t')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
const n = Number.parseFloat(String(v))
|
||||
if (!Number.isFinite(n) || n < 0) {
|
||||
origErr.call(ctx.console, 'read: invalid timeout: ' + String(v))
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
timeoutMs = Math.min(Math.floor(n * 1000), 120000)
|
||||
i++
|
||||
}
|
||||
else if (a === '--') {
|
||||
i++
|
||||
break
|
||||
} else {
|
||||
origErr.call(ctx.console, 'read: unsupported option: ' + a)
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
i++
|
||||
}
|
||||
const names = argv.slice(i).filter(Boolean)
|
||||
if (!names.length) names.push('REPLY')
|
||||
const maxRaw = env.BARE_OS_SHELL_READ_MAX_BYTES
|
||||
const maxParsed =
|
||||
maxRaw != null && String(maxRaw).trim() !== ''
|
||||
? Number.parseInt(String(maxRaw), 10)
|
||||
: 65536
|
||||
const maxBytes =
|
||||
Number.isFinite(maxParsed) && maxParsed > 0
|
||||
? Math.min(maxParsed, 2_000_000)
|
||||
: 65536
|
||||
let line = await bareOsShellReadOneLine(
|
||||
ctx,
|
||||
maxBytes,
|
||||
(m) => origErr.call(ctx.console, m),
|
||||
{ delimiter, timeoutMs }
|
||||
)
|
||||
if (line === null) {
|
||||
for (const n of names) env[n] = ''
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (!rawMode) {
|
||||
line = line.replace(/\\(.)/g, '$1')
|
||||
}
|
||||
const ifs =
|
||||
env.IFS !== undefined && env.IFS !== null ? String(env.IFS) : ' \t\n'
|
||||
const fields = bareOsShellReadSplitFields(line, ifs, names.length)
|
||||
for (let j = 0; j < names.length; j++) {
|
||||
const k = names[j]
|
||||
if (
|
||||
ctx.shellReadonlyVars instanceof Set &&
|
||||
ctx.shellReadonlyVars.has(k)
|
||||
) {
|
||||
origErr.call(ctx.console, k + ': readonly variable')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
env[k] = fields[j] ?? ''
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* When `suspend-job` sets `stopped` on a background entry, yield between statements
|
||||
* until `fg` / `bg` clears it (cooperative logical job control; no host SIGTSTP).
|
||||
* @param {{ stopped?: boolean }} entry
|
||||
*/
|
||||
function waitWhileShellJobStopped(entry) {
|
||||
if (!entry || !entry.stopped) return Promise.resolve()
|
||||
return new Promise((resolve) => {
|
||||
const id = setInterval(() => {
|
||||
if (!entry.stopped) {
|
||||
clearInterval(id)
|
||||
resolve(undefined)
|
||||
}
|
||||
}, 10)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} event
|
||||
* @param {Record<string, unknown>} payload
|
||||
*/
|
||||
function appendShellAuditEvent(ctx, event, payload = {}) {
|
||||
if (!Array.isArray(ctx.shellAuditEvents)) ctx.shellAuditEvents = []
|
||||
ctx.shellAuditEvents.push({
|
||||
schema: 1,
|
||||
ts: Date.now(),
|
||||
event,
|
||||
...payload
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, unknown>} childCtx
|
||||
*/
|
||||
function mergeChildExitCode(ctx, childCtx) {
|
||||
if (childCtx.exitCode !== undefined && childCtx.exitCode !== null) {
|
||||
ctx.exitCode = childCtx.exitCode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} identity
|
||||
* @returns {identity is { state: unknown, publicKey: unknown, secretKey: unknown }}
|
||||
*/
|
||||
function isMergeableIdentitySession(identity) {
|
||||
if (!identity || typeof identity !== 'object') return false
|
||||
return (
|
||||
Object.prototype.hasOwnProperty.call(identity, 'state') &&
|
||||
Object.prototype.hasOwnProperty.call(identity, 'publicKey') &&
|
||||
Object.prototype.hasOwnProperty.call(identity, 'secretKey')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge shell child-command side effects we intentionally allow to flow back.
|
||||
* Today this includes exit status and identity session state.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, unknown>} childCtx
|
||||
*/
|
||||
function mergePipelineChildCtx(ctx, childCtx) {
|
||||
mergeChildExitCode(ctx, childCtx)
|
||||
if (isMergeableIdentitySession(childCtx.identity)) {
|
||||
ctx.identity = /** @type {Record<string, unknown>} */ (childCtx.identity)
|
||||
}
|
||||
}
|
||||
|
||||
/** Env key mirroring last command exit status (POSIX `$?` parity). */
|
||||
export const BARE_OS_EXIT_STATUS_ENV = 'BARE_OS_EXIT_STATUS'
|
||||
|
||||
/**
|
||||
* Mirror `ctx.exitCode` into `ctx.vfs.env` so kernels and `echo $?` see last status.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
export function syncBareOsExitStatusEnv(ctx) {
|
||||
const env = ctx.vfs?.env
|
||||
if (!env || typeof env !== 'object') return
|
||||
const n = Number(ctx.exitCode)
|
||||
env[BARE_OS_EXIT_STATUS_ENV] = String(Number.isFinite(n) ? n : 0)
|
||||
}
|
||||
export {
|
||||
bareOsShellReadBuiltinEnabled,
|
||||
listBareOsShellBuiltins
|
||||
} from './shell-builtins.js'
|
||||
export {
|
||||
BARE_OS_EXIT_STATUS_ENV,
|
||||
syncBareOsExitStatusEnv,
|
||||
DEFAULT_PIPELINE_MAX_STAGES,
|
||||
DEFAULT_PIPELINE_MAX_CAPTURE_BYTES,
|
||||
DEFAULT_PIPELINE_MAX_CAPTURE_LINES,
|
||||
getBareOsPipelineLimits
|
||||
} from './shell-runtime.js'
|
||||
export { expandWord } from './shell-expand.js'
|
||||
|
||||
/** 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
|
||||
|
||||
/**
|
||||
* Resolved simulated pipeline limits for the current `vfs.env` / session env.
|
||||
* @param {Record<string, string> | null | undefined} env
|
||||
*/
|
||||
export function getBareOsPipelineLimits(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
|
||||
}
|
||||
/** Upper bounds on simulated capture (after streaming multiplier); tunable for high-RAM hosts. */
|
||||
const absMaxBytes = parse(
|
||||
'BARE_OS_PIPELINE_ABS_MAX_BYTES',
|
||||
512 * 1024 * 1024
|
||||
)
|
||||
const absMaxLines = parse('BARE_OS_PIPELINE_ABS_MAX_LINES', 2_000_000)
|
||||
const streamOn =
|
||||
o.BARE_OS_SHELL_STREAMING === '1' || o.BARE_OS_SHELL_STREAMING === 'true'
|
||||
const multRaw = Number.parseFloat(
|
||||
String(o.BARE_OS_SHELL_STREAMING_MULT || '4')
|
||||
)
|
||||
const mult =
|
||||
streamOn && Number.isFinite(multRaw) && multRaw > 1
|
||||
? Math.min(multRaw, 16)
|
||||
: 1
|
||||
const baseBytes = parse(
|
||||
'BARE_OS_PIPELINE_MAX_BYTES',
|
||||
DEFAULT_PIPELINE_MAX_CAPTURE_BYTES
|
||||
)
|
||||
const baseLines = parse(
|
||||
'BARE_OS_PIPELINE_MAX_LINES',
|
||||
DEFAULT_PIPELINE_MAX_CAPTURE_LINES
|
||||
)
|
||||
const effectiveBytes = Math.floor(baseBytes * mult)
|
||||
const effectiveLines = Math.floor(baseLines * mult)
|
||||
return {
|
||||
maxStages: parse(
|
||||
'BARE_OS_PIPELINE_MAX_STAGES',
|
||||
DEFAULT_PIPELINE_MAX_STAGES
|
||||
),
|
||||
maxBytes: Math.min(effectiveBytes, absMaxBytes),
|
||||
maxLines: Math.min(effectiveLines, absMaxLines),
|
||||
streamingMultiplier: mult,
|
||||
/** True when `BARE_OS_SHELL_STREAMING` relaxes caps via multiplier. */
|
||||
streamingEnabled: streamOn,
|
||||
/** Parsed `BARE_OS_PIPELINE_MAX_*` before multiplier (for operator snapshots). */
|
||||
baseMaxBytes: baseBytes,
|
||||
baseMaxLines: baseLines,
|
||||
/** Hard ceilings after multiplier (`BARE_OS_PIPELINE_ABS_MAX_*`; defaults 512 MiB / 2 M lines). */
|
||||
absCapBytes: absMaxBytes,
|
||||
absCapLines: absMaxLines
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Baseline aliases; `~/.barerc` and `unalias -a` merge/reset from this table.
|
||||
* @returns {Record<string, string>}
|
||||
@@ -622,365 +283,6 @@ export function tokenize(line) {
|
||||
return /** @type {Token[]} */ (lexShellLine(line))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} inner
|
||||
* @param {Record<string, string>} env
|
||||
*/
|
||||
function expandParamBracedInner(inner, env, depth = 0) {
|
||||
const expandNested = (s) => {
|
||||
const txt = String(s ?? '')
|
||||
if (!txt.includes('$')) return txt
|
||||
return expandWord(txt, env, depth + 1)
|
||||
}
|
||||
const tr = inner.trim()
|
||||
const lenParam = /^#([A-Za-z_][A-Za-z0-9_]*)$/.exec(tr)
|
||||
if (lenParam) {
|
||||
shellCheckUnboundParam(lenParam[1], env)
|
||||
return String(String(env[lenParam[1]] ?? '').length)
|
||||
}
|
||||
const indirectOn =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_INDIRECT_EXPANSION === '1' ||
|
||||
env.BARE_OS_SHELL_INDIRECT_EXPANSION === 'true')
|
||||
const indirectName = /^!([A-Za-z_][A-Za-z0-9_]*)$/.exec(tr)
|
||||
if (indirectOn && indirectName) {
|
||||
const ref = String(env[indirectName[1]] ?? '')
|
||||
shellCheckUnboundParam(ref, env)
|
||||
return String(env[ref] ?? '')
|
||||
}
|
||||
const paramV2 =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === '1' ||
|
||||
env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === 'true')
|
||||
const paramV3 =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === '1' ||
|
||||
env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === 'true')
|
||||
|
||||
const errIdx = inner.indexOf(':?')
|
||||
if (paramV3 && errIdx > 0) {
|
||||
const name = inner.slice(0, errIdx).trim()
|
||||
const msg = inner.slice(errIdx + 2)
|
||||
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
||||
const v = env[name]
|
||||
if (v == null || String(v) === '') {
|
||||
throw new Error(expandNested(msg) || 'parameter null or unset')
|
||||
}
|
||||
return String(v)
|
||||
}
|
||||
}
|
||||
|
||||
const assignIdx = inner.indexOf(':=')
|
||||
if (paramV2 && assignIdx > 0) {
|
||||
const name = inner.slice(0, assignIdx).trim()
|
||||
const alt = inner.slice(assignIdx + 2)
|
||||
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
||||
let v = env[name]
|
||||
if (v == null || String(v) === '') {
|
||||
const ex = expandNested(alt)
|
||||
env[name] = ex
|
||||
v = ex
|
||||
}
|
||||
return String(v ?? '')
|
||||
}
|
||||
}
|
||||
|
||||
const posixUnsetOnly =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_POSIX_UNSET_ONLY_DEFAULT === '1' ||
|
||||
env.BARE_OS_SHELL_POSIX_UNSET_ONLY_DEFAULT === 'true')
|
||||
if (posixUnsetOnly) {
|
||||
const hy = inner.indexOf('-')
|
||||
if (
|
||||
hy > 0 &&
|
||||
inner.slice(hy - 1, hy + 1) !== ':-' &&
|
||||
!inner.includes(':')
|
||||
) {
|
||||
const m = /^([A-Za-z_][A-Za-z0-9_]*)-(.+)$/.exec(inner)
|
||||
if (m && m[1] && m[2] != null) {
|
||||
const name = m[1]
|
||||
const alt = m[2]
|
||||
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(env, name))
|
||||
return expandNested(alt)
|
||||
return String(env[name] ?? '')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const idx = inner.indexOf(':-')
|
||||
if (idx > 0) {
|
||||
const name = inner.slice(0, idx).trim()
|
||||
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
||||
const alt = inner.slice(idx + 2)
|
||||
const v = env[name]
|
||||
if (v != null && String(v) !== '') return String(v)
|
||||
return expandNested(alt)
|
||||
}
|
||||
}
|
||||
|
||||
const plusIdx = inner.indexOf(':+')
|
||||
if (paramV3 && plusIdx > 0) {
|
||||
const name = inner.slice(0, plusIdx).trim()
|
||||
const alt = inner.slice(plusIdx + 2)
|
||||
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
||||
const v = env[name]
|
||||
if (v != null && String(v) !== '') return expandNested(alt)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
if (paramV3) {
|
||||
const sliceRe = /^([A-Za-z_][A-Za-z0-9_]*):(\d+)(?::(\d+))?$/.exec(tr)
|
||||
if (sliceRe) {
|
||||
const name = sliceRe[1]
|
||||
const off = Number.parseInt(sliceRe[2], 10)
|
||||
const ln =
|
||||
sliceRe[3] != null ? Number.parseInt(sliceRe[3], 10) : undefined
|
||||
const v = String(env[name] ?? '')
|
||||
let out = Number.isFinite(off) ? v.slice(off) : v
|
||||
if (ln != null && Number.isFinite(ln)) out = out.slice(0, ln)
|
||||
return out
|
||||
}
|
||||
const globalRepl = /^([A-Za-z_][A-Za-z0-9_]*)\/\/(.*)\/(.*)$/.exec(tr)
|
||||
if (globalRepl && globalRepl[2].length <= 256 && globalRepl[3].length <= 512) {
|
||||
const name = globalRepl[1]
|
||||
let v = String(env[name] ?? '')
|
||||
const pat = globalRepl[2]
|
||||
const rep = expandNested(globalRepl[3])
|
||||
try {
|
||||
const re = new RegExp(pat, 'g')
|
||||
v = v.replace(re, rep)
|
||||
} catch {
|
||||
/* invalid regex — leave value */
|
||||
}
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
if (paramV2) {
|
||||
const longPref = /^([A-Za-z_][A-Za-z0-9_]*)##(.+)$/.exec(inner)
|
||||
if (longPref && longPref[2].length > 0 && longPref[2].length <= 128) {
|
||||
const v = String(env[longPref[1]] ?? '')
|
||||
const pat = longPref[2]
|
||||
if (pat === '*/') {
|
||||
const i = v.lastIndexOf('/')
|
||||
return i >= 0 ? v.slice(i + 1) : v
|
||||
}
|
||||
let end = -1
|
||||
for (let i = 0; i <= v.length - pat.length; i++) {
|
||||
if (v.slice(i, i + pat.length) === pat) end = i + pat.length
|
||||
}
|
||||
return end >= 0 ? v.slice(end) : v
|
||||
}
|
||||
const shortPref = /^([A-Za-z_][A-Za-z0-9_]*)#(.+)$/.exec(inner)
|
||||
if (shortPref && shortPref[2].length > 0 && shortPref[2].length <= 128) {
|
||||
const v = String(env[shortPref[1]] ?? '')
|
||||
const pat = shortPref[2]
|
||||
if (pat === '*/') {
|
||||
const i = v.indexOf('/')
|
||||
return i >= 0 ? v.slice(i + 1) : v
|
||||
}
|
||||
const i = v.indexOf(pat)
|
||||
return i >= 0 ? v.slice(i + pat.length) : v
|
||||
}
|
||||
const longSuf = /^([A-Za-z_][A-Za-z0-9_]*)%%(.+)$/.exec(inner)
|
||||
if (longSuf && longSuf[2].length > 0 && longSuf[2].length <= 128) {
|
||||
const v = String(env[longSuf[1]] ?? '')
|
||||
const pat = longSuf[2]
|
||||
if (!/[?*[]/.test(pat) && v.endsWith(pat))
|
||||
return v.slice(0, v.length - pat.length)
|
||||
if (pat.includes('*') && !pat.includes('[') && !pat.includes('?')) {
|
||||
const parts = pat.split('*')
|
||||
if (parts.length === 2) {
|
||||
const a = parts[0]
|
||||
const b = parts[1]
|
||||
let best = -1
|
||||
for (let len = 1; len <= v.length; len++) {
|
||||
const suf = v.slice(v.length - len)
|
||||
if (
|
||||
suf.startsWith(a) &&
|
||||
suf.endsWith(b) &&
|
||||
suf.length >= a.length + b.length
|
||||
) {
|
||||
if (best < 0 || len > best) best = len
|
||||
}
|
||||
}
|
||||
if (best > 0) return v.slice(0, v.length - best)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
const shortSuf = /^([A-Za-z_][A-Za-z0-9_]*)%(.+)$/.exec(inner)
|
||||
if (shortSuf && shortSuf[2].length > 0 && shortSuf[2].length <= 128) {
|
||||
const v = String(env[shortSuf[1]] ?? '')
|
||||
const pat = shortSuf[2]
|
||||
if (!/[?*[]/.test(pat) && v.endsWith(pat))
|
||||
return v.slice(0, v.length - pat.length)
|
||||
if (pat.includes('*') && !pat.includes('[') && !pat.includes('?')) {
|
||||
const parts = pat.split('*')
|
||||
if (parts.length === 2) {
|
||||
const a = parts[0]
|
||||
const b = parts[1]
|
||||
let best = -1
|
||||
for (let len = 1; len <= v.length; len++) {
|
||||
const suf = v.slice(v.length - len)
|
||||
if (
|
||||
suf.startsWith(a) &&
|
||||
suf.endsWith(b) &&
|
||||
suf.length >= a.length + b.length
|
||||
) {
|
||||
if (best < 0 || len < best) best = len
|
||||
}
|
||||
}
|
||||
if (best > 0) return v.slice(0, v.length - best)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
const hash = inner.indexOf('#')
|
||||
if (hash > 0) {
|
||||
const name = inner.slice(0, hash).trim()
|
||||
const pref = inner.slice(hash + 1)
|
||||
if (
|
||||
/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) &&
|
||||
pref.length > 0 &&
|
||||
pref.length <= 128
|
||||
) {
|
||||
const v = String(env[name] ?? '')
|
||||
return v.startsWith(pref) ? v.slice(pref.length) : v
|
||||
}
|
||||
}
|
||||
const keySimple = inner.trim()
|
||||
shellCheckUnboundParam(keySimple, env)
|
||||
return env[keySimple] ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} s
|
||||
* @param {Record<string, string>} env
|
||||
*/
|
||||
export function expandWord(s, env, depth = 0) {
|
||||
const maxDepthRaw = Number.parseInt(
|
||||
String(env?.BARE_OS_SHELL_EXPANSION_MAX_DEPTH || '32'),
|
||||
10
|
||||
)
|
||||
const maxDepth =
|
||||
Number.isFinite(maxDepthRaw) && maxDepthRaw > 0 ? Math.min(maxDepthRaw, 256) : 32
|
||||
if (depth > maxDepth) {
|
||||
throw new Error(`shell: expansion recursion too deep (max ${maxDepth})`)
|
||||
}
|
||||
const paramExpOn =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_PARAM_EXPANSION === '1' ||
|
||||
env.BARE_OS_SHELL_PARAM_EXPANSION === 'true')
|
||||
const paramV2 =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === '1' ||
|
||||
env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === 'true')
|
||||
const paramV3 =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === '1' ||
|
||||
env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === 'true')
|
||||
let out = ''
|
||||
let j = 0
|
||||
while (j < s.length) {
|
||||
if (s[j] === '$') {
|
||||
if (s[j + 1] === '(' && s[j + 2] === '(') {
|
||||
const close = findArithmeticClose(s, j + 3)
|
||||
if (close < 0) {
|
||||
out += s.slice(j)
|
||||
break
|
||||
}
|
||||
const inner = s.slice(j + 3, close)
|
||||
try {
|
||||
out += bareOsEvalArithmeticExpr(inner, env)
|
||||
} catch (e) {
|
||||
if (
|
||||
env?.BARE_OS_SHELL_POSIX_MODE === '1' ||
|
||||
env?.BARE_OS_SHELL_POSIX_MODE === 'true'
|
||||
) {
|
||||
throw new Error(
|
||||
'shell: arithmetic: invalid token (POSIX mode strict arithmetic)'
|
||||
)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
j = close + 2
|
||||
continue
|
||||
}
|
||||
if (s[j + 1] === '{') {
|
||||
const end = s.indexOf('}', j + 2)
|
||||
if (end === -1) {
|
||||
out += s.slice(j)
|
||||
break
|
||||
}
|
||||
const inner = s.slice(j + 2, end)
|
||||
const tr0 = inner.trim()
|
||||
const indirectOnBr =
|
||||
env?.BARE_OS_SHELL_INDIRECT_EXPANSION === '1' ||
|
||||
env?.BARE_OS_SHELL_INDIRECT_EXPANSION === 'true'
|
||||
if (inner === '?') {
|
||||
out += env[BARE_OS_EXIT_STATUS_ENV] ?? '0'
|
||||
} else if (
|
||||
/^#[A-Za-z_][A-Za-z0-9_]*$/.test(tr0) ||
|
||||
(indirectOnBr && /^![A-Za-z_][A-Za-z0-9_]*$/.test(tr0)) ||
|
||||
(paramExpOn &&
|
||||
(inner.includes(':-') ||
|
||||
(paramV3 && (inner.includes(':+') || inner.includes(':?'))) ||
|
||||
(paramV3 &&
|
||||
(/^[A-Za-z_][A-Za-z0-9_]*:\d/.test(tr0) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*\/\//.test(inner))) ||
|
||||
(paramV2 &&
|
||||
(inner.includes(':=') ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*##/.test(inner) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*%%/.test(inner) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*%[^%]/.test(inner) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*#[^#]/.test(inner))) ||
|
||||
(/^[A-Za-z_][A-Za-z0-9_]*#/.test(inner) && inner.includes('#'))))
|
||||
) {
|
||||
out += expandParamBracedInner(inner, env, depth + 1)
|
||||
} else {
|
||||
const ik = inner.trim()
|
||||
shellCheckUnboundParam(ik, env)
|
||||
out += env[ik] ?? ''
|
||||
}
|
||||
j = end + 1
|
||||
continue
|
||||
}
|
||||
if (s[j + 1] === '?') {
|
||||
out += env[BARE_OS_EXIT_STATUS_ENV] ?? '0'
|
||||
j += 2
|
||||
continue
|
||||
}
|
||||
if (/[0-9]/.test(s[j + 1] ?? '')) {
|
||||
const pn = s[j + 1]
|
||||
shellCheckUnboundParam(pn, env)
|
||||
out += env[pn] ?? ''
|
||||
j += 2
|
||||
continue
|
||||
}
|
||||
let k = j + 1
|
||||
while (k < s.length && /[A-Za-z0-9_]/.test(s[k])) k++
|
||||
const name = s.slice(j + 1, k)
|
||||
if (name) {
|
||||
shellCheckUnboundParam(name, env)
|
||||
out += env[name] ?? ''
|
||||
j = k
|
||||
} else {
|
||||
out += '$'
|
||||
j++
|
||||
}
|
||||
continue
|
||||
}
|
||||
out += s[j++]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* argv: Extract<Token, { type: 'word' }>[],
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* Linux-shaped `/proc` / `/dev` pseudo texts used by createVfs.
|
||||
*/
|
||||
import b4a from 'b4a'
|
||||
|
||||
/** UTF-8 bytes; Bare may not define global TextEncoder (see curl-cli utf8Encode). */
|
||||
export function utf8Encode(str) {
|
||||
return b4a.from(String(str), 'utf8')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* env: Record<string, string | undefined>,
|
||||
* procSnapshot?: { version?: string, cmdline?: string } | null,
|
||||
* bootStartedMs?: number | null,
|
||||
* hostProcStatsRef?: { stats?: unknown } | null,
|
||||
* getProcSelfExtraFds?: (() => { fdNum: string, target: string }[]) | null,
|
||||
* getProcSyntheticLinuxCompat?: (() => { sessionId?: string, swarmPeerCount?: number, peerIds?: unknown[] } | null | undefined) | null,
|
||||
* secureRandomBytes?: ((n: number) => Uint8Array) | null,
|
||||
* environKeyAllowed: (k: string) => boolean
|
||||
* }} deps
|
||||
*/
|
||||
export function createVfsPseudoLinux(deps) {
|
||||
const {
|
||||
env,
|
||||
procSnapshot = null,
|
||||
bootStartedMs = null,
|
||||
hostProcStatsRef = null,
|
||||
getProcSelfExtraFds = null,
|
||||
getProcSyntheticLinuxCompat = null,
|
||||
secureRandomBytes = null,
|
||||
environKeyAllowed
|
||||
} = deps
|
||||
|
||||
function pseudoVersionText() {
|
||||
const v = procSnapshot?.version ?? env.BARE_OS_CTX_API_VERSION ?? 'unknown'
|
||||
let out = `Bare OS\nbare_os_ctx_api_version=${v}\n`
|
||||
const tty = String(env.BARE_OS_PEAR_TTY_FLAGS_JSON || '').trim()
|
||||
if (tty) {
|
||||
out += `pear_tty_flags_json=${tty.slice(0, 512)}\n`
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function pseudoCmdlineText() {
|
||||
return `${procSnapshot?.cmdline ?? 'bare-os'}\n`
|
||||
}
|
||||
|
||||
function listProcSelfFdDirNames() {
|
||||
const names = new Set(['0', '1', '2'])
|
||||
const extra = getProcSelfExtraFds ? getProcSelfExtraFds() : []
|
||||
for (const e of extra) {
|
||||
if (e && e.fdNum && /^[0-9]+$/.test(String(e.fdNum))) {
|
||||
names.add(String(e.fdNum))
|
||||
}
|
||||
}
|
||||
return [...names].sort((a, b) => Number(a) - Number(b))
|
||||
}
|
||||
|
||||
function encodeProcSelfFdSymlink(fdNumRaw) {
|
||||
const n = String(fdNumRaw ?? '0')
|
||||
if (n === '0' || n === '1' || n === '2') {
|
||||
return utf8Encode(
|
||||
`/proc/self/fd/${n} -> (bare-os stdio pseudo inode)\n`
|
||||
)
|
||||
}
|
||||
const extra = getProcSelfExtraFds ? getProcSelfExtraFds() : []
|
||||
const hit = extra.find((e) => e && String(e.fdNum) === n)
|
||||
if (hit && hit.target) {
|
||||
return utf8Encode(`/proc/self/fd/${n} -> ${hit.target}\n`)
|
||||
}
|
||||
return utf8Encode(`/proc/self/fd/${n} -> (not open)\n`)
|
||||
}
|
||||
|
||||
function pseudoEnvironBytes() {
|
||||
const parts = []
|
||||
for (const k of Object.keys(env).sort()) {
|
||||
if (!environKeyAllowed(k)) continue
|
||||
const v = env[k]
|
||||
if (typeof v !== 'string') continue
|
||||
parts.push(`${k}=${v}\0`)
|
||||
}
|
||||
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() {
|
||||
const snap = hostProcStatsRef?.stats
|
||||
const mu =
|
||||
snap &&
|
||||
typeof snap === 'object' &&
|
||||
snap !== null &&
|
||||
/** @type {{ memoryUsage?: () => unknown }} */ (snap).memoryUsage
|
||||
? /** @type {{ memoryUsage: () => { rss?: number, heapTotal?: number, heapUsed?: number, external?: number } }} */ (
|
||||
snap
|
||||
).memoryUsage()
|
||||
: null
|
||||
if (mu && typeof mu === 'object') {
|
||||
const rss = Number(mu.rss) || 0
|
||||
const heap = Number(mu.heapUsed) || 0
|
||||
const total = Math.max(rss, heap, 1)
|
||||
const kb = (n) => Math.max(0, Math.floor(n / 1024))
|
||||
return [
|
||||
`MemTotal: ${kb(total * 2)} kB`,
|
||||
`MemFree: ${kb(Math.max(0, total - heap))} kB`,
|
||||
`MemAvailable: ${kb(Math.max(0, total - heap))} kB`,
|
||||
'SwapTotal: 0 kB',
|
||||
'SwapFree: 0 kB',
|
||||
'BogoSource: bare-os host memoryUsage snapshot',
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
return [
|
||||
'MemTotal: 524288 kB',
|
||||
'MemFree: 262144 kB',
|
||||
'MemAvailable: 262144 kB',
|
||||
'SwapTotal: 0 kB',
|
||||
'SwapFree: 0 kB',
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function pseudoCpuinfoText() {
|
||||
const snap = hostProcStatsRef?.stats
|
||||
const cpus =
|
||||
snap &&
|
||||
typeof snap === 'object' &&
|
||||
snap !== null &&
|
||||
typeof (/** @type {{ cpus?: () => unknown[] }} */ (snap).cpus) ===
|
||||
'function'
|
||||
? /** @type {{ cpus: () => unknown[] }} */ (snap).cpus()
|
||||
: null
|
||||
if (Array.isArray(cpus) && cpus.length > 0) {
|
||||
const lines = []
|
||||
let i = 0
|
||||
for (const c of cpus) {
|
||||
const m =
|
||||
c && typeof c === 'object' && 'model' in c
|
||||
? String(/** @type {{ model?: string }} */ (c).model || '')
|
||||
: ''
|
||||
const sp =
|
||||
c && typeof c === 'object' && 'speed' in c
|
||||
? Number(/** @type {{ speed?: number }} */ (c).speed) || 0
|
||||
: 0
|
||||
lines.push(`processor\t: ${i}`)
|
||||
lines.push(`vendor_id\t: host`)
|
||||
lines.push(`model name\t: ${m || 'host CPU (bare-os snapshot)'}`)
|
||||
lines.push(`cpu MHz\t\t: ${sp}`)
|
||||
lines.push(`flags\t: bare_os_host_stats`)
|
||||
lines.push('')
|
||||
i++
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
return [
|
||||
'processor\t: 0',
|
||||
'vendor_id\t: BareOS',
|
||||
'model name\t: Bare OS pseudo CPU (vfs-backed)',
|
||||
'flags\t: bare_os',
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function pseudoLoadavgText() {
|
||||
return '0.05 0.04 0.02 1/64 1\n'
|
||||
}
|
||||
|
||||
function pseudoSelfExeText() {
|
||||
return '/bin/sh (bare-os pseudo inode; no backing host file)\n'
|
||||
}
|
||||
|
||||
function bareOsSanitizeCgroupSegment(s) {
|
||||
return String(s || 'unknown')
|
||||
.replace(/[^a-zA-Z0-9._-]/g, '_')
|
||||
.slice(0, 128)
|
||||
}
|
||||
|
||||
/** @returns {{ sessionId: string, swarmPeerCount: number, peerIds: string[] }} */
|
||||
function readProcSyntheticLinuxCompat() {
|
||||
let sessionId = String(env.BARE_OS_SESSION_ID || env.BARE_OS_TRACE_ID || '')
|
||||
.trim()
|
||||
let swarmPeerCount = 0
|
||||
let peerIds = []
|
||||
try {
|
||||
const h = getProcSyntheticLinuxCompat ? getProcSyntheticLinuxCompat() : null
|
||||
if (h && typeof h === 'object') {
|
||||
if (h.sessionId != null && String(h.sessionId).trim())
|
||||
sessionId = String(h.sessionId).trim()
|
||||
const n = Number(h.swarmPeerCount)
|
||||
if (Number.isFinite(n) && n >= 0) swarmPeerCount = Math.min(4096, n | 0)
|
||||
if (Array.isArray(h.peerIds))
|
||||
peerIds = h.peerIds.map((x) => String(x || '')).filter(Boolean)
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
if (!sessionId) sessionId = 'session'
|
||||
return { sessionId, swarmPeerCount, peerIds }
|
||||
}
|
||||
|
||||
/**
|
||||
* Linux cgroup v2 single-column lines, scoped to the guest session (not host cgroups).
|
||||
*/
|
||||
function pseudoSelfCgroupsText() {
|
||||
const { sessionId } = readProcSyntheticLinuxCompat()
|
||||
const sid = bareOsSanitizeCgroupSegment(sessionId)
|
||||
return [
|
||||
'# bare_os synthetic cgroup v2 view (guest session + swarm scope; not the host cgroup hierarchy)',
|
||||
'0::/bare-os.scope',
|
||||
`0::/bare-os.session/${sid}/init`,
|
||||
`0::/bare-os.session/${sid}/swarm`,
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** IPv4:port as in /proc/net/tcp (little-endian quad hex + port hex). */
|
||||
function bareOsProcNetIpv4PortHex(ip, port) {
|
||||
const parts = String(ip || '0.0.0.0').split('.')
|
||||
const a = Number(parts[0]) || 0
|
||||
const b = Number(parts[1]) || 0
|
||||
const c = Number(parts[2]) || 0
|
||||
const d = Number(parts[3]) || 0
|
||||
const addr = (a | (b << 8) | (c << 16) | (d << 24)) >>> 0
|
||||
const p = port & 0xffff
|
||||
return (
|
||||
addr.toString(16).padStart(8, '0') +
|
||||
':' +
|
||||
p.toString(16).padStart(4, '0')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Logical Hyperswarm / replication streams as Linux-shaped /proc/net/tcp rows (inode encodes slot).
|
||||
*/
|
||||
function pseudoNetTcpText() {
|
||||
const { swarmPeerCount, peerIds } = readProcSyntheticLinuxCompat()
|
||||
const nPeers = Math.max(swarmPeerCount, peerIds.length)
|
||||
const lines = [
|
||||
' sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode',
|
||||
'# bare_os: logical swarm/replication endpoints (not host TCP); st 0A=LISTEN 01=ESTABLISHED'
|
||||
]
|
||||
lines.push(
|
||||
` 0: ${bareOsProcNetIpv4PortHex('127.0.0.1', 57800)} 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 800000`
|
||||
)
|
||||
let inode = 800001
|
||||
for (let i = 0; i < nPeers; i++) {
|
||||
const id = peerIds[i] || '0'.repeat(64)
|
||||
const h = id.slice(0, 8) || '00000000'
|
||||
const v = Number.parseInt(h, 16) || 0
|
||||
const o1 = v & 255
|
||||
const o2 = (v >>> 8) & 255
|
||||
const o3 = (v >>> 16) & 255
|
||||
const o4 = (v >>> 24) & 255
|
||||
const remIp = `${o1}.${o2}.${o3}.${o4}`
|
||||
lines.push(
|
||||
` ${i + 1}: ${bareOsProcNetIpv4PortHex('127.0.0.1', 58000 + i)} ${bareOsProcNetIpv4PortHex(remIp, 57900 + i)} 01 00000000:00000000 00:00000000 00000000 0 0 ${inode}`
|
||||
)
|
||||
inode++
|
||||
}
|
||||
lines.push('')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function pseudoNetUdpText() {
|
||||
const { swarmPeerCount } = readProcSyntheticLinuxCompat()
|
||||
const lines = [
|
||||
' sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref',
|
||||
'# bare_os: logical UDP-shaped discovery/datagram hints (not host UDP)'
|
||||
]
|
||||
lines.push(
|
||||
` 0: ${bareOsProcNetIpv4PortHex('0.0.0.0', 49721)} 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 810000 2`
|
||||
)
|
||||
if (swarmPeerCount > 0) {
|
||||
lines.push(
|
||||
` 1: ${bareOsProcNetIpv4PortHex('127.0.0.1', 49722)} 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 810001 2`
|
||||
)
|
||||
}
|
||||
lines.push('')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* When the booter supplies `secureRandomBytes` (e.g. bare-crypto), reads are suitable
|
||||
* for cryptographic use; otherwise falls back to Math.random (not crypto-grade).
|
||||
*/
|
||||
function pseudoUrandomBytes() {
|
||||
const n = 4096
|
||||
if (secureRandomBytes) {
|
||||
try {
|
||||
return secureRandomBytes(n)
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
const u = new Uint8Array(n)
|
||||
for (let i = 0; i < n; i++) u[i] = Math.floor(Math.random() * 256)
|
||||
return u
|
||||
}
|
||||
|
||||
return {
|
||||
listProcSelfFdDirNames,
|
||||
encodeProcSelfFdSymlink,
|
||||
pseudoVersionText,
|
||||
pseudoCmdlineText,
|
||||
pseudoEnvironBytes,
|
||||
pseudoUptimeText,
|
||||
pseudoMeminfoText,
|
||||
pseudoCpuinfoText,
|
||||
pseudoLoadavgText,
|
||||
pseudoSelfExeText,
|
||||
bareOsSanitizeCgroupSegment,
|
||||
readProcSyntheticLinuxCompat,
|
||||
pseudoSelfCgroupsText,
|
||||
bareOsProcNetIpv4PortHex,
|
||||
pseudoNetTcpText,
|
||||
pseudoNetUdpText,
|
||||
pseudoUrandomBytes
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,10 @@ import {
|
||||
vfsErr
|
||||
} from './vfs-path.js'
|
||||
import { createVfsPersonalLayout } from './vfs-personal.js'
|
||||
import {
|
||||
utf8Encode,
|
||||
createVfsPseudoLinux
|
||||
} from './vfs-pseudo-linux.js'
|
||||
|
||||
export {
|
||||
classifyBareOsVfsPathClass,
|
||||
@@ -797,6 +801,31 @@ export function createVfs(
|
||||
bareOsGuestSensitivePersonalDenied,
|
||||
environKeyAllowed
|
||||
} = createVfsPersonalLayout(env, bareOsIdentityVfsRef)
|
||||
const {
|
||||
listProcSelfFdDirNames,
|
||||
encodeProcSelfFdSymlink,
|
||||
pseudoVersionText,
|
||||
pseudoCmdlineText,
|
||||
pseudoEnvironBytes,
|
||||
pseudoUptimeText,
|
||||
pseudoMeminfoText,
|
||||
pseudoCpuinfoText,
|
||||
pseudoLoadavgText,
|
||||
pseudoSelfExeText,
|
||||
pseudoSelfCgroupsText,
|
||||
pseudoNetTcpText,
|
||||
pseudoNetUdpText,
|
||||
pseudoUrandomBytes
|
||||
} = createVfsPseudoLinux({
|
||||
env,
|
||||
procSnapshot,
|
||||
bootStartedMs,
|
||||
hostProcStatsRef,
|
||||
getProcSelfExtraFds,
|
||||
getProcSyntheticLinuxCompat,
|
||||
secureRandomBytes,
|
||||
environKeyAllowed
|
||||
})
|
||||
let cwd = env.PWD || HOME()
|
||||
|
||||
/**
|
||||
@@ -813,281 +842,6 @@ export function createVfs(
|
||||
)
|
||||
}
|
||||
|
||||
function pseudoVersionText() {
|
||||
const v = procSnapshot?.version ?? env.BARE_OS_CTX_API_VERSION ?? 'unknown'
|
||||
let out = `Bare OS\nbare_os_ctx_api_version=${v}\n`
|
||||
const tty = String(env.BARE_OS_PEAR_TTY_FLAGS_JSON || '').trim()
|
||||
if (tty) {
|
||||
out += `pear_tty_flags_json=${tty.slice(0, 512)}\n`
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function pseudoCmdlineText() {
|
||||
return `${procSnapshot?.cmdline ?? 'bare-os'}\n`
|
||||
}
|
||||
|
||||
/** UTF-8 bytes; Bare may not define global TextEncoder (see curl-cli utf8Encode). */
|
||||
function utf8Encode(str) {
|
||||
return b4a.from(String(str), 'utf8')
|
||||
}
|
||||
|
||||
function listProcSelfFdDirNames() {
|
||||
const names = new Set(['0', '1', '2'])
|
||||
const extra = getProcSelfExtraFds ? getProcSelfExtraFds() : []
|
||||
for (const e of extra) {
|
||||
if (e && e.fdNum && /^[0-9]+$/.test(String(e.fdNum))) {
|
||||
names.add(String(e.fdNum))
|
||||
}
|
||||
}
|
||||
return [...names].sort((a, b) => Number(a) - Number(b))
|
||||
}
|
||||
|
||||
function encodeProcSelfFdSymlink(fdNumRaw) {
|
||||
const n = String(fdNumRaw ?? '0')
|
||||
if (n === '0' || n === '1' || n === '2') {
|
||||
return utf8Encode(
|
||||
`/proc/self/fd/${n} -> (bare-os stdio pseudo inode)\n`
|
||||
)
|
||||
}
|
||||
const extra = getProcSelfExtraFds ? getProcSelfExtraFds() : []
|
||||
const hit = extra.find((e) => e && String(e.fdNum) === n)
|
||||
if (hit && hit.target) {
|
||||
return utf8Encode(`/proc/self/fd/${n} -> ${hit.target}\n`)
|
||||
}
|
||||
return utf8Encode(`/proc/self/fd/${n} -> (not open)\n`)
|
||||
}
|
||||
|
||||
function pseudoEnvironBytes() {
|
||||
const parts = []
|
||||
for (const k of Object.keys(env).sort()) {
|
||||
if (!environKeyAllowed(k)) continue
|
||||
const v = env[k]
|
||||
if (typeof v !== 'string') continue
|
||||
parts.push(`${k}=${v}\0`)
|
||||
}
|
||||
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() {
|
||||
const snap = hostProcStatsRef?.stats
|
||||
const mu =
|
||||
snap &&
|
||||
typeof snap === 'object' &&
|
||||
snap !== null &&
|
||||
/** @type {{ memoryUsage?: () => unknown }} */ (snap).memoryUsage
|
||||
? /** @type {{ memoryUsage: () => { rss?: number, heapTotal?: number, heapUsed?: number, external?: number } }} */ (
|
||||
snap
|
||||
).memoryUsage()
|
||||
: null
|
||||
if (mu && typeof mu === 'object') {
|
||||
const rss = Number(mu.rss) || 0
|
||||
const heap = Number(mu.heapUsed) || 0
|
||||
const total = Math.max(rss, heap, 1)
|
||||
const kb = (n) => Math.max(0, Math.floor(n / 1024))
|
||||
return [
|
||||
`MemTotal: ${kb(total * 2)} kB`,
|
||||
`MemFree: ${kb(Math.max(0, total - heap))} kB`,
|
||||
`MemAvailable: ${kb(Math.max(0, total - heap))} kB`,
|
||||
'SwapTotal: 0 kB',
|
||||
'SwapFree: 0 kB',
|
||||
'BogoSource: bare-os host memoryUsage snapshot',
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
return [
|
||||
'MemTotal: 524288 kB',
|
||||
'MemFree: 262144 kB',
|
||||
'MemAvailable: 262144 kB',
|
||||
'SwapTotal: 0 kB',
|
||||
'SwapFree: 0 kB',
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function pseudoCpuinfoText() {
|
||||
const snap = hostProcStatsRef?.stats
|
||||
const cpus =
|
||||
snap &&
|
||||
typeof snap === 'object' &&
|
||||
snap !== null &&
|
||||
typeof (/** @type {{ cpus?: () => unknown[] }} */ (snap).cpus) ===
|
||||
'function'
|
||||
? /** @type {{ cpus: () => unknown[] }} */ (snap).cpus()
|
||||
: null
|
||||
if (Array.isArray(cpus) && cpus.length > 0) {
|
||||
const lines = []
|
||||
let i = 0
|
||||
for (const c of cpus) {
|
||||
const m =
|
||||
c && typeof c === 'object' && 'model' in c
|
||||
? String(/** @type {{ model?: string }} */ (c).model || '')
|
||||
: ''
|
||||
const sp =
|
||||
c && typeof c === 'object' && 'speed' in c
|
||||
? Number(/** @type {{ speed?: number }} */ (c).speed) || 0
|
||||
: 0
|
||||
lines.push(`processor\t: ${i}`)
|
||||
lines.push(`vendor_id\t: host`)
|
||||
lines.push(`model name\t: ${m || 'host CPU (bare-os snapshot)'}`)
|
||||
lines.push(`cpu MHz\t\t: ${sp}`)
|
||||
lines.push(`flags\t: bare_os_host_stats`)
|
||||
lines.push('')
|
||||
i++
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
return [
|
||||
'processor\t: 0',
|
||||
'vendor_id\t: BareOS',
|
||||
'model name\t: Bare OS pseudo CPU (vfs-backed)',
|
||||
'flags\t: bare_os',
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function pseudoLoadavgText() {
|
||||
return '0.05 0.04 0.02 1/64 1\n'
|
||||
}
|
||||
|
||||
function pseudoSelfExeText() {
|
||||
return '/bin/sh (bare-os pseudo inode; no backing host file)\n'
|
||||
}
|
||||
|
||||
function bareOsSanitizeCgroupSegment(s) {
|
||||
return String(s || 'unknown')
|
||||
.replace(/[^a-zA-Z0-9._-]/g, '_')
|
||||
.slice(0, 128)
|
||||
}
|
||||
|
||||
/** @returns {{ sessionId: string, swarmPeerCount: number, peerIds: string[] }} */
|
||||
function readProcSyntheticLinuxCompat() {
|
||||
let sessionId = String(env.BARE_OS_SESSION_ID || env.BARE_OS_TRACE_ID || '')
|
||||
.trim()
|
||||
let swarmPeerCount = 0
|
||||
let peerIds = []
|
||||
try {
|
||||
const h = getProcSyntheticLinuxCompat ? getProcSyntheticLinuxCompat() : null
|
||||
if (h && typeof h === 'object') {
|
||||
if (h.sessionId != null && String(h.sessionId).trim())
|
||||
sessionId = String(h.sessionId).trim()
|
||||
const n = Number(h.swarmPeerCount)
|
||||
if (Number.isFinite(n) && n >= 0) swarmPeerCount = Math.min(4096, n | 0)
|
||||
if (Array.isArray(h.peerIds))
|
||||
peerIds = h.peerIds.map((x) => String(x || '')).filter(Boolean)
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
if (!sessionId) sessionId = 'session'
|
||||
return { sessionId, swarmPeerCount, peerIds }
|
||||
}
|
||||
|
||||
/**
|
||||
* Linux cgroup v2 single-column lines, scoped to the guest session (not host cgroups).
|
||||
*/
|
||||
function pseudoSelfCgroupsText() {
|
||||
const { sessionId } = readProcSyntheticLinuxCompat()
|
||||
const sid = bareOsSanitizeCgroupSegment(sessionId)
|
||||
return [
|
||||
'# bare_os synthetic cgroup v2 view (guest session + swarm scope; not the host cgroup hierarchy)',
|
||||
'0::/bare-os.scope',
|
||||
`0::/bare-os.session/${sid}/init`,
|
||||
`0::/bare-os.session/${sid}/swarm`,
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** IPv4:port as in /proc/net/tcp (little-endian quad hex + port hex). */
|
||||
function bareOsProcNetIpv4PortHex(ip, port) {
|
||||
const parts = String(ip || '0.0.0.0').split('.')
|
||||
const a = Number(parts[0]) || 0
|
||||
const b = Number(parts[1]) || 0
|
||||
const c = Number(parts[2]) || 0
|
||||
const d = Number(parts[3]) || 0
|
||||
const addr = (a | (b << 8) | (c << 16) | (d << 24)) >>> 0
|
||||
const p = port & 0xffff
|
||||
return (
|
||||
addr.toString(16).padStart(8, '0') +
|
||||
':' +
|
||||
p.toString(16).padStart(4, '0')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Logical Hyperswarm / replication streams as Linux-shaped /proc/net/tcp rows (inode encodes slot).
|
||||
*/
|
||||
function pseudoNetTcpText() {
|
||||
const { swarmPeerCount, peerIds } = readProcSyntheticLinuxCompat()
|
||||
const nPeers = Math.max(swarmPeerCount, peerIds.length)
|
||||
const lines = [
|
||||
' sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode',
|
||||
'# bare_os: logical swarm/replication endpoints (not host TCP); st 0A=LISTEN 01=ESTABLISHED'
|
||||
]
|
||||
lines.push(
|
||||
` 0: ${bareOsProcNetIpv4PortHex('127.0.0.1', 57800)} 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 800000`
|
||||
)
|
||||
let inode = 800001
|
||||
for (let i = 0; i < nPeers; i++) {
|
||||
const id = peerIds[i] || '0'.repeat(64)
|
||||
const h = id.slice(0, 8) || '00000000'
|
||||
const v = Number.parseInt(h, 16) || 0
|
||||
const o1 = v & 255
|
||||
const o2 = (v >>> 8) & 255
|
||||
const o3 = (v >>> 16) & 255
|
||||
const o4 = (v >>> 24) & 255
|
||||
const remIp = `${o1}.${o2}.${o3}.${o4}`
|
||||
lines.push(
|
||||
` ${i + 1}: ${bareOsProcNetIpv4PortHex('127.0.0.1', 58000 + i)} ${bareOsProcNetIpv4PortHex(remIp, 57900 + i)} 01 00000000:00000000 00:00000000 00000000 0 0 ${inode}`
|
||||
)
|
||||
inode++
|
||||
}
|
||||
lines.push('')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function pseudoNetUdpText() {
|
||||
const { swarmPeerCount } = readProcSyntheticLinuxCompat()
|
||||
const lines = [
|
||||
' sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref',
|
||||
'# bare_os: logical UDP-shaped discovery/datagram hints (not host UDP)'
|
||||
]
|
||||
lines.push(
|
||||
` 0: ${bareOsProcNetIpv4PortHex('0.0.0.0', 49721)} 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 810000 2`
|
||||
)
|
||||
if (swarmPeerCount > 0) {
|
||||
lines.push(
|
||||
` 1: ${bareOsProcNetIpv4PortHex('127.0.0.1', 49722)} 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 810001 2`
|
||||
)
|
||||
}
|
||||
lines.push('')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* When the booter supplies `secureRandomBytes` (e.g. bare-crypto), reads are suitable
|
||||
* for cryptographic use; otherwise falls back to Math.random (not crypto-grade).
|
||||
*/
|
||||
function pseudoUrandomBytes() {
|
||||
const n = 4096
|
||||
if (secureRandomBytes) {
|
||||
try {
|
||||
return secureRandomBytes(n)
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
const u = new Uint8Array(n)
|
||||
for (let i = 0; i < n; i++) u[i] = Math.floor(Math.random() * 256)
|
||||
return u
|
||||
}
|
||||
|
||||
function pseudoFileBytes(routePseudo) {
|
||||
const f = routePseudo.file
|
||||
const k = routePseudo.kind
|
||||
|
||||
@@ -34,6 +34,25 @@ import {
|
||||
casePatternMatches
|
||||
} from './lib/shell-syntax.js'
|
||||
import { createVfsPersonalLayout } from './lib/vfs-personal.js'
|
||||
import {
|
||||
listBareOsShellBuiltins,
|
||||
isShellBuiltin,
|
||||
bareOsShellReadSplitFields
|
||||
} from './lib/shell-builtins.js'
|
||||
import {
|
||||
getBareOsPipelineLimits,
|
||||
mergePipelineChildCtx,
|
||||
DEFAULT_PIPELINE_MAX_STAGES
|
||||
} from './lib/shell-runtime.js'
|
||||
import { expandWord } from './lib/shell-expand.js'
|
||||
import {
|
||||
utf8Encode,
|
||||
createVfsPseudoLinux
|
||||
} from './lib/vfs-pseudo-linux.js'
|
||||
import { createBareOsVirtualSignalDeliverer } from './lib/bare-os-virtual-signal.js'
|
||||
import { createKernelLoaderAuditAppend } from './lib/bare-os-loader-audit.js'
|
||||
import { createBooterBootEmitter } from './lib/bare-os-boot-phases.js'
|
||||
import { createBooterProcHelpers } from './lib/bare-os-booter-proc-helpers.js'
|
||||
|
||||
test('posix env flags and caps', (t) => {
|
||||
t.ok(wantPosixSocketFdBridge({ BARE_OS_POSIX_SOCKET_FD_BRIDGE: '1' }))
|
||||
@@ -245,3 +264,127 @@ test('vfs personal layout and guest-sensitive paths', (t) => {
|
||||
t.ok(!unlocked.bareOsGuestSensitivePersonalDenied('/.bare/vault/key'))
|
||||
t.is(unlocked.personalLayoutRootAbs(), '/.bare-os')
|
||||
})
|
||||
|
||||
test('shell builtins and read field split', (t) => {
|
||||
t.ok(isShellBuiltin('cd', {}))
|
||||
t.ok(!isShellBuiltin('read', {}))
|
||||
t.ok(isShellBuiltin('read', { BARE_OS_SHELL_READ_BUILTIN: '1' }))
|
||||
t.ok(listBareOsShellBuiltins({ BARE_OS_SHELL_READ_BUILTIN: '1' }).includes('read'))
|
||||
t.alike(bareOsShellReadSplitFields('a b c', ' ', 2), ['a', 'b c'])
|
||||
})
|
||||
|
||||
test('shell runtime pipeline limits and child merge', (t) => {
|
||||
t.is(getBareOsPipelineLimits({}).maxStages, DEFAULT_PIPELINE_MAX_STAGES)
|
||||
t.is(getBareOsPipelineLimits({ BARE_OS_PIPELINE_MAX_STAGES: '4' }).maxStages, 4)
|
||||
const ctx = { exitCode: 0 }
|
||||
mergePipelineChildCtx(ctx, {
|
||||
exitCode: 7,
|
||||
identity: { state: 'unlocked', publicKey: 1, secretKey: 2 }
|
||||
})
|
||||
t.is(ctx.exitCode, 7)
|
||||
t.is(ctx.identity.state, 'unlocked')
|
||||
})
|
||||
|
||||
test('expandWord param and arithmetic', (t) => {
|
||||
t.is(expandWord('x${HOME}y', { HOME: '/h' }), 'x/hy')
|
||||
t.is(expandWord('$(( $a + 1 ))', { a: '2' }), '3')
|
||||
})
|
||||
|
||||
test('vfs linux-shaped /proc texts', (t) => {
|
||||
const linux = createVfsPseudoLinux({
|
||||
env: {
|
||||
BARE_OS_CTX_API_VERSION: '1.2.3',
|
||||
BARE_OS_SESSION_ID: 'sess-1',
|
||||
HOME: '/home/guest',
|
||||
PASSWORD: 'nope'
|
||||
},
|
||||
procSnapshot: { version: '1.2.3', cmdline: 'bare-os' },
|
||||
bootStartedMs: Date.now() - 2500,
|
||||
environKeyAllowed: (k) => k !== 'PASSWORD',
|
||||
getProcSyntheticLinuxCompat: () => ({
|
||||
sessionId: 'sess-1',
|
||||
swarmPeerCount: 1,
|
||||
peerIds: ['aabbccdd']
|
||||
})
|
||||
})
|
||||
t.ok(linux.pseudoVersionText().includes('1.2.3'))
|
||||
t.ok(linux.pseudoMeminfoText().includes('MemTotal:'))
|
||||
t.ok(linux.pseudoCpuinfoText().includes('processor'))
|
||||
t.ok(linux.pseudoNetTcpText().includes('ESTABLISHED'))
|
||||
t.ok(linux.pseudoSelfCgroupsText().includes('sess-1'))
|
||||
t.ok(utf8Encode('hi').byteLength >= 2)
|
||||
})
|
||||
|
||||
test('virtual signal deliverer ignore and existence probe', (t) => {
|
||||
const state = new Map()
|
||||
const traps = []
|
||||
const deliver = createBareOsVirtualSignalDeliverer({
|
||||
dispatchShellTrapSignal: (ctx, sig) => {
|
||||
traps.push(sig)
|
||||
},
|
||||
virtualSignalState: state
|
||||
})
|
||||
const ctx = {
|
||||
bareOsLogicalSigaction: { TERM: 'IGNORE' },
|
||||
requestBooterExit() {
|
||||
t.fail('should not exit when ignored')
|
||||
}
|
||||
}
|
||||
const ignored = deliver(ctx, 3, 'TERM')
|
||||
t.ok(ignored.ignored)
|
||||
t.is(traps.length, 0)
|
||||
const probe = deliver({ requestBooterExit() {} }, 3, '0')
|
||||
t.ok(probe.exists)
|
||||
t.absent(probe.delivered)
|
||||
})
|
||||
|
||||
test('loader audit appends when enabled', async (t) => {
|
||||
const files = new Map()
|
||||
const append = createKernelLoaderAuditAppend({
|
||||
env: { BARE_OS_LOADER_AUDIT: '1' },
|
||||
sessionId: 's1',
|
||||
vfs: {
|
||||
async readFile(p) {
|
||||
if (!files.has(p)) throw new Error('missing')
|
||||
return files.get(p)
|
||||
},
|
||||
async writeFile(p, buf) {
|
||||
files.set(p, buf)
|
||||
}
|
||||
}
|
||||
})
|
||||
await append({ phase: 'vfs' })
|
||||
const txt = new TextDecoder().decode(files.get('/run/bare-os/loader-audit.ndjson'))
|
||||
t.ok(txt.includes('"type":"loader_audit"'))
|
||||
t.ok(txt.includes('"phase":"vfs"'))
|
||||
})
|
||||
|
||||
test('booter boot emitter records phases', (t) => {
|
||||
const bootReadyStateRef = { booterPhases: [], booterStages: [] }
|
||||
const events = []
|
||||
const { emitBooterBootStep } = createBooterBootEmitter({
|
||||
bootReadyStateRef,
|
||||
bootStartedMs: Date.now(),
|
||||
bootEventSubs: [(ev) => events.push(ev)],
|
||||
diagnosticsSubs: [],
|
||||
sessionId: 's',
|
||||
lifecycleSchemaVersion: 1
|
||||
})
|
||||
emitBooterBootStep('vfs')
|
||||
emitBooterBootStep('vfs')
|
||||
t.alike(bootReadyStateRef.booterPhases, ['vfs'])
|
||||
t.is(events.length, 2)
|
||||
t.is(events[0].phase, 'booter:vfs')
|
||||
})
|
||||
|
||||
test('booter proc helpers chat/meshdrop off notes', (t) => {
|
||||
const helpers = createBooterProcHelpers({
|
||||
interactiveCtxRef: { ctx: { bareOsLogicalFds: { 7: '/tmp/x' } } },
|
||||
disk: {},
|
||||
shellEnv: {},
|
||||
hostEnv: {}
|
||||
})
|
||||
t.alike(helpers.bareOsCollectLogicalFdRows(), [{ fd: 7, target: '/tmp/x' }])
|
||||
t.ok(helpers.bareOsChatProcSnapshotRecord().note)
|
||||
t.ok(helpers.bareOsMeshdropProcSnapshotRecord().note)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user