Files
bare-operating-system/scripts/lib/run-bin-harness.mjs
T
Raven Scott ff9ab499a2 New tooling
Entry: scripts/run-bin.mjs
Preload shim (must load first): scripts/run-bin-prelude.mjs
Harness: scripts/lib/run-bin-harness.mjs

Usage:

node scripts/run-bin.mjs ls -la /bin
node scripts/run-bin.mjs cat /etc/passwd
node scripts/run-bin.mjs --cmd sed -n '1,5p' /home/user/note.txt
node scripts/run-bin.mjs --json echo "hello"
printf 'a\nb\n' | node scripts/run-bin.mjs cat
npm run test:coreutils -- --suite basic
npm run test:coreutils -- --build --watch -- echo test   # rebuild + watch src/<cmd>.js (use `--buil
2026-04-27 21:32:50 -04:00

314 lines
8.4 KiB
JavaScript

/**
* Temp Corestore + Hyperdrive + stock createVfs / runBinCommand — same path as
* scripts/probe-kernel-bin.mjs, extended for run-bin (buffers, stubs, fixtures).
*/
import b4a from 'b4a'
import Corestore from 'corestore'
import Hyperdrive from 'hyperdrive'
import os from 'node:os'
import path from 'node:path'
import { mkdtemp, readdir, readFile, rm, stat } from 'node:fs/promises'
import { createBareOsIpc } from '../../packages/bare-os-booter/lib/bare-os-ipc.js'
import { runBinCommand } from '../../packages/bare-os-booter/lib/kernel-runner.js'
import { createVfs } from '../../packages/bare-os-booter/lib/vfs.js'
import { BARE_OS_CTX_API_VERSION } from '../../packages/bare-os-booter/lib/bare-os-ctx-api.js'
const textDecoder = new TextDecoder()
/**
* Backing path on the personal drive for logical `/home/<seg>/…` (see `vfs.js` + `test.js`).
* @param {string} logicalPath e.g. `/home/user/note.txt`
* @returns {string | null} null when not under `/home/…`
*/
function personalDrivePathForHome(logicalPath) {
const p = String(logicalPath || '').replace(/\\/g, '/')
if (!p.startsWith('/home/')) return null
return '/.bare-os/home/' + p.slice('/home/'.length)
}
const DEFAULT_SYSTEM_FILES = {
'/etc/passwd':
'root:x:0:0:root:/root:/bin/sh\nuser:x:1000:1000:user:/home/user:/bin/sh\n',
'/etc/os-release':
'NAME="Bare OS"\nID=bare-os\nVERSION="run-bin"\nPRETTY_NAME="Bare OS (run-bin harness)"\n'
}
const DEFAULT_HOME_FILES = {
'/home/user/.profile': '# run-bin harness\n',
'/home/user/note.txt': 'hello from fixture layout\n'
}
/**
* @param {string} repoRoot
* @param {{
* timeoutMs?: number,
* cwd?: string,
* identitySession?: 'guest' | 'unlocked'
* }} [opts]
*/
export async function createRunBinHarness(repoRoot, opts = {}) {
const timeoutMs =
Number.parseInt(process.env.BARE_OS_RUNBIN_DEFAULT_TIMEOUT_MS || '', 10) ||
opts.timeoutMs ||
120_000
const cwd = opts.cwd ?? '/'
const identitySession = opts.identitySession ?? 'guest'
const tmp = await mkdtemp(path.join(os.tmpdir(), 'bare-os-run-bin-'))
const store = new Corestore(tmp)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('run-bin-personal'))
await drive.ready()
await personal.ready()
const env = {
HOME: '/home/user',
PATH: '/bin:/usr/bin',
USER: 'user',
LOGNAME: 'user',
SHELL: '/bin/sh',
UID: '1000',
GID: '1000',
PWD: cwd,
BARE_OS_EXIT_STATUS: '0',
BARE_OS_RUNBIN_DEFAULT_TIMEOUT_MS: String(timeoutMs),
BARE_OS_DELEGATE_ALLOW: 'none',
BARE_OS_DELEGATE_AUDIT_ONLY: '1',
BARE_OS_AUDIT: '1',
TERM: 'dumb',
LANG: 'C.UTF-8'
}
const bareOsIpc = createBareOsIpc()
const vfs = createVfs(drive, personal, env, null, {
bareOsIpc,
bareOsIdentityVfsRef: { session: identitySession }
})
/** @type {string[]} */
const logs = []
/** @type {string[]} */
const errs = []
/** @type {string[]} */
const binWriteChunks = []
function resetBuffers() {
logs.length = 0
errs.length = 0
binWriteChunks.length = 0
}
/** @type {Record<string, unknown>} */
const ctx = {
drive,
personalDrive: personal,
vfs,
bareOsIpc,
env,
b4a,
bareOsCtxApiVersion: BARE_OS_CTX_API_VERSION,
platform: process.platform,
shellStdin: '',
exitCode: 0,
console: {
log: (...a) => logs.push(a.join(' ')),
error: (...a) => errs.push(a.join(' '))
},
bareOsBinWrite: (chunk) => {
const u8 =
typeof chunk === 'string'
? b4a.from(chunk)
: chunk instanceof Uint8Array
? chunk
: new Uint8Array(chunk)
binWriteChunks.push(textDecoder.decode(u8))
},
bareOsDiagnosticsSubscribe(_cb) {
return () => {}
},
async bareOsHrpcRequest() {
throw new Error(
'run-bin: bare_os RPC unavailable (no swarm — use full booter for HRPC)'
)
},
async bareOsRunCurlCli() {
errs.push('run-bin: curl delegate disabled (stub — no network)')
this.exitCode = 126
},
async bareOsRunWgetCli() {
errs.push('run-bin: wget delegate disabled (stub — no network)')
this.exitCode = 126
},
async bareOsRunHolesailCli() {
errs.push('run-bin: holesail delegate disabled (stub — no swarm)')
this.exitCode = 126
},
async bareOsRunOpensslCli() {
errs.push(
'run-bin: openssl delegate disabled (stub — use host openssl outside run-bin)'
)
this.exitCode = 126
}
}
ctx.runBinCommand = function runSelf(argv, runOpts) {
return runBinCommand(this, argv, runOpts)
}
const kernelBinDir = path.join(repoRoot, 'kernel', 'bin')
async function loadKernelBinNames() {
const ents = await readdir(kernelBinDir, { withFileTypes: true })
return ents
.filter((ent) => ent.isFile())
.map((ent) => ent.name)
.sort((a, b) => a.localeCompare(b))
}
async function seedKernelBins() {
const names = await loadKernelBinNames()
for (const name of names) {
const src = await readFile(path.join(kernelBinDir, name), 'utf8')
await drive.put('/bin/' + name, b4a.from(src))
}
return names
}
async function putPath(logicalPath, body) {
const u8 = typeof body === 'string' ? b4a.from(body) : body
const pp = personalDrivePathForHome(logicalPath)
if (pp) await personal.put(pp, u8)
else await drive.put(logicalPath, u8)
}
async function seedLayout() {
for (const [p, body] of Object.entries(DEFAULT_SYSTEM_FILES)) {
await drive.put(p, b4a.from(body))
}
for (const [p, body] of Object.entries(DEFAULT_HOME_FILES)) {
await putPath(p, body)
}
}
/**
* Overlay files from test/fixtures/coreutils/overlays/<fixtureName>/ as /-rooted paths.
* @param {string} fixtureName
*/
async function applyFixture(fixtureName) {
if (!fixtureName) return
const root = path.join(
repoRoot,
'test/fixtures/coreutils/overlays',
fixtureName
)
let st
try {
st = await stat(root)
} catch {
throw new Error(`run-bin: fixture overlay not found: ${root}`)
}
if (!st.isDirectory()) {
throw new Error(`run-bin: fixture overlay is not a directory: ${root}`)
}
async function walkDir(absDir) {
const ents = await readdir(absDir, { withFileTypes: true })
for (const ent of ents) {
const full = path.join(absDir, ent.name)
if (ent.isDirectory()) {
await walkDir(full)
} else {
const rel = path.relative(root, full)
const vfsPath = '/' + rel.split(path.sep).join('/')
const buf = await readFile(full)
await putPath(vfsPath, buf)
}
}
}
await walkDir(root)
}
/**
* @param {{ skip?: boolean }} [o]
*/
async function vfsSnapshot(o = {}) {
if (o.skip === true) return undefined
/** @type {Record<string, string[]>} */
const out = {}
const roots = ['/', '/bin', '/etc', '/home/user', '/proc', '/sys', '/dev']
for (const r of roots) {
try {
const names = await vfs.readdir(r)
out[r] = Array.isArray(names)
? names.slice().sort((a, b) => a.localeCompare(b))
: []
} catch {
out[r] = []
}
}
return out
}
function combinedStdout() {
const text = logs.join('\n')
const raw = binWriteChunks.join('')
if (!raw) return text
return text ? text + '\n' + raw : raw
}
async function cleanup() {
await personal.close()
await drive.close()
await store.close()
await rm(tmp, { recursive: true, force: true })
}
/**
* @param {string[]} argv
* @param {{ timeoutMs?: number }} [runOpts]
*/
async function run(argv, runOpts) {
resetBuffers()
ctx.exitCode = 0
ctx.shellStdin =
typeof runOpts?.shellStdin === 'string' ? runOpts.shellStdin : ''
const t0 = performance.now()
let thrown = null
try {
await runBinCommand(ctx, argv, {
timeoutMs: runOpts?.timeoutMs ?? timeoutMs
})
} catch (e) {
thrown = e instanceof Error ? e : new Error(String(e))
}
const durationMs = performance.now() - t0
return {
exitCode: ctx.exitCode ?? 0,
stdout: combinedStdout(),
stderr: errs.join('\n'),
durationMs,
thrown
}
}
return {
repoRoot,
tmp,
store,
drive,
personal,
vfs,
ctx,
kernelBinDir,
resetBuffers,
loadKernelBinNames,
seedKernelBins,
seedLayout,
applyFixture,
vfsSnapshot,
cleanup,
run,
timeoutMs
}
}