Files
bare-operating-system/packages/bare-os-booter/lib/systemctl-cli.js
T
2026-04-21 23:19:38 -04:00

542 lines
15 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,
readInitdMaskedSet,
writeInitdDisabledSet,
writeInitdMaskedSet
} from './bare-initd-user.js'
import { getBareInitdJournalNdjson } from './bare-initd-journal.js'
/** Strip trailing `.service` (systemd-style) for bare-initd unit ids. */
function normalizeBareInitdUnitName(unit) {
const u = String(unit || '').trim()
return u.endsWith('.service') ? u.slice(0, -'.service'.length) : u
}
/** @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) {
const msg = e?.message || String(e)
if (/ENOENT/u.test(msg)) {
ctx.console.log(
'(log file not created yet — first ctx.console.log/error after login populates this path)'
)
} else {
ctx.console.error(`${errPrefix}: ` + msg)
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} mask|unmask UNIT
${prog} is-enabled UNIT
${prog} is-active UNIT
${prog} daemon-reload
${prog} preset (no-op; use enable/disable per unit)
${prog} help
Persistent preset: ~/.config/bare-os/initd/disabled.txt (personal drive).
Masked units: ~/.config/bare-os/initd/masked.txt (cannot start until unmask).
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 = normalizeBareInitdUnitName(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()
let masked = new Set()
if (ctx.vfs && typeof ctx.vfs.readFile === 'function') {
try {
disabled = await readInitdDisabledSet(ctx.vfs)
} catch {
disabled = new Set()
}
try {
masked = await readInitdMaskedSet(ctx.vfs)
} catch {
masked = 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 = masked.has(d.name)
? 'masked'
: 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 = normalizeBareInitdUnitName(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)
const mas = await readInitdMaskedSet(ctx.vfs)
if (mas.has(unit)) preset = 'masked'
else 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 = normalizeBareInitdUnitName(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 = normalizeBareInitdUnitName(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 = normalizeBareInitdUnitName(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 = normalizeBareInitdUnitName(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 === 'daemon-reload') {
ctx.console.log(
'daemon-reload: bare-initd unit registry is fixed for the session; no disk reload required (exit 0).'
)
ctx.exitCode = 0
return
}
if (sub === 'preset') {
ctx.console.log(
'preset: no vendor preset files in Bare OS — use systemctl enable|disable UNIT.'
)
ctx.exitCode = 0
return
}
if (sub === 'mask' || sub === 'unmask') {
const unit = normalizeBareInitdUnitName(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 mask/unmask`)
ctx.exitCode = 1
return
}
try {
const masked = await readInitdMaskedSet(ctx.vfs)
const dis = await readInitdDisabledSet(ctx.vfs)
if (sub === 'mask') {
masked.add(unit)
dis.add(unit)
await writeInitdMaskedSet(ctx.vfs, masked)
await writeInitdDisabledSet(ctx.vfs, dis)
ctx.console.log(
`Masked ${unit} (also disabled for future boots; see ~/.config/bare-os/initd/masked.txt)`
)
} else {
masked.delete(unit)
await writeInitdMaskedSet(ctx.vfs, masked)
ctx.console.log(`Unmasked ${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 = normalizeBareInitdUnitName(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 = normalizeBareInitdUnitName(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)
const mas = await readInitdMaskedSet(ctx.vfs)
if (mas.has(unit)) {
ctx.console.log('masked')
ctx.exitCode = 1
} else 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 = normalizeBareInitdUnitName(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
}