Updates
This commit is contained in:
@@ -32,7 +32,10 @@ import {
|
||||
applyLoginKeys
|
||||
} from './lib/identity-session.js'
|
||||
import { HdmsController, runHdmsCli } from './lib/hdms-manager.js'
|
||||
import { startBareInitd } from './lib/bare-initd.js'
|
||||
import {
|
||||
startBareInitd,
|
||||
registerKernelShutdownHook
|
||||
} from './lib/bare-initd.js'
|
||||
import './lib/bare-cron.js'
|
||||
|
||||
const _pkg = packageRootDir(import.meta.url)
|
||||
@@ -317,6 +320,13 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
*/
|
||||
async runBinCommand(argv) {
|
||||
return runBinCommand(this, argv)
|
||||
},
|
||||
/**
|
||||
* Register async/sync teardown when the kernel session ends (before initd disposers).
|
||||
* @param {() => void | Promise<void>} fn
|
||||
*/
|
||||
registerKernelShutdownHook(fn) {
|
||||
registerKernelShutdownHook(fn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +385,7 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
try {
|
||||
await runKernelFromSource(b4a.toString(initSource), ctx)
|
||||
} finally {
|
||||
session.cleanup()
|
||||
await session.cleanup()
|
||||
}
|
||||
return sessionExitCode
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ const registry = []
|
||||
/** @type {(() => void)[]} */
|
||||
const disposers = []
|
||||
|
||||
/** @type {(() => void | Promise<void>)[]} */
|
||||
const kernelShutdownHooks = []
|
||||
|
||||
/**
|
||||
* Register cleanup (e.g. clearInterval) for when the kernel session ends.
|
||||
* @param {() => void} fn
|
||||
@@ -19,6 +22,29 @@ export function registerBareInitdDisposer(fn) {
|
||||
if (typeof fn === 'function') disposers.push(fn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a function to run when the REPL/kernel session ends (before initd disposers).
|
||||
* Use for async teardown (flush buffers, close handles). Errors are swallowed.
|
||||
* @param {() => void | Promise<void>} fn
|
||||
*/
|
||||
export function registerKernelShutdownHook(fn) {
|
||||
if (typeof fn === 'function') kernelShutdownHooks.push(fn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs shutdown hooks in reverse registration order (LIFO), then clears the queue.
|
||||
*/
|
||||
export async function runKernelShutdownHooks() {
|
||||
while (kernelShutdownHooks.length) {
|
||||
const fn = kernelShutdownHooks.pop()
|
||||
try {
|
||||
await fn()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function stopBareInitd() {
|
||||
while (disposers.length) {
|
||||
const fn = disposers.pop()
|
||||
|
||||
@@ -67,7 +67,9 @@ async function runScriptFromSource(ctx, src, argv, label = argv[0]) {
|
||||
'argv',
|
||||
`${body}\nif (typeof run !== 'function') throw new Error('missing run() in ${label}')\nreturn run(ctx, argv)\n`
|
||||
)
|
||||
return await fn(ctx, argv)
|
||||
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
|
||||
@@ -80,6 +82,7 @@ async function runScriptFromSource(ctx, src, argv, label = argv[0]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +118,7 @@ export async function runBinCommand(ctx, argv) {
|
||||
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)
|
||||
@@ -143,6 +147,7 @@ export async function runBinCommand(ctx, argv) {
|
||||
}
|
||||
|
||||
ctx.console.log('unknown command: ' + cmd)
|
||||
ctx.exitCode = 127
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
replDbg,
|
||||
unbindReplDebugStream
|
||||
} from './debug-repl.js'
|
||||
import { stopBareInitd } from './bare-initd.js'
|
||||
import {
|
||||
runKernelShutdownHooks,
|
||||
stopBareInitd
|
||||
} from './bare-initd.js'
|
||||
|
||||
/**
|
||||
* Kernel `console` must write to the same stream as the line editor so cursor stays in sync.
|
||||
@@ -162,8 +165,9 @@ export async function createKernelReplSession({
|
||||
return line
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
async function cleanup() {
|
||||
if (isReplDebug()) replDbg('repl', 'cleanup', fishRead ? 'fish teardown' : 'noop')
|
||||
await runKernelShutdownHooks()
|
||||
stopBareInitd()
|
||||
if (fishRead && stdin) {
|
||||
disableFishRawMode(stdin)
|
||||
|
||||
@@ -20,6 +20,16 @@ function isShellBuiltin(cmd) {
|
||||
return SHELL_BUILTINS.has(cmd)
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
}
|
||||
}
|
||||
|
||||
/** Max alias indirections (prevents cycles). */
|
||||
const MAX_ALIAS_DEPTH = 16
|
||||
|
||||
@@ -224,11 +234,31 @@ export function tokenize(line) {
|
||||
if (i >= line.length) break
|
||||
|
||||
const c = line[i]
|
||||
if (c === '|') {
|
||||
tokens.push({ type: 'op', value: '|' })
|
||||
if (c === ';') {
|
||||
tokens.push({ type: 'op', value: ';' })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c === '&') {
|
||||
if (line[i + 1] === '&') {
|
||||
tokens.push({ type: 'op', value: '&&' })
|
||||
i += 2
|
||||
} else {
|
||||
tokens.push({ type: 'op', value: '&' })
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (c === '|') {
|
||||
if (line[i + 1] === '|') {
|
||||
tokens.push({ type: 'op', value: '||' })
|
||||
i += 2
|
||||
} else {
|
||||
tokens.push({ type: 'op', value: '|' })
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (c === '>') {
|
||||
if (line[i + 1] === '>') {
|
||||
tokens.push({ type: 'op', value: '>>' })
|
||||
@@ -274,7 +304,15 @@ export function tokenize(line) {
|
||||
if (i < line.length) i++
|
||||
continue
|
||||
}
|
||||
if (/\s/.test(ch) || ch === '|' || ch === '>' || ch === '<') break
|
||||
if (
|
||||
/\s/.test(ch) ||
|
||||
ch === '|' ||
|
||||
ch === '>' ||
|
||||
ch === '<' ||
|
||||
ch === ';' ||
|
||||
ch === '&'
|
||||
)
|
||||
break
|
||||
word += ch
|
||||
i++
|
||||
}
|
||||
@@ -423,19 +461,66 @@ function parseSimpleCommand(seg) {
|
||||
return { argv: argvWords, assign, redirIn, redirOut, redirAppend }
|
||||
}
|
||||
|
||||
/**
|
||||
* Split token list on `;` into separate commands (AND-OR lists).
|
||||
* @param {Token[]} tokens
|
||||
* @returns {Token[][]}
|
||||
*/
|
||||
export function splitTokensBySemicolon(tokens) {
|
||||
/** @type {Token[][]} */
|
||||
const lists = []
|
||||
/** @type {Token[]} */
|
||||
let cur = []
|
||||
for (const t of tokens) {
|
||||
if (t.type === 'op' && t.value === ';') {
|
||||
lists.push(cur)
|
||||
cur = []
|
||||
} else {
|
||||
cur.push(t)
|
||||
}
|
||||
}
|
||||
lists.push(cur)
|
||||
return lists
|
||||
}
|
||||
|
||||
/**
|
||||
* Split one semicolon-separated list on `&&` / `||` (left-associative chain).
|
||||
* @param {Token[]} tokens
|
||||
* @returns {{ segments: Token[][], ops: string[] }}
|
||||
*/
|
||||
export function splitTokensByAndOr(tokens) {
|
||||
/** @type {Token[][]} */
|
||||
const segments = []
|
||||
/** @type {string[]} */
|
||||
const ops = []
|
||||
/** @type {Token[]} */
|
||||
let cur = []
|
||||
for (const t of tokens) {
|
||||
if (t.type === 'op' && (t.value === '&&' || t.value === '||')) {
|
||||
segments.push(cur)
|
||||
ops.push(t.value)
|
||||
cur = []
|
||||
} else {
|
||||
cur.push(t)
|
||||
}
|
||||
}
|
||||
segments.push(cur)
|
||||
return { segments, ops }
|
||||
}
|
||||
|
||||
/** @param {Token[]} seg */
|
||||
function segmentHasCommand(seg) {
|
||||
if (!seg.length) return false
|
||||
const cmd = parseSimpleCommand(seg)
|
||||
return cmd.argv.length > 0 || Object.keys(cmd.assign).length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} line
|
||||
* @param {SimpleCmd[]} pipeline
|
||||
* @returns {Promise<'exit' | 'ok'>}
|
||||
*/
|
||||
export async function execShellLine(ctx, line) {
|
||||
const raw = line.trim()
|
||||
if (!raw) return 'ok'
|
||||
|
||||
const tokens = tokenize(raw)
|
||||
if (!tokens.length) return 'ok'
|
||||
|
||||
const pipeline = parsePipeline(tokens)
|
||||
async function execParsedPipeline(ctx, pipeline) {
|
||||
const vfs = ctx.vfs
|
||||
const env = vfs.env
|
||||
|
||||
@@ -454,6 +539,7 @@ export async function execShellLine(ctx, line) {
|
||||
argv = expandArgvAliases(argv, ctx.shellAliases)
|
||||
} catch (e) {
|
||||
ctx.console.error((e && e.message) || String(e))
|
||||
ctx.exitCode = 1
|
||||
continue
|
||||
}
|
||||
const name = argv[0]
|
||||
@@ -474,12 +560,18 @@ export async function execShellLine(ctx, line) {
|
||||
ctx.shellReadonlyVars.has(k)
|
||||
) {
|
||||
origErr.call(ctx.console, k + ': readonly variable')
|
||||
ctx.exitCode = 1
|
||||
continue
|
||||
}
|
||||
env[k] = expandWord(val, env)
|
||||
}
|
||||
|
||||
if (!cmd.argv.length) continue
|
||||
if (!cmd.argv.length) {
|
||||
ctx.exitCode = 0
|
||||
continue
|
||||
}
|
||||
|
||||
ctx.exitCode = 0
|
||||
|
||||
if (!isLast || cmd.redirOut) {
|
||||
ctx.console.log = (...args) => {
|
||||
@@ -506,15 +598,20 @@ export async function execShellLine(ctx, line) {
|
||||
ctx.console,
|
||||
'alias: usage: alias name=value [name=value ...]'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
} else if (name === 'unalias') {
|
||||
runUnaliasBuiltin(ctx, argv, (m) => origErr.call(ctx.console, m))
|
||||
runUnaliasBuiltin(ctx, argv, (m) => {
|
||||
origErr.call(ctx.console, m)
|
||||
ctx.exitCode = 1
|
||||
})
|
||||
} else if (name === 'cd') {
|
||||
try {
|
||||
await vfs.chdir(argv[1] || vfs.home)
|
||||
} catch (e) {
|
||||
origErr.call(ctx.console, (e && e.message) || String(e))
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
} else if (name === 'export') {
|
||||
for (const a of argv.slice(1)) {
|
||||
@@ -526,6 +623,7 @@ export async function execShellLine(ctx, line) {
|
||||
ctx.shellReadonlyVars.has(k)
|
||||
) {
|
||||
origErr.call(ctx.console, k + ': readonly variable')
|
||||
ctx.exitCode = 1
|
||||
continue
|
||||
}
|
||||
env[k] = expandWord(a.slice(eq + 1), env)
|
||||
@@ -540,6 +638,7 @@ export async function execShellLine(ctx, line) {
|
||||
ctx.console,
|
||||
'unset: ' + a + ': cannot unset: readonly variable'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
continue
|
||||
}
|
||||
delete env[a]
|
||||
@@ -560,6 +659,7 @@ export async function execShellLine(ctx, line) {
|
||||
const oct = argv[1]
|
||||
if (!/^[0-7]{1,4}$/.test(oct)) {
|
||||
origErr.call(ctx.console, 'umask: invalid octal mask')
|
||||
ctx.exitCode = 1
|
||||
} else {
|
||||
env.UMASK = oct
|
||||
}
|
||||
@@ -573,15 +673,21 @@ export async function execShellLine(ctx, line) {
|
||||
const cargs = argv.slice(1)
|
||||
if (cargs.length === 0) {
|
||||
origErr.call(ctx.console, 'command: missing operand')
|
||||
ctx.exitCode = 1
|
||||
} else if (cargs[0] === '-v' || cargs[0] === '-V') {
|
||||
const cmdn = cargs[1]
|
||||
if (!cmdn) origErr.call(ctx.console, 'command: missing operand')
|
||||
else if (isShellBuiltin(cmdn)) {
|
||||
if (!cmdn) {
|
||||
origErr.call(ctx.console, 'command: missing operand')
|
||||
ctx.exitCode = 1
|
||||
} else if (isShellBuiltin(cmdn)) {
|
||||
origLog.call(ctx.console, cmdn)
|
||||
} else {
|
||||
const p = await resolveBinInPath(ctx, cmdn)
|
||||
if (p) origLog.call(ctx.console, p)
|
||||
else origErr.call(ctx.console, 'command: ' + cmdn + ': not found')
|
||||
else {
|
||||
origErr.call(ctx.console, 'command: ' + cmdn + ': not found')
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const childCtx =
|
||||
@@ -589,16 +695,22 @@ export async function execShellLine(ctx, line) {
|
||||
? Object.assign({}, ctx, { shellStdin: stdinText, env })
|
||||
: Object.assign({}, ctx, { env })
|
||||
await runBinCommand(childCtx, cargs)
|
||||
mergeChildExitCode(ctx, childCtx)
|
||||
}
|
||||
} else if (name === 'type') {
|
||||
const cmdn = argv[1]
|
||||
if (!cmdn) origErr.call(ctx.console, 'type: missing operand')
|
||||
else if (isShellBuiltin(cmdn)) {
|
||||
if (!cmdn) {
|
||||
origErr.call(ctx.console, 'type: missing operand')
|
||||
ctx.exitCode = 1
|
||||
} else if (isShellBuiltin(cmdn)) {
|
||||
origLog.call(ctx.console, cmdn + ' is a shell builtin')
|
||||
} else {
|
||||
const p = await resolveBinInPath(ctx, cmdn)
|
||||
if (p) origLog.call(ctx.console, cmdn + ' is ' + p)
|
||||
else origErr.call(ctx.console, 'type: ' + cmdn + ': not found')
|
||||
else {
|
||||
origErr.call(ctx.console, 'type: ' + cmdn + ': not found')
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
} else if (name === 'login') {
|
||||
const rest = argv.slice(1)
|
||||
@@ -610,6 +722,7 @@ export async function execShellLine(ctx, line) {
|
||||
const passphrase = rest.map((w) => expandWord(w, env)).join(' ')
|
||||
if (!passphrase) {
|
||||
origErr.call(ctx.console, 'usage: login [--new] <passphrase>')
|
||||
ctx.exitCode = 1
|
||||
} else if (
|
||||
typeof ctx.applyRegister === 'function' &&
|
||||
typeof ctx.applyUnlock === 'function'
|
||||
@@ -619,9 +732,11 @@ export async function execShellLine(ctx, line) {
|
||||
else await ctx.applyUnlock(passphrase)
|
||||
} catch (e) {
|
||||
origErr.call(ctx.console, (e && e.message) || String(e))
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
} else {
|
||||
origErr.call(ctx.console, 'login: not supported in this environment')
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
} else if (name === 'logout') {
|
||||
const save = argv.includes('--save')
|
||||
@@ -630,9 +745,11 @@ export async function execShellLine(ctx, line) {
|
||||
await ctx.applyLogout({ save })
|
||||
} catch (e) {
|
||||
origErr.call(ctx.console, (e && e.message) || String(e))
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
} else {
|
||||
origErr.call(ctx.console, 'logout: not supported in this environment')
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
} else if (name === 'exit') {
|
||||
code = 'exit'
|
||||
@@ -651,8 +768,10 @@ export async function execShellLine(ctx, line) {
|
||||
: Object.assign({}, ctx, { env })
|
||||
try {
|
||||
await runBinCommand(childCtx, argv)
|
||||
mergeChildExitCode(ctx, childCtx)
|
||||
} catch (e) {
|
||||
origErr.call(ctx.console, (e && e.message) || String(e))
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
if (name === '/bin/exit' || name.endsWith('/exit')) {
|
||||
code = 'exit'
|
||||
@@ -687,3 +806,61 @@ export async function execShellLine(ctx, line) {
|
||||
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Token[]} tokens
|
||||
* @returns {Promise<'exit' | 'ok'>}
|
||||
*/
|
||||
async function execAndOrList(ctx, tokens) {
|
||||
const { segments, ops } = splitTokensByAndOr(tokens)
|
||||
for (let s = 0; s < segments.length; s++) {
|
||||
if (!segmentHasCommand(segments[s])) {
|
||||
ctx.console.error('shell: invalid null command')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
}
|
||||
let lastStatus = 0
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
if (i > 0) {
|
||||
const op = ops[i - 1]
|
||||
if (op === '&&' && lastStatus !== 0) continue
|
||||
if (op === '||' && lastStatus === 0) continue
|
||||
}
|
||||
const pipeline = parsePipeline(segments[i])
|
||||
const r = await execParsedPipeline(ctx, pipeline)
|
||||
if (r === 'exit') return 'exit'
|
||||
lastStatus = Number(ctx.exitCode) || 0
|
||||
}
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} line
|
||||
* @returns {Promise<'exit' | 'ok'>}
|
||||
*/
|
||||
export async function execShellLine(ctx, line) {
|
||||
const raw = line.trim()
|
||||
if (!raw) return 'ok'
|
||||
|
||||
const tokens = tokenize(raw)
|
||||
if (!tokens.length) return 'ok'
|
||||
|
||||
if (tokens.some((t) => t.type === 'op' && t.value === '&')) {
|
||||
ctx.console.error(
|
||||
'shell: unsupported operator & (job control is not available)'
|
||||
)
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
const lists = splitTokensBySemicolon(tokens)
|
||||
for (const listTok of lists) {
|
||||
if (!listTok.length) continue
|
||||
const r = await execAndOrList(ctx, listTok)
|
||||
if (r === 'exit') return 'exit'
|
||||
}
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ import {
|
||||
expandArgvAliases,
|
||||
defaultShellAliases,
|
||||
loadBarerc,
|
||||
BARERC_SKELETON
|
||||
BARERC_SKELETON,
|
||||
splitTokensBySemicolon,
|
||||
splitTokensByAndOr
|
||||
} from './lib/shell.js'
|
||||
import {
|
||||
fuzzyMatch,
|
||||
@@ -35,7 +37,12 @@ import {
|
||||
parseCronLine,
|
||||
jobMatchesDate
|
||||
} from './lib/bare-cron.js'
|
||||
import { registerBareInitdDisposer, stopBareInitd } from './lib/bare-initd.js'
|
||||
import {
|
||||
registerBareInitdDisposer,
|
||||
registerKernelShutdownHook,
|
||||
runKernelShutdownHooks,
|
||||
stopBareInitd
|
||||
} from './lib/bare-initd.js'
|
||||
import {
|
||||
DEFAULT_CURL_USER_AGENT,
|
||||
DEFAULT_WGET_USER_AGENT
|
||||
@@ -562,6 +569,92 @@ async function run(ctx, argv) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('tokenize && || ; and split helpers', async (t) => {
|
||||
const toks = tokenize('a&&b||c;d')
|
||||
t.is(
|
||||
toks.map((x) => x.value).join(' '),
|
||||
'a && b || c ; d'
|
||||
)
|
||||
const lists = splitTokensBySemicolon(toks)
|
||||
t.is(lists.length, 2)
|
||||
const { segments, ops } = splitTokensByAndOr(lists[0])
|
||||
t.is(segments.length, 3)
|
||||
t.is(ops.join(' '), '&& ||')
|
||||
})
|
||||
|
||||
test('execShellLine && || ; short-circuit and exitCode', async (t) => {
|
||||
const dir = testCorestoreDir('shandor')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('sand'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const stub = `
|
||||
async function run(ctx, argv) {
|
||||
const c = argv[0]
|
||||
ctx.ran.push(c)
|
||||
ctx.exitCode = c === 'false' ? 1 : 0
|
||||
}
|
||||
`
|
||||
await drive.put('/bin/true', b4a.from(stub))
|
||||
await drive.put('/bin/false', b4a.from(stub))
|
||||
await drive.put(
|
||||
'/bin/rec',
|
||||
b4a.from(`
|
||||
async function run(ctx, argv) {
|
||||
ctx.ran.push('rec')
|
||||
ctx.exitCode = 0
|
||||
}
|
||||
`)
|
||||
)
|
||||
const ran = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.ran = ran
|
||||
ctx.exitCode = 0
|
||||
|
||||
await execShellLine(ctx, 'false && rec')
|
||||
t.is(ran.join(','), 'false')
|
||||
t.is(ctx.exitCode, 1)
|
||||
|
||||
ran.length = 0
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, 'false || rec')
|
||||
t.is(ran.join(','), 'false,rec')
|
||||
t.is(ctx.exitCode, 0)
|
||||
|
||||
ran.length = 0
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, 'true && rec')
|
||||
t.is(ran.join(','), 'true,rec')
|
||||
t.is(ctx.exitCode, 0)
|
||||
|
||||
ran.length = 0
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, 'true || rec')
|
||||
t.is(ran.join(','), 'true')
|
||||
t.is(ctx.exitCode, 0)
|
||||
|
||||
ran.length = 0
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, 'false || false || rec')
|
||||
t.is(ran.join(','), 'false,false,rec')
|
||||
t.is(ctx.exitCode, 0)
|
||||
|
||||
ran.length = 0
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, 'true && false || rec')
|
||||
t.is(ran.join(','), 'true,false,rec')
|
||||
t.is(ctx.exitCode, 0)
|
||||
|
||||
ran.length = 0
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, 'true; rec')
|
||||
t.is(ran.join(','), 'true,rec')
|
||||
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('cron fieldMatches and dowFieldMatches', async (t) => {
|
||||
t.ok(fieldMatches('*', 0, 0, 59))
|
||||
t.ok(fieldMatches('*/5', 10, 0, 59))
|
||||
@@ -601,6 +694,20 @@ test('stopBareInitd runs registered disposers', async (t) => {
|
||||
t.is(n, 1)
|
||||
})
|
||||
|
||||
test('runKernelShutdownHooks runs LIFO once', async (t) => {
|
||||
const o = []
|
||||
registerKernelShutdownHook(async () => {
|
||||
o.push('a')
|
||||
})
|
||||
registerKernelShutdownHook(async () => {
|
||||
o.push('b')
|
||||
})
|
||||
await runKernelShutdownHooks()
|
||||
t.is(o.join(','), 'b,a')
|
||||
await runKernelShutdownHooks()
|
||||
t.is(o.join(','), 'b,a')
|
||||
})
|
||||
|
||||
test('fish-readline stripAnsi and fuzzyMatch', async (t) => {
|
||||
t.is(stripAnsi('\x1b[32mhi\x1b[0m'), 'hi')
|
||||
t.ok(fuzzyMatch('hello', 'hlo'))
|
||||
@@ -750,6 +857,67 @@ test('tier-1 jq from system drive', async (t) => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('tier-1 getconf and xargs from system drive', async (t) => {
|
||||
const dir = testCorestoreDir('gxc')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pgx'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await drive.put('/bin/getconf', b4a.from(await readBuiltBin('getconf')))
|
||||
await drive.put('/bin/echolog', b4a.from(`
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.log(argv.slice(1).join(' '))
|
||||
}
|
||||
`))
|
||||
await drive.put('/bin/xargs', b4a.from(await readBuiltBin('xargs')))
|
||||
await drive.put('/bin/false', b4a.from(await readBuiltBin('false')))
|
||||
await drive.put('/bin/printf', b4a.from(await readBuiltBin('printf')))
|
||||
const lines = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.runBinCommand = (argv) => runBinCommand(ctx, argv)
|
||||
ctx.exitCode = 0
|
||||
ctx.console = {
|
||||
log(s) {
|
||||
lines.push(String(s))
|
||||
},
|
||||
error(s) {
|
||||
lines.push(String(s))
|
||||
}
|
||||
}
|
||||
await runBinCommand(ctx, ['getconf', 'PATH_MAX'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(lines.pop(), '4096')
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['getconf', 'NOT_A_REAL_CONF_NAME'])
|
||||
t.is(ctx.exitCode, 1)
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
ctx.shellStdin = 'hello\tworld\n'
|
||||
await runBinCommand(ctx, ['xargs', 'echolog'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(lines.join('\n'), 'hello world')
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
ctx.shellStdin = 'a\0b\0'
|
||||
await runBinCommand(ctx, ['xargs', '-0', '-n1', 'echolog'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(lines.join('\n'), 'a\nb')
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
ctx.shellStdin = ''
|
||||
await execShellLine(ctx, 'false || printf recovered')
|
||||
t.is(lines.pop(), 'recovered')
|
||||
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('curl delegated from booter with stub fetch', async (t) => {
|
||||
const dir = testCorestoreDir('curl')
|
||||
const store = new Corestore(dir)
|
||||
|
||||
@@ -39,7 +39,7 @@ Or `node packages/bare-os-coreutils/build.mjs`.
|
||||
|
||||
**`sed`** / **`awk`** use large interpreters in **`lib/sed-engine.js`** and **`lib/awk-engine.js`** — capable, but not guaranteed to match every POSIX or GNU edge case.
|
||||
|
||||
**Stubs** (**`xargs`**, **`getconf`**, **`chown`**, **`chgrp`**, **`mkfifo`**) print a clear error and exit non-zero.
|
||||
**Stubs** (**`chown`**, **`chgrp`**, **`mkfifo`**) print a clear error and exit non-zero. **`getconf`** and **`xargs`** implement a documented Bare-specific subset (see `src/getconf.js`, `src/xargs.js`).
|
||||
|
||||
See [handbook §6 — Kernel and `/bin`](../../handbook/06-kernel-and-binaries.md), [handbook §9 — POSIX alignment](../../handbook/09-posix-utilities-shell-and-vfs.md), and [handbook §10 — `man` and online help](../../handbook/10-manpages-and-online-help.md).
|
||||
|
||||
|
||||
@@ -3,22 +3,32 @@
|
||||
"section": 1,
|
||||
"title": "get configuration values",
|
||||
"synopsis": [
|
||||
"getconf [OPTION]... [OPERAND]..."
|
||||
"getconf [-a] system_var"
|
||||
],
|
||||
"description": "Prints a fixed subset of POSIX-style limit names and values for Bare OS. There is no host sysconf path; constants match the documented JavaScript/VFS environment.",
|
||||
"options": [
|
||||
{
|
||||
"flag": "-a",
|
||||
"meaning": "Write every known variable (each name on one line, value on the next)"
|
||||
}
|
||||
],
|
||||
"description": "Host sysconf-style values are not exposed. The command prints an error.",
|
||||
"options": [],
|
||||
"keywords": [
|
||||
"getconf",
|
||||
"limits",
|
||||
"PATH_MAX",
|
||||
"POSIX",
|
||||
"bare-os",
|
||||
"coreutils",
|
||||
"stub"
|
||||
"coreutils"
|
||||
],
|
||||
"stub": true,
|
||||
"bareOsNotes": "Stub only; no kernel sysconf surface.",
|
||||
"bareOsNotes": "Subset only; unknown names fail with exit status 1. See src/getconf.js for the name table.",
|
||||
"examples": [
|
||||
{
|
||||
"caption": "stub",
|
||||
"code": "# getconf PATH_MAX — not available on Bare OS"
|
||||
"caption": "path length limit",
|
||||
"code": "getconf PATH_MAX"
|
||||
},
|
||||
{
|
||||
"caption": "list known names and values",
|
||||
"code": "getconf -a"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,21 +3,37 @@
|
||||
"section": 1,
|
||||
"title": "construct argument lists and invoke utility",
|
||||
"synopsis": [
|
||||
"xargs [OPTION]... [OPERAND]..."
|
||||
"xargs [-0] [-n maxargs] [--] [utility [argument ...]]"
|
||||
],
|
||||
"description": "Reads stdin, splits into words (or null-terminated records with -0), and invokes the utility via ctx.runBinCommand in batches. Enforces fixed limits on stdin size, token count, arguments per run, and total invocations.",
|
||||
"options": [
|
||||
{
|
||||
"flag": "-0, --null",
|
||||
"meaning": "Input items are separated by null bytes instead of whitespace"
|
||||
},
|
||||
{
|
||||
"flag": "-n maxargs, --max-args maxargs",
|
||||
"meaning": "Use at most maxargs arguments from stdin per utility invocation (capped at 128)"
|
||||
}
|
||||
],
|
||||
"description": "xargs does not spawn arbitrary /bin utilities on Bare OS. Use shell word splitting or pipelines.",
|
||||
"options": [],
|
||||
"keywords": [
|
||||
"xargs",
|
||||
"arguments",
|
||||
"bare-os",
|
||||
"coreutils",
|
||||
"stub"
|
||||
"coreutils"
|
||||
],
|
||||
"stub": true,
|
||||
"bareOsNotes": "No process fork model; see handbook ch.9.",
|
||||
"bareOsNotes": "No host fork; subset of POSIX/GNU xargs. See src/xargs.js for numeric limits.",
|
||||
"examples": [
|
||||
{
|
||||
"caption": "workaround: shell word split",
|
||||
"caption": "pass lines as arguments",
|
||||
"code": "printf 'a\\nb\\n' | xargs echo"
|
||||
},
|
||||
{
|
||||
"caption": "one argument per run",
|
||||
"code": "printf 'a\\nb\\n' | xargs -n1 echo"
|
||||
},
|
||||
{
|
||||
"caption": "workaround for complex scripts",
|
||||
"code": "# for f in *.txt; do grep -l foo $f; done"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -11,7 +11,7 @@ import { COREUTILS_COMMANDS, MAN_EXTRA_PAGES } from '../lib/commands.mjs'
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const pagesDir = join(__dirname, '../man/pages')
|
||||
|
||||
const STUB = new Set(['chgrp', 'chown', 'xargs', 'getconf', 'mkfifo'])
|
||||
const STUB = new Set(['chgrp', 'chown', 'mkfifo'])
|
||||
|
||||
const POSIX_TITLE = {
|
||||
awk: 'pattern scanning and processing language',
|
||||
@@ -184,7 +184,8 @@ EXAMPLES.find = [
|
||||
{ caption: 'OR names', code: 'find . \\( -name "*.c" -o -name "*.h" \\)' }
|
||||
]
|
||||
EXAMPLES.getconf = [
|
||||
{ caption: 'stub', code: '# getconf PATH_MAX — not available on Bare OS' }
|
||||
{ caption: 'path length limit', code: 'getconf PATH_MAX' },
|
||||
{ caption: 'list known names and values', code: 'getconf -a' }
|
||||
]
|
||||
EXAMPLES.grep = [
|
||||
{ caption: 'recursive feel (grep each file)', code: 'grep -n error *.log' },
|
||||
@@ -327,7 +328,15 @@ EXAMPLES.which = [{ caption: 'resolve on PATH', code: 'which ls' }]
|
||||
EXAMPLES.whoami = [{ caption: 'effective user', code: 'whoami' }]
|
||||
EXAMPLES.xargs = [
|
||||
{
|
||||
caption: 'workaround: shell word split',
|
||||
caption: 'pass lines as arguments',
|
||||
code: "printf 'a\\nb\\n' | xargs echo"
|
||||
},
|
||||
{
|
||||
caption: 'one argument per run',
|
||||
code: "printf 'a\\nb\\n' | xargs -n1 echo"
|
||||
},
|
||||
{
|
||||
caption: 'workaround for complex scripts',
|
||||
code: '# for f in *.txt; do grep -l foo $f; done'
|
||||
}
|
||||
]
|
||||
@@ -349,13 +358,25 @@ EXTRA.chown = {
|
||||
}
|
||||
EXTRA.xargs = {
|
||||
description:
|
||||
'xargs does not spawn arbitrary /bin utilities on Bare OS. Use shell word splitting or pipelines.',
|
||||
bareOsNotes: 'No process fork model; see handbook ch.9.'
|
||||
'Reads stdin into argument batches and runs **`ctx.runBinCommand`** (same as the shell). Enforces stdin size, token count, batch size, and invocation limits for safety.',
|
||||
options: [
|
||||
{ flag: '-0, --null', meaning: 'Input items are null-terminated, not whitespace-separated' },
|
||||
{
|
||||
flag: '-n, --max-args',
|
||||
meaning: 'Up to N arguments per utility invocation (capped at 128)'
|
||||
}
|
||||
],
|
||||
bareOsNotes:
|
||||
'No host process spawn; not full POSIX xargs (no -I, -P, etc.). See src/xargs.js for limits.'
|
||||
}
|
||||
EXTRA.getconf = {
|
||||
description:
|
||||
'Host sysconf-style values are not exposed. The command prints an error.',
|
||||
bareOsNotes: 'Stub only; no kernel sysconf surface.'
|
||||
'Prints a fixed subset of configuration limits for Bare OS (JavaScript runtime and VFS). There is no host sysconf(3); values are documented constants, not live kernel queries.',
|
||||
options: [
|
||||
{ flag: '-a', meaning: 'Write all known variables (name then value per pair)' }
|
||||
],
|
||||
bareOsNotes:
|
||||
'Unknown variable names exit with status 1. Not a full Issue 7 getconf implementation.'
|
||||
}
|
||||
EXTRA.mkfifo = {
|
||||
description:
|
||||
|
||||
@@ -1,10 +1,65 @@
|
||||
/**
|
||||
* Subset of POSIX.1-2017 getconf — fixed values for Bare OS (no host sysconf).
|
||||
* Unknown names exit with status 1 (matches common getconf for invalid var).
|
||||
*/
|
||||
|
||||
const CONF = {
|
||||
PATH_MAX: '4096',
|
||||
NAME_MAX: '255',
|
||||
/** POSIX minimum for ARG_MAX; Bare uses a conservative cap for runBinCommand argv. */
|
||||
_POSIX_ARG_MAX: '4096',
|
||||
ARG_MAX: '262144',
|
||||
LINE_MAX: '2048',
|
||||
/** POSIX.1-2008 */
|
||||
_POSIX_VERSION: '200809',
|
||||
_POSIX2_VERSION: '200809',
|
||||
NGROUPS_MAX: '32',
|
||||
OPEN_MAX: '256',
|
||||
STREAM_MAX: '256',
|
||||
TZNAME_MAX: '32',
|
||||
_POSIX_CHOWN_RESTRICTED: '1',
|
||||
_POSIX_NO_TRUNC: '1',
|
||||
_POSIX_VDISABLE: '0',
|
||||
_POSIX_JOB_CONTROL: '0',
|
||||
_POSIX_SAVED_IDS: '0',
|
||||
/** Bare shell line length (reasonable REPL limit, not a hard kernel cap). */
|
||||
BARE_OS_INPUT_LINE_MAX: '8192'
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
const name = argv[1]
|
||||
const args = argv.slice(1).filter((a) => a !== '--')
|
||||
let dumpAll = false
|
||||
let name = null
|
||||
for (const a of args) {
|
||||
if (a === '-a') dumpAll = true
|
||||
else if (!a.startsWith('-')) name = a
|
||||
else {
|
||||
ctx.console.error('getconf: unknown option: ' + a)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (dumpAll) {
|
||||
for (const k of Object.keys(CONF).sort()) {
|
||||
ctx.console.log(k + '\n' + CONF[k])
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
ctx.console.error('usage: getconf VARIABLE_NAME')
|
||||
ctx.console.error('usage: getconf [-a] system_var')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
ctx.console.error('getconf: not implemented on Bare OS (no full sysconf path): ' + name)
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(CONF, name)) {
|
||||
ctx.console.log(CONF[name])
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
ctx.console.error('getconf: ' + name + ': unknown variable')
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
|
||||
@@ -1,6 +1,118 @@
|
||||
/**
|
||||
* Bounded xargs for Bare OS: invokes ctx.runBinCommand only (no host spawn).
|
||||
* Limits: stdin 256KiB, 4096 whitespace/null tokens, 128 args per invocation,
|
||||
* 64 invocations per run. Exceeding limits is a fatal error (exit 125).
|
||||
*/
|
||||
|
||||
const MAX_STDIN = 256 * 1024
|
||||
const MAX_TOKENS = 4096
|
||||
const MAX_PER_INVOCATION = 128
|
||||
const MAX_INVOCATIONS = 64
|
||||
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.error(
|
||||
'xargs: running arbitrary commands from /bin/xargs is not supported in Bare OS; use the shell to expand arguments.'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
if (typeof ctx.runBinCommand !== 'function') {
|
||||
ctx.console.error(
|
||||
'xargs: ctx.runBinCommand is not available (requires a Bare OS booter session)'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const args = argv.slice(1)
|
||||
let nullSep = false
|
||||
let maxBatch = MAX_PER_INVOCATION
|
||||
let i = 0
|
||||
|
||||
while (i < args.length && args[i].startsWith('-')) {
|
||||
const a = args[i]
|
||||
if (a === '--') {
|
||||
i++
|
||||
break
|
||||
}
|
||||
if (a === '-0' || a === '--null') {
|
||||
nullSep = true
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (a === '-n' || a === '--max-args') {
|
||||
const n = args[i + 1]
|
||||
if (n == null || !/^\d+$/.test(n) || Number(n) < 1) {
|
||||
ctx.console.error('xargs: -n requires a positive integer')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
maxBatch = Math.min(Number(n), MAX_PER_INVOCATION)
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (a.startsWith('-n') && a.length > 2 && /^\d+$/.test(a.slice(2))) {
|
||||
const n = Number(a.slice(2))
|
||||
maxBatch = Math.min(n, MAX_PER_INVOCATION)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
ctx.console.error('xargs: unsupported option: ' + a)
|
||||
ctx.console.error(
|
||||
'xargs: Bare OS supports: -0/--null, -n N (max ' +
|
||||
MAX_PER_INVOCATION +
|
||||
' per run)'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
let cmd = args.slice(i)
|
||||
if (cmd.length === 0) cmd = ['echo']
|
||||
|
||||
let text = bareStdin(ctx) || ''
|
||||
if (text.length > MAX_STDIN) {
|
||||
ctx.console.error(
|
||||
'xargs: stdin exceeds ' + MAX_STDIN + ' bytes (Bare OS limit)'
|
||||
)
|
||||
ctx.exitCode = 125
|
||||
return
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
let pieces
|
||||
if (nullSep) {
|
||||
pieces = text.split('\0').filter((s) => s.length > 0)
|
||||
} else {
|
||||
pieces = text.trim() === '' ? [] : text.trim().split(/\s+/).filter(Boolean)
|
||||
}
|
||||
|
||||
if (pieces.length > MAX_TOKENS) {
|
||||
ctx.console.error(
|
||||
'xargs: too many arguments (' + pieces.length + ' > ' + MAX_TOKENS + ')'
|
||||
)
|
||||
ctx.exitCode = 125
|
||||
return
|
||||
}
|
||||
|
||||
const runOne = async (batch) => {
|
||||
await ctx.runBinCommand(cmd.concat(batch))
|
||||
}
|
||||
|
||||
if (pieces.length === 0) {
|
||||
await runOne([])
|
||||
return
|
||||
}
|
||||
|
||||
let invocations = 0
|
||||
for (let o = 0; o < pieces.length; o += maxBatch) {
|
||||
if (++invocations > MAX_INVOCATIONS) {
|
||||
ctx.console.error(
|
||||
'xargs: exceeded ' + MAX_INVOCATIONS + ' invocations (Bare OS limit)'
|
||||
)
|
||||
ctx.exitCode = 125
|
||||
return
|
||||
}
|
||||
const batch = pieces.slice(o, o + maxBatch)
|
||||
await runOne(batch)
|
||||
const ec = Number(ctx.exitCode) || 0
|
||||
if (ec !== 0) {
|
||||
ctx.exitCode = ec
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,13 +60,68 @@ function barePosixBlocks(size) {
|
||||
return Math.ceil(Number(size) / 512) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Subset of POSIX.1-2017 getconf — fixed values for Bare OS (no host sysconf).
|
||||
* Unknown names exit with status 1 (matches common getconf for invalid var).
|
||||
*/
|
||||
|
||||
const CONF = {
|
||||
PATH_MAX: '4096',
|
||||
NAME_MAX: '255',
|
||||
/** POSIX minimum for ARG_MAX; Bare uses a conservative cap for runBinCommand argv. */
|
||||
_POSIX_ARG_MAX: '4096',
|
||||
ARG_MAX: '262144',
|
||||
LINE_MAX: '2048',
|
||||
/** POSIX.1-2008 */
|
||||
_POSIX_VERSION: '200809',
|
||||
_POSIX2_VERSION: '200809',
|
||||
NGROUPS_MAX: '32',
|
||||
OPEN_MAX: '256',
|
||||
STREAM_MAX: '256',
|
||||
TZNAME_MAX: '32',
|
||||
_POSIX_CHOWN_RESTRICTED: '1',
|
||||
_POSIX_NO_TRUNC: '1',
|
||||
_POSIX_VDISABLE: '0',
|
||||
_POSIX_JOB_CONTROL: '0',
|
||||
_POSIX_SAVED_IDS: '0',
|
||||
/** Bare shell line length (reasonable REPL limit, not a hard kernel cap). */
|
||||
BARE_OS_INPUT_LINE_MAX: '8192'
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
const name = argv[1]
|
||||
const args = argv.slice(1).filter((a) => a !== '--')
|
||||
let dumpAll = false
|
||||
let name = null
|
||||
for (const a of args) {
|
||||
if (a === '-a') dumpAll = true
|
||||
else if (!a.startsWith('-')) name = a
|
||||
else {
|
||||
ctx.console.error('getconf: unknown option: ' + a)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (dumpAll) {
|
||||
for (const k of Object.keys(CONF).sort()) {
|
||||
ctx.console.log(k + '\n' + CONF[k])
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
ctx.console.error('usage: getconf VARIABLE_NAME')
|
||||
ctx.console.error('usage: getconf [-a] system_var')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
ctx.console.error('getconf: not implemented on Bare OS (no full sysconf path): ' + name)
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(CONF, name)) {
|
||||
ctx.console.log(CONF[name])
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
ctx.console.error('getconf: ' + name + ': unknown variable')
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
|
||||
@@ -60,9 +60,121 @@ function barePosixBlocks(size) {
|
||||
return Math.ceil(Number(size) / 512) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounded xargs for Bare OS: invokes ctx.runBinCommand only (no host spawn).
|
||||
* Limits: stdin 256KiB, 4096 whitespace/null tokens, 128 args per invocation,
|
||||
* 64 invocations per run. Exceeding limits is a fatal error (exit 125).
|
||||
*/
|
||||
|
||||
const MAX_STDIN = 256 * 1024
|
||||
const MAX_TOKENS = 4096
|
||||
const MAX_PER_INVOCATION = 128
|
||||
const MAX_INVOCATIONS = 64
|
||||
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.error(
|
||||
'xargs: running arbitrary commands from /bin/xargs is not supported in Bare OS; use the shell to expand arguments.'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
if (typeof ctx.runBinCommand !== 'function') {
|
||||
ctx.console.error(
|
||||
'xargs: ctx.runBinCommand is not available (requires a Bare OS booter session)'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const args = argv.slice(1)
|
||||
let nullSep = false
|
||||
let maxBatch = MAX_PER_INVOCATION
|
||||
let i = 0
|
||||
|
||||
while (i < args.length && args[i].startsWith('-')) {
|
||||
const a = args[i]
|
||||
if (a === '--') {
|
||||
i++
|
||||
break
|
||||
}
|
||||
if (a === '-0' || a === '--null') {
|
||||
nullSep = true
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (a === '-n' || a === '--max-args') {
|
||||
const n = args[i + 1]
|
||||
if (n == null || !/^\d+$/.test(n) || Number(n) < 1) {
|
||||
ctx.console.error('xargs: -n requires a positive integer')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
maxBatch = Math.min(Number(n), MAX_PER_INVOCATION)
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (a.startsWith('-n') && a.length > 2 && /^\d+$/.test(a.slice(2))) {
|
||||
const n = Number(a.slice(2))
|
||||
maxBatch = Math.min(n, MAX_PER_INVOCATION)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
ctx.console.error('xargs: unsupported option: ' + a)
|
||||
ctx.console.error(
|
||||
'xargs: Bare OS supports: -0/--null, -n N (max ' +
|
||||
MAX_PER_INVOCATION +
|
||||
' per run)'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
let cmd = args.slice(i)
|
||||
if (cmd.length === 0) cmd = ['echo']
|
||||
|
||||
let text = bareStdin(ctx) || ''
|
||||
if (text.length > MAX_STDIN) {
|
||||
ctx.console.error(
|
||||
'xargs: stdin exceeds ' + MAX_STDIN + ' bytes (Bare OS limit)'
|
||||
)
|
||||
ctx.exitCode = 125
|
||||
return
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
let pieces
|
||||
if (nullSep) {
|
||||
pieces = text.split('\0').filter((s) => s.length > 0)
|
||||
} else {
|
||||
pieces = text.trim() === '' ? [] : text.trim().split(/\s+/).filter(Boolean)
|
||||
}
|
||||
|
||||
if (pieces.length > MAX_TOKENS) {
|
||||
ctx.console.error(
|
||||
'xargs: too many arguments (' + pieces.length + ' > ' + MAX_TOKENS + ')'
|
||||
)
|
||||
ctx.exitCode = 125
|
||||
return
|
||||
}
|
||||
|
||||
const runOne = async (batch) => {
|
||||
await ctx.runBinCommand(cmd.concat(batch))
|
||||
}
|
||||
|
||||
if (pieces.length === 0) {
|
||||
await runOne([])
|
||||
return
|
||||
}
|
||||
|
||||
let invocations = 0
|
||||
for (let o = 0; o < pieces.length; o += maxBatch) {
|
||||
if (++invocations > MAX_INVOCATIONS) {
|
||||
ctx.console.error(
|
||||
'xargs: exceeded ' + MAX_INVOCATIONS + ' invocations (Bare OS limit)'
|
||||
)
|
||||
ctx.exitCode = 125
|
||||
return
|
||||
}
|
||||
const batch = pieces.slice(o, o + maxBatch)
|
||||
await runOne(batch)
|
||||
const ec = Number(ctx.exitCode) || 0
|
||||
if (ec !== 0) {
|
||||
ctx.exitCode = ec
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user