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
314 lines
8.4 KiB
JavaScript
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
|
|
}
|
|
}
|