Updated kernel-runner delegate deny path to emit on stderr in packages/bare-os-booter/lib/kernel-runner.js. Added structured errno metadata (err.code) via vfsErr(...) and applied it to key VFS traversal/permission throws in packages/bare-os-booter/lib/vfs.js (ENOENT/EACCES paths). Critical command exit-code fixes: packages/bare-os-coreutils/src/ls.js Tracks access/stat failures and sets nonzero exit when any operand fails. packages/bare-os-coreutils/src/grep.js Recursive walker now reports traversal/stat errors back to main flow so fatal status becomes 2. packages/bare-os-coreutils/src/rm.js -f only suppresses not-found style errors; permission/deny failures stay nonzero. packages/bare-os-coreutils/src/chmod.js Treats falsy/no-op backend chmod result as failure (nonzero). packages/bare-os-coreutils/src/find.js Added root-path preflight so missing root now reports explicit error + nonzero. Minor quirks: packages/bare-os-coreutils/src/xargs.js -P0 now maps to bounded parallel cap (env/default cap), not forced serial. packages/bare-os-coreutils/src/ulimit.js -f with value now returns explicit unsupported-setter diagnostic + nonzero. -f alone reports unlimited. Regression tests: Added packages/bare-os-coreutils/test/error-propagation.test.mjs covering: grep missing file => exit 2 rm -f permission error => nonzero find missing root => nonzero + diagnostic ulimit -f 1M => explicit unsupported + nonzero xargs -P0 bounded behavior
558 lines
17 KiB
JavaScript
558 lines
17 KiB
JavaScript
import b4a from 'b4a'
|
|
import fs from '#host-fs'
|
|
import unixPathResolve from 'unix-path-resolve'
|
|
import { raceWithAbortAndTimeout } from './bare-os-abort.js'
|
|
import {
|
|
isDelegateKindAllowed,
|
|
loadBareOsHostDelegates,
|
|
parseDelegateAllowSet
|
|
} from './host-delegate-registry.js'
|
|
import { bareOsDelegateRateAllow } from './delegate-rate-limit.js'
|
|
import {
|
|
bareOsDelegateConcurrentExit,
|
|
bareOsDelegateConcurrentTryEnter
|
|
} from './bare-os-delegate-concurrent.js'
|
|
|
|
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
|
/** @type {Map<string, number>} */
|
|
const workerHeartbeatByKey = new Map()
|
|
|
|
/**
|
|
* @param {string} key
|
|
*/
|
|
export function bareOsNoteWorkerHeartbeat(key) {
|
|
workerHeartbeatByKey.set(String(key || '').slice(0, 128), Date.now())
|
|
}
|
|
|
|
/**
|
|
* @param {{ staleAfterMs?: number }} [opts]
|
|
*/
|
|
export function bareOsWorkerHeartbeatSnapshot(opts = {}) {
|
|
const now = Date.now()
|
|
const staleAfterMs = Math.max(
|
|
100,
|
|
Math.min(300000, Number(opts.staleAfterMs) || 30000)
|
|
)
|
|
/** @type {{ key: string, lastMs: number, stale: boolean }[]} */
|
|
const rows = []
|
|
for (const [key, lastMs] of workerHeartbeatByKey.entries()) {
|
|
rows.push({
|
|
key,
|
|
lastMs,
|
|
stale: now - lastMs > staleAfterMs
|
|
})
|
|
}
|
|
return {
|
|
schema: 1,
|
|
atMs: now,
|
|
staleAfterMs,
|
|
workers: rows
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Persist non-secret liveness checkpoint when enabled.
|
|
* @param {Record<string, string | undefined> | null | undefined} env
|
|
*/
|
|
export async function bareOsPersistWorkerLivenessCheckpoint(env) {
|
|
const p = String(env?.BARE_OS_WORKER_LIVENESS_PATH || '').trim()
|
|
if (!p || (!p.startsWith('/') && !p.startsWith('./'))) return false
|
|
try {
|
|
await fs.promises.writeFile(
|
|
p,
|
|
JSON.stringify(bareOsWorkerHeartbeatSnapshot(), null, 2) + '\n'
|
|
)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pear/Bare cannot resolve `node:module`. Holepunch's `bare-module` exposes the same `createRequire(parentURL)` as Node. Only called from bin-worker offload after the Node early-return in `tryRunBinInBareWorker`.
|
|
* @returns {((specifier: string) => unknown) | null}
|
|
*/
|
|
async function bareOsCreateRequireFromMetaUrl() {
|
|
try {
|
|
const bm = await import('bare-module')
|
|
const createRequire = bm.createRequire || bm.default?.createRequire
|
|
if (typeof createRequire !== 'function') return null
|
|
return createRequire(import.meta.url)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} cmd
|
|
* @param {Record<string, string | undefined> | null | undefined} env
|
|
*/
|
|
const _BIN_WORKER_TEXTPROC = new Set([
|
|
'awk',
|
|
'sed',
|
|
'jq',
|
|
'cut',
|
|
'tr',
|
|
'sort',
|
|
'uniq'
|
|
])
|
|
|
|
const _BIN_WORKER_MATHPROC = new Set(['bc', 'dc'])
|
|
|
|
const _BIN_WORKER_IOPROC = new Set(['cat', 'head', 'tail'])
|
|
|
|
const _BIN_WORKER_MEDIAPROC = new Set(['ffmpeg', 'ffprobe'])
|
|
|
|
/** Crypto-class `/bin` entries (e.g. openssl) under `cryptoproc:*` (word 11). */
|
|
const _BIN_WORKER_CRYPTOPROC = new Set(['openssl'])
|
|
|
|
/** Indexer-adjacent tools under `indexerproc:*` (word 11; reserved; extend when `/bin` gains indexer helpers). */
|
|
const _BIN_WORKER_INDEXERPROC = new Set([])
|
|
|
|
/** VFS metadata utilities under `metaproc:*` (ACL / xattr sidecars). */
|
|
const _BIN_WORKER_METAPROC = new Set(['getfacl', 'setfacl', 'xattr'])
|
|
|
|
/** Process/shell-adjacent builtins allowed under `sysproc:*` (documentary group; bare-process-class). */
|
|
const _BIN_WORKER_SYSPROC = new Set([
|
|
'pwd',
|
|
'echo',
|
|
'printenv',
|
|
'uname',
|
|
'id',
|
|
'env',
|
|
'true',
|
|
'false'
|
|
])
|
|
|
|
/**
|
|
* @param {string} cmd
|
|
* @param {Record<string, string | undefined> | null | undefined} env
|
|
*/
|
|
function binWorkerOffloadEnabled(cmd, env) {
|
|
if (!env) return false
|
|
if (
|
|
env.BARE_OS_BIN_WORKER_OFFLOAD !== '1' &&
|
|
env.BARE_OS_BIN_WORKER_OFFLOAD !== 'true'
|
|
) {
|
|
return false
|
|
}
|
|
const allowRaw = String(env.BARE_OS_BIN_WORKER_ALLOW || '').trim()
|
|
if (!allowRaw) return cmd === 'awk' || cmd === 'sed' || cmd === 'jq'
|
|
const parts = allowRaw
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
for (const p of parts) {
|
|
if (p === 'textproc:*') {
|
|
if (_BIN_WORKER_TEXTPROC.has(cmd)) return true
|
|
continue
|
|
}
|
|
if (p === 'mathproc:*') {
|
|
if (_BIN_WORKER_MATHPROC.has(cmd)) return true
|
|
continue
|
|
}
|
|
if (p === 'ioproc:*') {
|
|
if (_BIN_WORKER_IOPROC.has(cmd)) return true
|
|
continue
|
|
}
|
|
if (p === 'mediaproc:*') {
|
|
if (_BIN_WORKER_MEDIAPROC.has(cmd)) return true
|
|
continue
|
|
}
|
|
if (p === 'sysproc:*') {
|
|
if (_BIN_WORKER_SYSPROC.has(cmd)) return true
|
|
continue
|
|
}
|
|
if (p === 'cryptoproc:*') {
|
|
if (_BIN_WORKER_CRYPTOPROC.has(cmd)) return true
|
|
continue
|
|
}
|
|
if (p === 'indexerproc:*') {
|
|
if (_BIN_WORKER_INDEXERPROC.has(cmd)) return true
|
|
continue
|
|
}
|
|
if (p === 'metaproc:*') {
|
|
if (_BIN_WORKER_METAPROC.has(cmd)) return true
|
|
continue
|
|
}
|
|
if (p.endsWith(':*')) {
|
|
const pre = p.slice(0, -2)
|
|
if (pre === 'textproc' && _BIN_WORKER_TEXTPROC.has(cmd)) return true
|
|
if (pre === 'mathproc' && _BIN_WORKER_MATHPROC.has(cmd)) return true
|
|
if (pre === 'ioproc' && _BIN_WORKER_IOPROC.has(cmd)) return true
|
|
if (pre === 'mediaproc' && _BIN_WORKER_MEDIAPROC.has(cmd)) return true
|
|
if (pre === 'sysproc' && _BIN_WORKER_SYSPROC.has(cmd)) return true
|
|
if (pre === 'cryptoproc' && _BIN_WORKER_CRYPTOPROC.has(cmd)) return true
|
|
if (pre === 'indexerproc' && _BIN_WORKER_INDEXERPROC.has(cmd)) return true
|
|
if (pre === 'metaproc' && _BIN_WORKER_METAPROC.has(cmd)) return true
|
|
continue
|
|
}
|
|
if (p === cmd) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string[]} argv
|
|
* @param {string} source
|
|
* @returns {Promise<boolean>} true when the worker handled the command
|
|
*/
|
|
async function tryRunBinInBareWorker(ctx, argv, source) {
|
|
if (
|
|
typeof process !== 'undefined' &&
|
|
process.release &&
|
|
process.release.name === 'node'
|
|
) {
|
|
return false
|
|
}
|
|
const cmd = argv[0]
|
|
const env = ctx.vfs?.env
|
|
if (!binWorkerOffloadEnabled(cmd, env)) return false
|
|
const reportFallback = (reason, err) => {
|
|
if (!env) return
|
|
const on =
|
|
env.BARE_OS_BIN_WORKER_REPORT_FALLBACK === '1' ||
|
|
env.BARE_OS_BIN_WORKER_REPORT_FALLBACK === 'true'
|
|
if (!on) return
|
|
const msg = err && err.message ? `${reason}: ${err.message}` : String(reason)
|
|
try {
|
|
ctx.console?.error?.(`[bare-os] bin-worker fallback for ${cmd}: ${msg}`)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
try {
|
|
const req = await bareOsCreateRequireFromMetaUrl()
|
|
if (!req) {
|
|
reportFallback('bare-module createRequire unavailable')
|
|
return false
|
|
}
|
|
const { runBinOffloaded } = req('./bare-os-bin-offload-worker.cjs')
|
|
const wasmRaw = env && String(env.BARE_OS_BIN_WORKER_WASM_MS_MAX || '').trim()
|
|
const wasmN = wasmRaw ? Number.parseInt(wasmRaw, 10) : 0
|
|
const maxWasmMs =
|
|
Number.isFinite(wasmN) && wasmN > 0
|
|
? Math.min(wasmN, 3_600_000)
|
|
: 0
|
|
const msg = await runBinOffloaded(
|
|
{ source, argv },
|
|
maxWasmMs > 0 ? { maxWasmMs } : undefined
|
|
)
|
|
if (!msg || msg.ok === false) {
|
|
reportFallback(
|
|
msg && typeof msg === 'object' && msg.reason ? `worker rejected (${msg.reason})` : 'worker rejected'
|
|
)
|
|
return false
|
|
}
|
|
const logs = Array.isArray(msg.logs) ? msg.logs : []
|
|
const errs = Array.isArray(msg.errs) ? msg.errs : []
|
|
for (const line of logs) ctx.console.log(line)
|
|
for (const line of errs) ctx.console.error(line)
|
|
if (Number(msg.droppedLogs) > 0 || Number(msg.droppedErrs) > 0) {
|
|
ctx.console.error(
|
|
`[bare-os] bin-worker output truncated for ${cmd} (dropped logs=${Number(msg.droppedLogs) || 0}, errs=${Number(msg.droppedErrs) || 0})`
|
|
)
|
|
}
|
|
ctx.exitCode =
|
|
msg.exitCode != null ? Number(msg.exitCode) || 0 : 0
|
|
return true
|
|
} catch (e) {
|
|
reportFallback('worker exception', /** @type {Error} */ (e))
|
|
return false
|
|
}
|
|
}
|
|
|
|
/** @type {ReturnType<loadBareOsHostDelegates>} */
|
|
let _delegateCache = null
|
|
function bareOsHostDelegates() {
|
|
if (!_delegateCache) _delegateCache = loadBareOsHostDelegates()
|
|
return _delegateCache
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} kind
|
|
* @param {string[]} argv
|
|
*/
|
|
async function auditDelegateJson(ctx, kind, argv) {
|
|
const env = ctx.vfs?.env
|
|
if (!env || (env.BARE_OS_AUDIT !== '1' && env.BARE_OS_AUDIT !== 'true'))
|
|
return
|
|
if (env.BARE_OS_AUDIT_JSON !== '1' && env.BARE_OS_AUDIT_JSON !== 'true')
|
|
return
|
|
const { appendVarLog, AUDIT_LOG } = await import('./bare-os-var-log.js')
|
|
const args = argv.slice(0, 24).map((a) => {
|
|
const s = String(a)
|
|
return s.length > 160 ? s.slice(0, 160) + '…' : s
|
|
})
|
|
void appendVarLog(
|
|
ctx,
|
|
AUDIT_LOG,
|
|
'json',
|
|
JSON.stringify({
|
|
auditSchemaVersion: 8,
|
|
requestSmugglingClass: 'none',
|
|
type: 'delegate',
|
|
kind,
|
|
ts: Date.now(),
|
|
argv: args,
|
|
argvRawCount: argv.length,
|
|
argvTruncated: argv.length > 24,
|
|
sessionId: String(env.BARE_OS_SESSION_ID || '').trim() || undefined,
|
|
delegateChainDepth: 1,
|
|
delegateChainTruncated: false
|
|
})
|
|
)
|
|
}
|
|
|
|
/** Strip one leading Unix shebang so AsyncFunction does not see `#!` as invalid syntax. */
|
|
function stripShebang(source) {
|
|
if (typeof source !== 'string' || !source.startsWith('#!')) return source
|
|
const m = source.match(/^#![^\n]*\n/)
|
|
return m ? source.slice(m[0].length) : source
|
|
}
|
|
|
|
/**
|
|
* Execute kernel source from Hyperdrive (trusted). Expects top-level `async function start(ctx)`.
|
|
* @param {string} source
|
|
* @param {Record<string, unknown>} ctx
|
|
*/
|
|
export async function runKernelFromSource(source, ctx) {
|
|
const fn = new AsyncFunction(
|
|
'ctx',
|
|
`${source}\nif (typeof start !== 'function') throw new Error('kernel must define async function start')\nreturn start(ctx)\n`
|
|
)
|
|
return fn(ctx)
|
|
}
|
|
|
|
/**
|
|
* Evaluate user script source: top-level statements run in an async function with `ctx` and `argv`.
|
|
* If the script defines a top-level `run` function, it is awaited after the body (same as `/bin` utilities).
|
|
*
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} src
|
|
* @param {string[]} argv
|
|
* @param {string} [_label] reserved for diagnostics
|
|
*/
|
|
/**
|
|
* Evaluate user script source (same contract as `/bin` utilities).
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} src
|
|
* @param {string[]} argv
|
|
* @param {string} [_label]
|
|
*/
|
|
export async function runUserScriptFromSource(ctx, src, argv, _label = argv[0]) {
|
|
try {
|
|
const body = stripShebang(src)
|
|
const sessionConsole =
|
|
ctx &&
|
|
ctx.console &&
|
|
typeof ctx.console === 'object' &&
|
|
typeof ctx.console.log === 'function'
|
|
? ctx.console
|
|
: null
|
|
const prelude =
|
|
sessionConsole != null
|
|
? 'var console = ctx.console;\n'
|
|
: ''
|
|
const fn = new AsyncFunction(
|
|
'ctx',
|
|
'argv',
|
|
`${prelude}${body}\nif (typeof run === 'function') await run(ctx, argv)\n`
|
|
)
|
|
await fn(ctx, argv)
|
|
if (ctx.exitCode === undefined || ctx.exitCode === null) ctx.exitCode = 0
|
|
return
|
|
} catch (e) {
|
|
const msg = e?.message || String(e)
|
|
const stack = e?.stack
|
|
if (typeof ctx.console?.error === 'function') {
|
|
ctx.console.error(msg)
|
|
if (stack && typeof stack === 'string' && stack !== msg) {
|
|
const lines = stack.split('\n').slice(1, 4)
|
|
for (const line of lines) {
|
|
if (line && line.trim()) ctx.console.error(line.trim())
|
|
}
|
|
}
|
|
}
|
|
ctx.exitCode = 1
|
|
ctx.bareOsScriptCompletedWithCatch = true
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run a command: bare `*.js` in cwd (before PATH), path script on any routed drive, then PATH on system.
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string[]} argv
|
|
* @param {import('./bare-os-abort.js').BareOsAbortOpts} [runOpts]
|
|
*/
|
|
export async function runBinCommand(ctx, argv, runOpts) {
|
|
const env = ctx?.vfs?.env
|
|
const defTo = Number.parseInt(
|
|
String(env?.BARE_OS_RUNBIN_DEFAULT_TIMEOUT_MS || ''),
|
|
10
|
|
)
|
|
/** @type {import('./bare-os-abort.js').BareOsAbortOpts | undefined} */
|
|
let merged =
|
|
runOpts && typeof runOpts === 'object' ? { ...runOpts } : undefined
|
|
if (Number.isFinite(defTo) && defTo > 0) {
|
|
const cap = Math.min(defTo, 3_600_000)
|
|
if (!merged) merged = { timeoutMs: cap }
|
|
else if (merged.timeoutMs == null) merged.timeoutMs = cap
|
|
}
|
|
const bridgeOn =
|
|
env?.BARE_OS_BARE_SUBPROCESS_BRIDGE === '1' ||
|
|
env?.BARE_OS_BARE_SUBPROCESS_BRIDGE === 'true'
|
|
if (bridgeOn) {
|
|
const subMs = Number.parseInt(
|
|
String(env?.BARE_OS_BARE_SUBPROCESS_TIMEOUT_MS || ''),
|
|
10
|
|
)
|
|
if (Number.isFinite(subMs) && subMs > 0) {
|
|
const cap = Math.min(subMs, 3_600_000)
|
|
if (!merged) merged = { timeoutMs: cap }
|
|
else if (merged.timeoutMs == null) merged.timeoutMs = cap
|
|
}
|
|
}
|
|
return raceWithAbortAndTimeout(
|
|
runBinCommandInner(ctx, argv),
|
|
merged,
|
|
'runBinCommand'
|
|
)
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string[]} argv
|
|
*/
|
|
async function runBinCommandInner(ctx, argv) {
|
|
const cmd = argv[0]
|
|
const systemDrive = ctx.drive
|
|
const vfs = ctx.vfs
|
|
const pathEnv = (vfs && vfs.env && vfs.env.PATH) || '/bin'
|
|
const allow = parseDelegateAllowSet(vfs?.env)
|
|
const argvTail = argv.length > 1 ? argv.slice(1) : []
|
|
|
|
// `.sh` scripts should execute through the shell frontend, not JS eval.
|
|
const runViaSh = (scriptPath) => runBinCommandInner(ctx, ['sh', scriptPath, ...argvTail])
|
|
|
|
for (const del of bareOsHostDelegates()) {
|
|
if (!del.shouldDelegate(cmd)) continue
|
|
if (!isDelegateKindAllowed(del.kind, allow)) {
|
|
// `curl` / `wget` have drive-resident `/bin` scripts that call
|
|
// `ctx.bareOsRunCurlCli` / `ctx.bareOsRunWgetCli` (same fetch backends).
|
|
if (del.kind === 'curl' || del.kind === 'wget') continue
|
|
ctx.console.error('delegate denied by BARE_OS_DELEGATE_ALLOW: ' + del.kind)
|
|
ctx.exitCode = 126
|
|
return
|
|
}
|
|
if (!bareOsDelegateRateAllow(del.kind, vfs?.env)) {
|
|
ctx.console.error('delegate rate limit exceeded: ' + del.kind)
|
|
ctx.exitCode = 126
|
|
return
|
|
}
|
|
const env = vfs?.env
|
|
const auditOnly =
|
|
env &&
|
|
(env.BARE_OS_DELEGATE_AUDIT_ONLY === '1' ||
|
|
env.BARE_OS_DELEGATE_AUDIT_ONLY === 'true') &&
|
|
(env.BARE_OS_AUDIT === '1' || env.BARE_OS_AUDIT === 'true')
|
|
if (auditOnly) {
|
|
await auditDelegateJson(ctx, del.kind, argv)
|
|
const { appendVarLog, AUDIT_LOG } = await import('./bare-os-var-log.js')
|
|
void appendVarLog(
|
|
ctx,
|
|
AUDIT_LOG,
|
|
'delegate-audit-only',
|
|
del.kind + ' ' + argv.slice(0, 8).join(' ')
|
|
)
|
|
ctx.console.error(
|
|
'[bare-os] delegate audit-only (skipped run): ' + del.kind
|
|
)
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (!bareOsDelegateConcurrentTryEnter(del.kind, vfs?.env)) {
|
|
ctx.console.error('delegate concurrency limit exceeded: ' + del.kind)
|
|
ctx.exitCode = 126
|
|
return
|
|
}
|
|
await auditDelegateJson(ctx, del.kind, argv)
|
|
try {
|
|
return await del.run(ctx, argv)
|
|
} finally {
|
|
bareOsDelegateConcurrentExit(del.kind)
|
|
}
|
|
}
|
|
|
|
if (cmd.includes('/')) {
|
|
if (cmd.endsWith('.sh')) return runViaSh(cmd)
|
|
const abs = vfs.resolveLogical(cmd)
|
|
const { drive, path } = vfs.route(abs)
|
|
const buf = await drive.get(path, { follow: true })
|
|
if (!buf) {
|
|
ctx.console.log('not found: ' + cmd)
|
|
ctx.exitCode = 127
|
|
return
|
|
}
|
|
const source = b4a.toString(buf)
|
|
if (await tryRunBinInBareWorker(ctx, argv, source)) return
|
|
return runUserScriptFromSource(ctx, source, argv, cmd)
|
|
}
|
|
|
|
// `script.js` in $PWD before PATH (same VFS routing as `./script.js`).
|
|
if (cmd.endsWith('.sh')) {
|
|
const abs = vfs.resolveLogical(cmd)
|
|
const { drive, path } = vfs.route(abs)
|
|
const buf = await drive.get(path, { follow: true })
|
|
if (buf) return runViaSh(cmd)
|
|
}
|
|
|
|
// `script.js` in $PWD before PATH (same VFS routing as `./script.js`).
|
|
if (cmd.endsWith('.js')) {
|
|
const abs = vfs.resolveLogical(cmd)
|
|
const { drive, path } = vfs.route(abs)
|
|
const buf = await drive.get(path, { follow: true })
|
|
if (buf) {
|
|
const source = b4a.toString(buf)
|
|
if (await tryRunBinInBareWorker(ctx, argv, source)) return
|
|
return runUserScriptFromSource(ctx, source, argv, cmd)
|
|
}
|
|
}
|
|
|
|
const dirs = pathEnv.split(':').filter(Boolean)
|
|
for (const dir of dirs) {
|
|
const p = unixPathResolve(dir, cmd)
|
|
const buf = await systemDrive.get(p, { follow: true })
|
|
if (buf) {
|
|
const source = b4a.toString(buf)
|
|
if (await tryRunBinInBareWorker(ctx, argv, source)) return
|
|
return runUserScriptFromSource(ctx, source, argv, p)
|
|
}
|
|
}
|
|
|
|
ctx.console.log('unknown command: ' + cmd)
|
|
ctx.exitCode = 127
|
|
}
|
|
|
|
/**
|
|
* Resolve an executable name on PATH (system drive only).
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} name
|
|
* @returns {Promise<string | null>}
|
|
*/
|
|
export async function resolveBinInPath(ctx, name) {
|
|
if (!name || name.includes('/')) return null
|
|
const systemDrive = ctx.drive
|
|
const vfs = ctx.vfs
|
|
const pathEnv = (vfs && vfs.env && vfs.env.PATH) || '/bin'
|
|
const dirs = pathEnv.split(':').filter(Boolean)
|
|
for (const dir of dirs) {
|
|
const p = unixPathResolve(dir, name)
|
|
const buf = await systemDrive.get(p, { follow: true })
|
|
if (buf) return p
|
|
}
|
|
return null
|
|
}
|