/** * 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} 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} 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/.unit with [Unit] After=other-unit. User overrides: ~/.config/bare-init/units/.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 }