vendored Bare bundles and seeder parity; tighten the no-`node:` verifier and host-vs-image docs; expand syscall/proc JSON with POSIX XSH-style ops, errno hints, signals, and schema updates; add simulated FD table hooks, pathconf/getconf coverage, IPC message-queue surface, swarm/protomux pool metrics and HyperDHT stats env; extend systemctl verbs, cron TZ-aware matching, Pear updater and corestore snapshot hints, WASM instantiate behind a gate, and runBin default timeout merging with caller abort options. Bump BARE_OS_CTX_API_VERSION to 1.34.0 and BARE_OS_POSIX_PROFILE_VERSION to 1.0.1; sync declared profile, compliance matrix, handbook, example syscalls JSON, CHANGELOG, and generated ctx client helper; extend bare-os-ctx.d.ts for new ctx members.
449 lines
12 KiB
JavaScript
449 lines
12 KiB
JavaScript
/**
|
|
* systemctl-compatible CLI for bare-initd units. Invoked from kernel-runner delegation.
|
|
*/
|
|
|
|
import b4a from 'b4a'
|
|
import { INITD_LOG } from './bare-os-var-log.js'
|
|
import {
|
|
findBareServiceDefinition,
|
|
getBareServiceRuntime,
|
|
listBareServices,
|
|
restartBareService,
|
|
startBareService,
|
|
stopBareService
|
|
} from './bare-initd.js'
|
|
import {
|
|
readInitdDisabledSet,
|
|
writeInitdDisabledSet
|
|
} from './bare-initd-user.js'
|
|
import { getBareInitdJournalNdjson } from './bare-initd-journal.js'
|
|
|
|
/** @param {Uint8Array | null} buf @param {number} maxLines */
|
|
function tailUtf8Lines(buf, maxLines) {
|
|
if (!buf || !buf.length) return ''
|
|
const text = b4a.toString(buf, 'utf8')
|
|
const lines = text.split(/\r?\n/)
|
|
if (lines.length <= maxLines) return text.trimEnd()
|
|
return lines.slice(-maxLines).join('\n')
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} [logPath]
|
|
* @param {number} lines
|
|
* @param {string} [errPrefix]
|
|
*/
|
|
async function printLogTail(ctx, logPath, lines, errPrefix = 'systemctl') {
|
|
const vfs = ctx.vfs
|
|
if (!logPath) {
|
|
ctx.console.log(
|
|
'(no dedicated log file for this unit; see ' +
|
|
INITD_LOG +
|
|
' for initd errors)'
|
|
)
|
|
return
|
|
}
|
|
if (!vfs || typeof vfs.readFile !== 'function') {
|
|
ctx.console.error(`${errPrefix}: vfs unavailable`)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
try {
|
|
const buf = await vfs.readFile(logPath)
|
|
const tail = tailUtf8Lines(buf, lines)
|
|
if (tail) ctx.console.log(tail)
|
|
else ctx.console.log('(empty)')
|
|
} catch (e) {
|
|
ctx.console.error(`${errPrefix}: ` + (e?.message || String(e)))
|
|
ctx.exitCode = 1
|
|
}
|
|
}
|
|
|
|
function printHelp(ctx, prog) {
|
|
ctx.console.log(
|
|
`Usage: ${prog} list|list-units
|
|
${prog} status [UNIT] [--lines N]
|
|
${prog} show|cat UNIT
|
|
${prog} is-failed UNIT
|
|
${prog} reset-failed [UNIT]
|
|
${prog} logs UNIT [--lines N]
|
|
${prog} start|stop|restart UNIT
|
|
${prog} enable|disable UNIT
|
|
${prog} is-enabled UNIT
|
|
${prog} is-active UNIT
|
|
${prog} help
|
|
|
|
Persistent preset: ~/.config/bare-os/initd/disabled.txt (personal drive).
|
|
Optional ~/.config/bare-os/units/<name>.unit with [Unit] After=other-unit.
|
|
User overrides: ~/.config/bare-init/units/<name>.unit (merged; systemctl --user style).
|
|
journalctl: journalctl -u UNIT [--lines N] (alias for logs)`
|
|
)
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} argv argv[0] is systemctl | bare-initctl (alias) | journalctl
|
|
*/
|
|
export async function runSystemctlCli(ctx, argv) {
|
|
const raw = argv[0] || 'systemctl'
|
|
const prog = 'systemctl'
|
|
const args = argv.slice(1)
|
|
|
|
if (raw === 'journalctl') {
|
|
let unit = ''
|
|
let lines = 40
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === '-u' && args[i + 1]) {
|
|
unit = args[++i]
|
|
} else if (args[i] === '--lines' && args[i + 1]) {
|
|
lines = Math.max(1, Number.parseInt(args[++i], 10) || 40)
|
|
} else if (args[i] === '-n' && args[i + 1]) {
|
|
lines = Math.max(1, Number.parseInt(args[++i], 10) || 40)
|
|
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
printHelp(ctx, 'journalctl -u UNIT')
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
}
|
|
if (!unit) {
|
|
ctx.console.error('journalctl: usage: journalctl -u UNIT [--lines N]')
|
|
ctx.exitCode = 2
|
|
return
|
|
}
|
|
const def = findBareServiceDefinition(unit)
|
|
if (!def) {
|
|
ctx.console.error(`journalctl: unknown unit: ${unit}`)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
await printLogTail(ctx, def.logPath, lines, 'journalctl')
|
|
const jtext = getBareInitdJournalNdjson(unit)
|
|
if (jtext.trim()) {
|
|
const jl = jtext.trimEnd().split(/\r?\n/)
|
|
const tailJ = jl.slice(-lines).join('\n')
|
|
ctx.console.log('--- unit journal (NDJSON tail) ---')
|
|
ctx.console.log(tailJ)
|
|
}
|
|
ctx.exitCode = ctx.exitCode ?? 0
|
|
return
|
|
}
|
|
|
|
/** @type {string} */
|
|
let sub
|
|
/** @type {string[]} */
|
|
let rest
|
|
|
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
printHelp(ctx, prog)
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
sub = args[0]
|
|
rest = args.slice(1)
|
|
if (sub === 'list-units') sub = 'list'
|
|
|
|
if (sub === 'help') {
|
|
printHelp(ctx, prog)
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
|
|
if (sub === 'list') {
|
|
const defs = listBareServices()
|
|
let disabled = new Set()
|
|
if (ctx.vfs && typeof ctx.vfs.readFile === 'function') {
|
|
try {
|
|
disabled = await readInitdDisabledSet(ctx.vfs)
|
|
} catch {
|
|
disabled = new Set()
|
|
}
|
|
}
|
|
ctx.console.log(
|
|
'UNIT LOAD PRESET ACTIVE SUB DESCRIPTION'
|
|
)
|
|
for (const d of defs) {
|
|
const rt = getBareServiceRuntime(d.name)
|
|
const active = !rt
|
|
? '—'
|
|
: rt.phase === 'active'
|
|
? 'active'
|
|
: rt.phase === 'failed'
|
|
? 'failed'
|
|
: rt.phase === 'inactive'
|
|
? 'inactive'
|
|
: 'unknown'
|
|
const subState = !rt
|
|
? 'n/a'
|
|
: rt.phase === 'failed'
|
|
? 'failed'
|
|
: rt.phase === 'active'
|
|
? 'running'
|
|
: rt.phase === 'inactive'
|
|
? 'dead'
|
|
: 'n/a'
|
|
const load = 'static'
|
|
const preset = disabled.has(d.name) ? 'disabled' : 'enabled'
|
|
const namePad = (d.name + ' '.repeat(22)).slice(0, 22)
|
|
const desc = d.description || '—'
|
|
ctx.console.log(
|
|
`${namePad} ${load.padEnd(6)} ${preset.padEnd(8)} ${active.padEnd(7)} ${subState.padEnd(12)} ${desc}`
|
|
)
|
|
}
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
|
|
if (sub === 'status') {
|
|
const unit = rest[0]
|
|
let lines = 20
|
|
for (let i = 1; i < rest.length; i++) {
|
|
if (rest[i] === '--lines' && rest[i + 1]) {
|
|
lines = Math.max(1, Number.parseInt(rest[++i], 10) || 20)
|
|
}
|
|
}
|
|
if (!unit) {
|
|
ctx.console.error(`${prog}: status requires a UNIT`)
|
|
ctx.exitCode = 2
|
|
return
|
|
}
|
|
const def = findBareServiceDefinition(unit)
|
|
if (!def) {
|
|
ctx.console.error(`${prog}: unknown unit: ${unit}`)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
const rt = getBareServiceRuntime(unit)
|
|
let preset = 'enabled'
|
|
if (ctx.vfs && typeof ctx.vfs.readFile === 'function') {
|
|
try {
|
|
const dis = await readInitdDisabledSet(ctx.vfs)
|
|
if (dis.has(unit)) preset = 'disabled'
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
ctx.console.log(`● ${unit}`)
|
|
ctx.console.log(` Description: ${def.description || '(none)'}`)
|
|
ctx.console.log(` Load: static`)
|
|
ctx.console.log(` Preset: ${preset}`)
|
|
ctx.console.log(
|
|
` Active: ${rt?.phase || 'unknown'}${rt?.error ? ` (${rt.error})` : ''}`
|
|
)
|
|
if (rt?.startedAtMs) {
|
|
ctx.console.log(` Since: ${new Date(rt.startedAtMs).toISOString()}`)
|
|
}
|
|
ctx.console.log(
|
|
` Stop supported: ${typeof def.stop === 'function' ? 'yes' : 'no'}`
|
|
)
|
|
if (def.logPath) ctx.console.log(` Log: ${def.logPath}`)
|
|
else ctx.console.log(` Log: (none; initd aggregate: ${INITD_LOG})`)
|
|
ctx.console.log('')
|
|
ctx.console.log('--- log tail ---')
|
|
await printLogTail(ctx, def.logPath, lines)
|
|
ctx.exitCode = ctx.exitCode ?? 0
|
|
return
|
|
}
|
|
|
|
if (sub === 'show' || sub === 'cat') {
|
|
const unit = rest[0]
|
|
if (!unit) {
|
|
ctx.console.error(`${prog}: ${sub} requires a UNIT`)
|
|
ctx.exitCode = 2
|
|
return
|
|
}
|
|
const def = findBareServiceDefinition(unit)
|
|
if (!def) {
|
|
ctx.console.error(`${prog}: unknown unit: ${unit}`)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
const rt = getBareServiceRuntime(unit)
|
|
ctx.console.log(`Id=${unit}`)
|
|
ctx.console.log(`Description=${def.description || ''}`)
|
|
ctx.console.log(`LoadState=loaded`)
|
|
ctx.console.log(`ActiveState=${rt?.phase || 'unknown'}`)
|
|
if (def.logPath) ctx.console.log(`LogPath=${def.logPath}`)
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
|
|
if (sub === 'is-failed') {
|
|
const unit = rest[0]
|
|
if (!unit) {
|
|
ctx.console.error(`${prog}: is-failed requires a UNIT`)
|
|
ctx.exitCode = 2
|
|
return
|
|
}
|
|
const def = findBareServiceDefinition(unit)
|
|
if (!def) {
|
|
ctx.console.error(`${prog}: unknown unit: ${unit}`)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
const rt = getBareServiceRuntime(unit)
|
|
if (rt?.phase === 'failed') {
|
|
ctx.console.log('failed')
|
|
ctx.exitCode = 0
|
|
} else {
|
|
ctx.console.log('active')
|
|
ctx.exitCode = 1
|
|
}
|
|
return
|
|
}
|
|
|
|
if (sub === 'reset-failed') {
|
|
ctx.console.log(
|
|
'reset-failed: no persistent failed state in stock bare-initd (logical no-op).'
|
|
)
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
|
|
if (sub === 'logs') {
|
|
const unit = rest[0]
|
|
let lines = 40
|
|
for (let i = 1; i < rest.length; i++) {
|
|
if (rest[i] === '--lines' && rest[i + 1]) {
|
|
lines = Math.max(1, Number.parseInt(rest[++i], 10) || 40)
|
|
}
|
|
}
|
|
if (!unit) {
|
|
ctx.console.error(`${prog}: logs requires a UNIT`)
|
|
ctx.exitCode = 2
|
|
return
|
|
}
|
|
const def = findBareServiceDefinition(unit)
|
|
if (!def) {
|
|
ctx.console.error(`${prog}: unknown unit: ${unit}`)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
await printLogTail(ctx, def.logPath, lines)
|
|
ctx.exitCode = ctx.exitCode ?? 0
|
|
return
|
|
}
|
|
|
|
if (sub === 'start' || sub === 'stop' || sub === 'restart') {
|
|
const unit = rest[0]
|
|
if (!unit) {
|
|
ctx.console.error(`${prog}: ${sub} requires a UNIT`)
|
|
ctx.exitCode = 2
|
|
return
|
|
}
|
|
try {
|
|
if (sub === 'start') {
|
|
const r = await startBareService(ctx, unit)
|
|
if (r?.noop) ctx.console.log(r.message || `${unit} already active`)
|
|
else ctx.console.log(`Started ${unit}`)
|
|
} else if (sub === 'stop') {
|
|
await stopBareService(ctx, unit)
|
|
ctx.console.log(`Stopped ${unit}`)
|
|
} else {
|
|
await restartBareService(ctx, unit)
|
|
ctx.console.log(`Restarted ${unit}`)
|
|
}
|
|
ctx.exitCode = 0
|
|
} catch (e) {
|
|
ctx.console.error(`${prog}: ${e?.message || String(e)}`)
|
|
ctx.exitCode = 1
|
|
}
|
|
return
|
|
}
|
|
|
|
if (sub === 'enable' || sub === 'disable') {
|
|
const unit = rest[0]
|
|
if (!unit) {
|
|
ctx.console.error(`${prog}: ${sub} requires a UNIT`)
|
|
ctx.exitCode = 2
|
|
return
|
|
}
|
|
const def = findBareServiceDefinition(unit)
|
|
if (!def) {
|
|
ctx.console.error(`${prog}: unknown unit: ${unit}`)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') {
|
|
ctx.console.error(`${prog}: vfs required for enable/disable`)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
try {
|
|
const dis = await readInitdDisabledSet(ctx.vfs)
|
|
if (sub === 'disable') dis.add(unit)
|
|
else dis.delete(unit)
|
|
await writeInitdDisabledSet(ctx.vfs, dis)
|
|
ctx.console.log(
|
|
sub === 'disable'
|
|
? `Disabled ${unit} for future boots (see ~/.config/bare-os/initd/disabled.txt)`
|
|
: `Enabled ${unit} for future boots`
|
|
)
|
|
ctx.exitCode = 0
|
|
} catch (e) {
|
|
ctx.console.error(`${prog}: ${e?.message || String(e)}`)
|
|
ctx.exitCode = 1
|
|
}
|
|
return
|
|
}
|
|
|
|
if (sub === 'is-enabled') {
|
|
const unit = rest[0]
|
|
if (!unit) {
|
|
ctx.console.error(`${prog}: is-enabled requires a UNIT`)
|
|
ctx.exitCode = 2
|
|
return
|
|
}
|
|
const def = findBareServiceDefinition(unit)
|
|
if (!def) {
|
|
ctx.console.error(`${prog}: unknown unit: ${unit}`)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') {
|
|
ctx.console.error(`${prog}: vfs required for is-enabled`)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
try {
|
|
const dis = await readInitdDisabledSet(ctx.vfs)
|
|
if (dis.has(unit)) {
|
|
ctx.console.log('disabled')
|
|
ctx.exitCode = 1
|
|
} else {
|
|
ctx.console.log('enabled')
|
|
ctx.exitCode = 0
|
|
}
|
|
} catch (e) {
|
|
ctx.console.error(`${prog}: ${e?.message || String(e)}`)
|
|
ctx.exitCode = 1
|
|
}
|
|
return
|
|
}
|
|
|
|
if (sub === 'is-active') {
|
|
const unit = rest[0]
|
|
if (!unit) {
|
|
ctx.console.error(`${prog}: is-active requires a UNIT`)
|
|
ctx.exitCode = 2
|
|
return
|
|
}
|
|
const def = findBareServiceDefinition(unit)
|
|
if (!def) {
|
|
ctx.console.error(`${prog}: unknown unit: ${unit}`)
|
|
ctx.exitCode = 3
|
|
return
|
|
}
|
|
const rt = getBareServiceRuntime(unit)
|
|
if (rt?.phase === 'active') {
|
|
ctx.console.log('active')
|
|
ctx.exitCode = 0
|
|
} else {
|
|
ctx.console.log('inactive')
|
|
ctx.exitCode = 3
|
|
}
|
|
return
|
|
}
|
|
|
|
ctx.console.error(`${prog}: unknown command: ${sub}`)
|
|
ctx.exitCode = 2
|
|
}
|