systemctl
This commit is contained in:
@@ -4,6 +4,25 @@
|
||||
*/
|
||||
|
||||
import { registerBareInitdDisposer, registerBareService } from './bare-initd.js'
|
||||
import { appendVarLog, CRON_LOG } from './bare-os-var-log.js'
|
||||
|
||||
/** @type {ReturnType<typeof setTimeout> | null} */
|
||||
let cronTimeoutId = null
|
||||
/** @type {ReturnType<typeof setInterval> | null} */
|
||||
let cronIntervalId = null
|
||||
/** @type {Record<string, unknown> | null} */
|
||||
let cronExecCtx = null
|
||||
|
||||
export function stopBareCron() {
|
||||
if (cronTimeoutId != null) {
|
||||
clearTimeout(cronTimeoutId)
|
||||
cronTimeoutId = null
|
||||
}
|
||||
if (cronIntervalId != null) {
|
||||
clearInterval(cronIntervalId)
|
||||
cronIntervalId = null
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {Date} d */
|
||||
function msToNextMinuteBoundary(d = new Date()) {
|
||||
@@ -176,19 +195,22 @@ async function readUserCrontabText(ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Set<number>} */
|
||||
const cronRunningJobs = new Set()
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
function startBareCron(ctx) {
|
||||
/** @type {Set<number>} */
|
||||
const running = new Set()
|
||||
let timeoutId = null
|
||||
let intervalId = null
|
||||
stopBareCron()
|
||||
cronExecCtx = ctx
|
||||
|
||||
async function tick() {
|
||||
const c = cronExecCtx
|
||||
if (!c) return
|
||||
let text
|
||||
try {
|
||||
text = await readUserCrontabText(ctx)
|
||||
text = await readUserCrontabText(c)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
@@ -206,45 +228,44 @@ function startBareCron(ctx) {
|
||||
for (const job of jobs) {
|
||||
if (!jobMatchesDate(job, now)) continue
|
||||
const key = job.lineIndex
|
||||
if (running.has(key)) continue
|
||||
running.add(key)
|
||||
if (cronRunningJobs.has(key)) continue
|
||||
cronRunningJobs.add(key)
|
||||
void (async () => {
|
||||
try {
|
||||
const execLine = ctx.execLine
|
||||
const execLine = c.execLine
|
||||
if (typeof execLine !== 'function') return
|
||||
await execLine(job.command)
|
||||
} catch (e) {
|
||||
const msg = e?.message || String(e)
|
||||
try {
|
||||
ctx.console?.error?.(`[bare-cron] ${msg}`)
|
||||
c.console?.error?.(`[bare-cron] ${msg}`)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
void appendVarLog(c, CRON_LOG, 'error', msg)
|
||||
} finally {
|
||||
running.delete(key)
|
||||
cronRunningJobs.delete(key)
|
||||
}
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
registerBareInitdDisposer(() => {
|
||||
if (timeoutId != null) clearTimeout(timeoutId)
|
||||
if (intervalId != null) clearInterval(intervalId)
|
||||
timeoutId = null
|
||||
intervalId = null
|
||||
})
|
||||
|
||||
const delay = msToNextMinuteBoundary()
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null
|
||||
cronTimeoutId = setTimeout(() => {
|
||||
cronTimeoutId = null
|
||||
void tick()
|
||||
intervalId = setInterval(() => {
|
||||
cronIntervalId = setInterval(() => {
|
||||
void tick()
|
||||
}, 60000)
|
||||
}, delay)
|
||||
}
|
||||
|
||||
registerBareInitdDisposer(stopBareCron)
|
||||
|
||||
registerBareService({
|
||||
name: 'bare-cron',
|
||||
start: startBareCron
|
||||
description: 'User crontab scheduler (minute-aligned; ~/.crontab)',
|
||||
logPath: CRON_LOG,
|
||||
start: startBareCron,
|
||||
stop: stopBareCron
|
||||
})
|
||||
|
||||
@@ -3,17 +3,44 @@
|
||||
* Services start after the session console exists; failures are logged, not fatal.
|
||||
*/
|
||||
|
||||
/** @typedef {{ name: string, start: (ctx: Record<string, unknown>) => void | Promise<void> }} BareService */
|
||||
import {
|
||||
appendVarLog,
|
||||
ensureBareOsVarLogTree,
|
||||
INITD_LOG,
|
||||
KERNEL_CONSOLE_LOG
|
||||
} from './bare-os-var-log.js'
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* name: string,
|
||||
* description?: string,
|
||||
* logPath?: string,
|
||||
* start: (ctx: Record<string, unknown>) => void | Promise<void>,
|
||||
* stop?: (ctx: Record<string, unknown>) => void | Promise<void>
|
||||
* }} BareService
|
||||
*/
|
||||
|
||||
/** @typedef {{ phase: 'active'|'failed'|'inactive', startedAtMs: number, error?: string }} BareServiceRuntime */
|
||||
|
||||
/** @type {BareService[]} */
|
||||
const registry = []
|
||||
|
||||
/** @type {Map<string, BareServiceRuntime>} */
|
||||
const runtime = new Map()
|
||||
|
||||
/** @type {(() => void)[]} */
|
||||
const disposers = []
|
||||
|
||||
/** @type {(() => void | Promise<void>)[]} */
|
||||
const kernelShutdownHooks = []
|
||||
|
||||
/** Kernel logger: restore console on stop/restart */
|
||||
let kernelLoggerWrapped = false
|
||||
/** @type {((...args: unknown[]) => void) | null} */
|
||||
let kernelLoggerOrigLog = null
|
||||
/** @type {((...args: unknown[]) => void) | null} */
|
||||
let kernelLoggerOrigErr = null
|
||||
|
||||
/**
|
||||
* Register cleanup (e.g. clearInterval) for when the kernel session ends.
|
||||
* @param {() => void} fn
|
||||
@@ -64,90 +91,173 @@ export function registerBareService(service) {
|
||||
registry.push(service)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {BareService | undefined}
|
||||
*/
|
||||
export function findBareServiceDefinition(name) {
|
||||
return registry.find((s) => s.name === name)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{ name: string, description: string, logPath?: string, hasStop: boolean }[]}
|
||||
*/
|
||||
export function listBareServices() {
|
||||
return registry.map((s) => ({
|
||||
name: s.name,
|
||||
description: s.description || '',
|
||||
logPath: s.logPath,
|
||||
hasStop: typeof s.stop === 'function'
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {BareServiceRuntime | undefined}
|
||||
*/
|
||||
export function getBareServiceRuntime(name) {
|
||||
return runtime.get(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
export async function startBareService(ctx, name) {
|
||||
const s = findBareServiceDefinition(name)
|
||||
if (!s) throw new Error(`Unknown unit: ${name}`)
|
||||
const rt = runtime.get(name)
|
||||
if (rt?.phase === 'active') {
|
||||
return { noop: true, message: `${name} is already active` }
|
||||
}
|
||||
const t0 = Date.now()
|
||||
try {
|
||||
await s.start(ctx)
|
||||
runtime.set(name, { phase: 'active', startedAtMs: t0 })
|
||||
return { ok: true }
|
||||
} catch (e) {
|
||||
const msg = e?.message || String(e)
|
||||
runtime.set(name, { phase: 'failed', startedAtMs: t0, error: msg })
|
||||
try {
|
||||
ctx.console?.error?.(`[bare-initd] ${name}: ${msg}`)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
void appendVarLog(ctx, INITD_LOG, name, msg)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} name
|
||||
*/
|
||||
export async function stopBareService(ctx, name) {
|
||||
const s = findBareServiceDefinition(name)
|
||||
if (!s) throw new Error(`Unknown unit: ${name}`)
|
||||
if (typeof s.stop !== 'function') {
|
||||
throw new Error(`Unit ${name} does not support stop (no stop handler)`)
|
||||
}
|
||||
await s.stop(ctx)
|
||||
const prev = runtime.get(name)
|
||||
runtime.set(name, {
|
||||
phase: 'inactive',
|
||||
startedAtMs: prev?.startedAtMs ?? Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} name
|
||||
*/
|
||||
export async function restartBareService(ctx, name) {
|
||||
const s = findBareServiceDefinition(name)
|
||||
if (!s) throw new Error(`Unknown unit: ${name}`)
|
||||
if (typeof s.stop !== 'function') {
|
||||
throw new Error(
|
||||
`Unit ${name} does not support restart (no stop handler; session teardown only)`
|
||||
)
|
||||
}
|
||||
await s.stop(ctx)
|
||||
const t0 = Date.now()
|
||||
try {
|
||||
await s.start(ctx)
|
||||
runtime.set(name, { phase: 'active', startedAtMs: t0 })
|
||||
} catch (e) {
|
||||
const msg = e?.message || String(e)
|
||||
runtime.set(name, { phase: 'failed', startedAtMs: t0, error: msg })
|
||||
try {
|
||||
ctx.console?.error?.(`[bare-initd] ${name}: ${msg}`)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
void appendVarLog(ctx, INITD_LOG, name, msg)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
export async function startBareInitd(ctx) {
|
||||
runtime.clear()
|
||||
await ensureBareOsVarLogTree(ctx)
|
||||
for (const s of registry) {
|
||||
const t0 = Date.now()
|
||||
try {
|
||||
await s.start(ctx)
|
||||
runtime.set(s.name, { phase: 'active', startedAtMs: t0 })
|
||||
} catch (e) {
|
||||
const msg = e?.message || String(e)
|
||||
runtime.set(s.name, { phase: 'failed', startedAtMs: t0, error: msg })
|
||||
try {
|
||||
ctx.console?.error?.(`[bare-initd] ${s.name}: ${msg}`)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
void appendVarLog(ctx, INITD_LOG, s.name, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const KERNEL_LOG_REL = '.kernel/kernel.log'
|
||||
function stopKernelLogger(ctx) {
|
||||
const c = ctx.console
|
||||
if (!kernelLoggerWrapped || !c) return
|
||||
if (kernelLoggerOrigLog) c.log = kernelLoggerOrigLog
|
||||
if (kernelLoggerOrigErr) c.error = kernelLoggerOrigErr
|
||||
kernelLoggerWrapped = false
|
||||
kernelLoggerOrigLog = null
|
||||
kernelLoggerOrigErr = null
|
||||
}
|
||||
|
||||
/** Max size before trimming older log bytes (best-effort). */
|
||||
const KERNEL_LOG_MAX_BYTES = 512 * 1024
|
||||
function startKernelLogger(ctx) {
|
||||
const c = ctx.console
|
||||
if (!c || typeof c.log !== 'function') return
|
||||
if (kernelLoggerWrapped) return
|
||||
|
||||
/** After trim, keep this many trailing bytes plus a notice line. */
|
||||
const KERNEL_LOG_KEEP_BYTES = 256 * 1024
|
||||
kernelLoggerOrigLog = c.log.bind(c)
|
||||
kernelLoggerOrigErr =
|
||||
typeof c.error === 'function' ? c.error.bind(c) : kernelLoggerOrigLog
|
||||
|
||||
/**
|
||||
* Append one UTF-8 line to ~/.kernel/kernel.log (personal drive). Best-effort; never throws.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} kind
|
||||
* @param {string} line
|
||||
*/
|
||||
async function appendKernelLog(ctx, kind, line) {
|
||||
try {
|
||||
const vfs = ctx.vfs
|
||||
if (!vfs || typeof vfs.readFile !== 'function') return
|
||||
const home = vfs.env?.HOME || '/home/guest'
|
||||
const h = home.replace(/\/$/, '')
|
||||
const keepPath = `${h}/.kernel/.keep`
|
||||
const logPath = `${h}/${KERNEL_LOG_REL}`
|
||||
try {
|
||||
await vfs.writeFile(keepPath, ctx.b4a.from(''))
|
||||
} catch {
|
||||
/* exists */
|
||||
}
|
||||
const prev = await vfs.readFile(logPath)
|
||||
const ts = new Date().toISOString()
|
||||
const chunk = ctx.b4a.from(`[${ts}] [${kind}] ${line}\n`)
|
||||
let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk
|
||||
if (merged.length > KERNEL_LOG_MAX_BYTES) {
|
||||
const start = Math.max(0, merged.length - KERNEL_LOG_KEEP_BYTES)
|
||||
const tail = merged.subarray(start)
|
||||
const notice = ctx.b4a.from(
|
||||
`[${ts}] [bare-os] kernel.log truncated (kept last ${KERNEL_LOG_KEEP_BYTES} bytes)\n`
|
||||
)
|
||||
merged = ctx.b4a.concat([notice, tail])
|
||||
}
|
||||
await vfs.writeFile(logPath, merged)
|
||||
} catch {
|
||||
/* ignore */
|
||||
c.log = (...args) => {
|
||||
const text = args.map(String).join(' ')
|
||||
void appendVarLog(ctx, KERNEL_CONSOLE_LOG, 'log', text)
|
||||
return kernelLoggerOrigLog(...args)
|
||||
}
|
||||
c.error = (...args) => {
|
||||
const text = args.map(String).join(' ')
|
||||
void appendVarLog(ctx, KERNEL_CONSOLE_LOG, 'error', text)
|
||||
return kernelLoggerOrigErr(...args)
|
||||
}
|
||||
kernelLoggerWrapped = true
|
||||
}
|
||||
|
||||
function registerKernelLoggerService() {
|
||||
registerBareService({
|
||||
name: 'kernel-logger',
|
||||
start(ctx) {
|
||||
const c = ctx.console
|
||||
if (!c || typeof c.log !== 'function') return
|
||||
|
||||
const origLog = c.log.bind(c)
|
||||
const origErr = typeof c.error === 'function' ? c.error.bind(c) : origLog
|
||||
|
||||
c.log = (...args) => {
|
||||
const text = args.map(String).join(' ')
|
||||
void appendKernelLog(ctx, 'log', text)
|
||||
return origLog(...args)
|
||||
}
|
||||
c.error = (...args) => {
|
||||
const text = args.map(String).join(' ')
|
||||
void appendKernelLog(ctx, 'error', text)
|
||||
return origErr(...args)
|
||||
}
|
||||
}
|
||||
description: 'Mirrors console.log / console.error to /var/log/bare-os/kernel-console.log',
|
||||
logPath: KERNEL_CONSOLE_LOG,
|
||||
start: startKernelLogger,
|
||||
stop: stopKernelLogger
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Append-only UTF-8 logs under logical `/var/log/…` (VFS maps to the personal Hyperdrive).
|
||||
*/
|
||||
|
||||
export const BARE_OS_VAR_LOG_DIR = '/var/log/bare-os'
|
||||
|
||||
export const KERNEL_CONSOLE_LOG = `${BARE_OS_VAR_LOG_DIR}/kernel-console.log`
|
||||
|
||||
export const CRON_LOG = `${BARE_OS_VAR_LOG_DIR}/cron.log`
|
||||
|
||||
export const INITD_LOG = `${BARE_OS_VAR_LOG_DIR}/initd.log`
|
||||
|
||||
const README_REL = `${BARE_OS_VAR_LOG_DIR}/README`
|
||||
|
||||
const README_TEXT = `Bare OS session logs (mirrored on your personal drive under /.bare-os/var/log).
|
||||
kernel-console.log — console.log / console.error from the kernel session
|
||||
cron.log — bare-cron job errors
|
||||
initd.log — bare-initd service start failures
|
||||
`
|
||||
|
||||
/** Max size before trimming older log bytes (best-effort). */
|
||||
const LOG_MAX_BYTES = 512 * 1024
|
||||
|
||||
/** After trim, keep this many trailing bytes plus a notice line. */
|
||||
const LOG_KEEP_BYTES = 256 * 1024
|
||||
|
||||
/**
|
||||
* Ensure `/var/log/bare-os` exists and a short README is present. Best-effort; never throws.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
export async function ensureBareOsVarLogTree(ctx) {
|
||||
try {
|
||||
const vfs = ctx.vfs
|
||||
if (!vfs || typeof vfs.mkdir !== 'function') return
|
||||
await vfs.mkdir(BARE_OS_VAR_LOG_DIR, { recursive: true })
|
||||
if (typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') return
|
||||
const existing = await vfs.readFile(README_REL)
|
||||
if (existing && ctx.b4a.from(existing).length > 0) return
|
||||
await vfs.writeFile(README_REL, ctx.b4a.from(README_TEXT))
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append one UTF-8 line to a logical log file under `/var/log/…`. Best-effort; never throws.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} logicalFilePath e.g. `/var/log/bare-os/cron.log`
|
||||
* @param {string} kind
|
||||
* @param {string} line
|
||||
*/
|
||||
export async function appendVarLog(ctx, logicalFilePath, kind, line) {
|
||||
try {
|
||||
const vfs = ctx.vfs
|
||||
if (!vfs || typeof vfs.readFile !== 'function') return
|
||||
const prev = await vfs.readFile(logicalFilePath)
|
||||
const ts = new Date().toISOString()
|
||||
const chunk = ctx.b4a.from(`[${ts}] [${kind}] ${line}\n`)
|
||||
let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk
|
||||
if (merged.length > LOG_MAX_BYTES) {
|
||||
const start = Math.max(0, merged.length - LOG_KEEP_BYTES)
|
||||
const tail = merged.subarray(start)
|
||||
const notice = ctx.b4a.from(
|
||||
`[${ts}] [bare-os] log truncated (kept last ${LOG_KEEP_BYTES} bytes): ${logicalFilePath}\n`
|
||||
)
|
||||
merged = ctx.b4a.concat([notice, tail])
|
||||
}
|
||||
await vfs.writeFile(logicalFilePath, merged)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,15 @@ function shouldDelegateWget(cmd) {
|
||||
return true
|
||||
}
|
||||
|
||||
function shouldDelegateSystemctl(cmd) {
|
||||
const base = cmd.includes('/') ? path.posix.basename(cmd) : cmd
|
||||
if (base === 'systemctl' || base === 'bare-initctl' || base === 'journalctl')
|
||||
return true
|
||||
if (!cmd.includes('/')) return false
|
||||
if (cmd.startsWith('./') || cmd.startsWith('../')) return false
|
||||
return 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
|
||||
@@ -112,6 +121,11 @@ export async function runBinCommand(ctx, argv) {
|
||||
return runWgetCli(ctx, argv)
|
||||
}
|
||||
|
||||
if (shouldDelegateSystemctl(cmd)) {
|
||||
const { runSystemctlCli } = await import('./systemctl-cli.js')
|
||||
return runSystemctlCli(ctx, argv)
|
||||
}
|
||||
|
||||
if (cmd.includes('/')) {
|
||||
const abs = vfs.resolveLogical(cmd)
|
||||
const { drive, path } = vfs.route(abs)
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* systemctl-compatible CLI for bare-initd units. Invoked from kernel-runner delegation.
|
||||
*/
|
||||
|
||||
import { INITD_LOG } from './bare-os-var-log.js'
|
||||
import {
|
||||
findBareServiceDefinition,
|
||||
getBareServiceRuntime,
|
||||
listBareServices,
|
||||
restartBareService,
|
||||
startBareService,
|
||||
stopBareService
|
||||
} from './bare-initd.js'
|
||||
|
||||
/** @param {Uint8Array | null} buf @param {number} maxLines */
|
||||
function tailUtf8Lines(buf, maxLines) {
|
||||
if (!buf || !buf.length) return ''
|
||||
const dec = new TextDecoder('utf-8', { fatal: false })
|
||||
const text = dec.decode(buf)
|
||||
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} logs UNIT [--lines N]
|
||||
${prog} start|stop|restart UNIT
|
||||
${prog} help
|
||||
|
||||
Session-scoped bare-initd units. No enable/disable (not persistent).
|
||||
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')
|
||||
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()
|
||||
ctx.console.log(
|
||||
'UNIT LOAD 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 namePad = (d.name + ' '.repeat(22)).slice(0, 22)
|
||||
const desc = d.description || '—'
|
||||
ctx.console.log(
|
||||
`${namePad} ${load.padEnd(6)} ${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)
|
||||
ctx.console.log(`● ${unit}`)
|
||||
ctx.console.log(` Description: ${def.description || '(none)'}`)
|
||||
ctx.console.log(` Load: static`)
|
||||
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 === '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
|
||||
}
|
||||
|
||||
ctx.console.error(`${prog}: unknown command: ${sub}`)
|
||||
ctx.exitCode = 2
|
||||
}
|
||||
@@ -175,10 +175,10 @@ export function newBareOsForSymlink(env) {
|
||||
|
||||
/**
|
||||
* @param {import('hyperdrive').default} personalDrive
|
||||
* @param {{ drive: import('hyperdrive').default, virtualHomeDir?: boolean, virtualMntRoot?: boolean }} r
|
||||
* @param {{ drive: import('hyperdrive').default, virtualHomeDir?: boolean, virtualMntRoot?: boolean, virtualVarRoot?: boolean }} r
|
||||
*/
|
||||
export function isPersonalRoute(personalDrive, r) {
|
||||
if (r.virtualHomeDir || r.virtualMntRoot) return false
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) return false
|
||||
return r.drive === personalDrive
|
||||
}
|
||||
|
||||
@@ -314,8 +314,10 @@ export function isVirtualMountPoint(abs) {
|
||||
abs === '/' ||
|
||||
abs === '/home' ||
|
||||
abs === '/mnt' ||
|
||||
abs === '/var' ||
|
||||
abs === '/home/' ||
|
||||
abs === '/mnt/'
|
||||
abs === '/mnt/' ||
|
||||
abs === '/var/'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,13 @@ import {
|
||||
/** Empty-directory marker (must match {@link ./git-fs-adapter.js}). */
|
||||
const DIR_MARKER = '.bareos_empty'
|
||||
|
||||
/** Personal-drive backing for logical `/var/log/…` (system drive is read-only). */
|
||||
const VAR_LOG_STORAGE = '/.bare-os/var/log'
|
||||
|
||||
/**
|
||||
* Unified path view: system Hyperdrive for OS paths, personal Hyperdrive under $HOME,
|
||||
* optional HDMS mounts under /mnt/<label>/…
|
||||
* optional HDMS mounts under /mnt/<label>/…, virtual /var with writable /var/log/…
|
||||
* on the personal drive at /.bare-os/var/log/…
|
||||
* @param {import('hyperdrive').default} systemDrive
|
||||
* @param {import('hyperdrive').default} personalDrive
|
||||
* @param {Record<string, string>} env
|
||||
@@ -99,6 +103,22 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
const mntR = routeMnt(absPath)
|
||||
if (mntR) return mntR
|
||||
|
||||
const varNorm = absPath.replace(/\/+$/, '') || '/'
|
||||
if (varNorm === '/var') {
|
||||
return { virtualVarRoot: true }
|
||||
}
|
||||
if (varNorm === '/var/log' || absPath === '/var/log/') {
|
||||
return { drive: personalDrive, path: VAR_LOG_STORAGE }
|
||||
}
|
||||
if (absPath.startsWith('/var/log/')) {
|
||||
const rel = absPath.slice('/var/log/'.length).replace(/^\/+/, '')
|
||||
const p = rel ? unixPathResolve(VAR_LOG_STORAGE, rel) : VAR_LOG_STORAGE
|
||||
return { drive: personalDrive, path: p }
|
||||
}
|
||||
if (absPath.startsWith('/var/')) {
|
||||
return { drive: systemDrive, path: absPath }
|
||||
}
|
||||
|
||||
const h = normalizeHome()
|
||||
const activeSeg = activeHomeBasename()
|
||||
|
||||
@@ -145,7 +165,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async function isRegularFile(absPath) {
|
||||
if (absPath === '/') return false
|
||||
const r = route(absPath)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot) return false
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) return false
|
||||
const { drive, path: p } = r
|
||||
if (isHyperdriveRootPath(p)) return false
|
||||
const e = await entryOn(drive, p, { follow: true })
|
||||
@@ -228,6 +248,9 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
if (r.virtualMntRoot) {
|
||||
return { ...synthesizeStat(abs, false, env, 'directory'), path: abs }
|
||||
}
|
||||
if (r.virtualVarRoot) {
|
||||
return { ...synthesizeStat(abs, true, env, 'directory'), path: abs }
|
||||
}
|
||||
const { drive, path: p } = r
|
||||
const personal = isPersonalRoute(personalDrive, r)
|
||||
if (isHyperdriveRootPath(p)) {
|
||||
@@ -250,28 +273,46 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
return { ...synthesizeStat(abs, personal, env, 'directory'), path: abs }
|
||||
}
|
||||
if (e) return { ...synthesizeStat(abs, personal, env, 'directory'), path: abs }
|
||||
const normLog = abs.replace(/\/+$/, '') || '/'
|
||||
if (
|
||||
normLog === '/var/log' &&
|
||||
r.drive === personalDrive &&
|
||||
p === VAR_LOG_STORAGE
|
||||
) {
|
||||
return { ...synthesizeStat(abs, true, env, 'directory'), path: abs }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function statFromAbs(abs) {
|
||||
const normVar = abs.replace(/\/+$/, '') || '/'
|
||||
if (
|
||||
abs === '/mnt' ||
|
||||
abs === '/mnt/' ||
|
||||
abs === '/home' ||
|
||||
abs === '/' ||
|
||||
normVar === '/var' ||
|
||||
normVar === '/var/log' ||
|
||||
(activeHomeBasename() && abs === '/home')
|
||||
) {
|
||||
return lstatFromAbs(abs)
|
||||
}
|
||||
const r0 = route(abs)
|
||||
if (r0.virtualMntRoot) return lstatFromAbs(abs)
|
||||
if (r0.virtualMntRoot || r0.virtualVarRoot) return lstatFromAbs(abs)
|
||||
let cur = abs
|
||||
for (let depth = 0; depth < 16; depth++) {
|
||||
if (cur === '/' || cur === '/home' || cur === '/mnt') {
|
||||
const curNorm = cur.replace(/\/+$/, '') || '/'
|
||||
if (
|
||||
cur === '/' ||
|
||||
cur === '/home' ||
|
||||
cur === '/mnt' ||
|
||||
curNorm === '/var' ||
|
||||
curNorm === '/var/log'
|
||||
) {
|
||||
return lstatFromAbs(cur)
|
||||
}
|
||||
const r = route(cur)
|
||||
if (r.virtualMntRoot) return lstatFromAbs(cur)
|
||||
if (r.virtualMntRoot || r.virtualVarRoot) return lstatFromAbs(cur)
|
||||
const { drive, path: p } = r
|
||||
if (isHyperdriveRootPath(p)) return lstatFromAbs(cur)
|
||||
const e = await entryOn(drive, p, { follow: false })
|
||||
@@ -394,6 +435,17 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
if (activeSeg && abs === '/home') {
|
||||
return [activeSeg].sort()
|
||||
}
|
||||
if (abs === '/var' || abs === '/var/') {
|
||||
const names = new Set()
|
||||
try {
|
||||
const stream = systemDrive.readdir('/var')
|
||||
for await (const n of stream) names.add(n)
|
||||
} catch {
|
||||
/* no /var on system image */
|
||||
}
|
||||
names.add('log')
|
||||
return [...names].sort()
|
||||
}
|
||||
const r = route(abs)
|
||||
if (r.virtualMntRoot) {
|
||||
return [...getMntMap().keys()].sort()
|
||||
@@ -411,12 +463,15 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
if (abs === '/' && !names.includes('mnt')) {
|
||||
names.push('mnt')
|
||||
}
|
||||
if (abs === '/' && !names.includes('var')) {
|
||||
names.push('var')
|
||||
}
|
||||
return names.sort()
|
||||
}
|
||||
|
||||
async function delFromAbs(abs) {
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot) {
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
throw new Error('Read-only path (not under $HOME): ' + abs)
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
@@ -443,7 +498,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
*/
|
||||
async function rmFromAbs(abs, { recursive = false, force = false } = {}) {
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot) {
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
if (force) return
|
||||
throw new Error('Read-only path (not under $HOME): ' + abs)
|
||||
}
|
||||
@@ -477,7 +532,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
*/
|
||||
async function writeFileAtAbs(abs, buf, opts = {}) {
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot) {
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
throw new Error('Read-only path (not under $HOME): ' + abs)
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
@@ -539,7 +594,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async readFile(userPath) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot) return null
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) return null
|
||||
const { drive, path: p } = r
|
||||
if (isHyperdriveRootPath(p)) return null
|
||||
await assertTraverseTo(abs, 'read')
|
||||
@@ -553,7 +608,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async unlink(userPath) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot) {
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
throw new Error('Read-only path (not under $HOME): ' + userPath)
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
@@ -593,7 +648,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async exists(userPath) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot) return true
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) return true
|
||||
const { drive, path: p } = r
|
||||
if (isHyperdriveRootPath(p)) return true
|
||||
return drive.exists(p)
|
||||
@@ -619,7 +674,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async chmod(userPath, modeOctal) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot) {
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
throw new Error('chmod: ' + userPath + ': Operation not supported')
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
@@ -750,7 +805,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async readlink(userPath) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot) {
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
throw new Error('EINVAL readlink')
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
@@ -770,7 +825,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
async symlink(target, userPath) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot) {
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot) {
|
||||
throw new Error('Read-only path (not under $HOME): ' + userPath)
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
registerBareInitdDisposer,
|
||||
registerKernelShutdownHook,
|
||||
runKernelShutdownHooks,
|
||||
startBareInitd,
|
||||
stopBareInitd
|
||||
} from './lib/bare-initd.js'
|
||||
import {
|
||||
@@ -446,6 +447,50 @@ test('vfs /mnt lists HDMS mounts and allows writable put', async (t) => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('vfs /var in root readdir; /var/log empty lstat; writes map to personal', async (t) => {
|
||||
const dir = testCorestoreDir('vfsvar')
|
||||
const store = new Corestore(dir)
|
||||
const sys = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pvvar'))
|
||||
await sys.ready()
|
||||
await personal.ready()
|
||||
const env = { HOME: '/home/u', PWD: '/home/u', PATH: '/bin' }
|
||||
const vfs = createVfs(sys, personal, env)
|
||||
const root = await vfs.readdir('/')
|
||||
t.ok(root.includes('var'))
|
||||
t.alike(await vfs.readdir('/var'), ['log'])
|
||||
const stLog = await vfs.lstat('/var/log')
|
||||
t.ok(stLog)
|
||||
t.is(stLog.type, 'directory')
|
||||
await vfs.mkdir('/var/log/bare-os', { recursive: true })
|
||||
await vfs.writeFile('/var/log/bare-os/test-vfs.log', b4a.from('hello'))
|
||||
const buf = await personal.get('/.bare-os/var/log/bare-os/test-vfs.log')
|
||||
t.ok(buf)
|
||||
t.is(b4a.toString(buf), 'hello')
|
||||
const sysTry = await sys.get('/.bare-os/var/log/bare-os/test-vfs.log')
|
||||
t.is(sysTry, null)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('vfs chdir /var/log from home', async (t) => {
|
||||
const dir = testCorestoreDir('vfsvarcd')
|
||||
const store = new Corestore(dir)
|
||||
const sys = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pvcv'))
|
||||
await sys.ready()
|
||||
await personal.ready()
|
||||
const vfs = createVfs(sys, personal, {
|
||||
HOME: '/home/u',
|
||||
PWD: '/home/u',
|
||||
PATH: '/bin'
|
||||
})
|
||||
await vfs.chdir('/var/log')
|
||||
t.is(vfs.getcwd(), '/var/log')
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('tokenize handles quotes and ops', async (t) => {
|
||||
const tok = tokenize('ls -la | cat > out')
|
||||
t.ok(tok.some((x) => x.type === 'op' && x.value === '|'))
|
||||
@@ -725,6 +770,77 @@ test('cron parseCronLine and jobMatchesDate', async (t) => {
|
||||
t.ok(!jobMatchesDate(j, new Date(2020, 5, 15, 14, 31, 0)))
|
||||
})
|
||||
|
||||
test('systemctl list after startBareInitd shows kernel-logger and bare-cron', async (t) => {
|
||||
const dir = testCorestoreDir('initctl')
|
||||
const store = new Corestore(dir)
|
||||
const sys = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pinit'))
|
||||
await sys.ready()
|
||||
await personal.ready()
|
||||
const logs = []
|
||||
const ctx = testCtx(sys, personal)
|
||||
ctx.execLine = async () => {}
|
||||
await startBareInitd(ctx)
|
||||
ctx.console = {
|
||||
log: (...a) => logs.push(a.join(' ')),
|
||||
error: (...a) => logs.push(a.join(' '))
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['systemctl', 'list'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
const text = logs.join('\n')
|
||||
t.ok(text.includes('kernel-logger'))
|
||||
t.ok(text.includes('bare-cron'))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('systemctl list-units delegates to same backend', async (t) => {
|
||||
const dir = testCorestoreDir('initctlsys')
|
||||
const store = new Corestore(dir)
|
||||
const sys = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pics'))
|
||||
await sys.ready()
|
||||
await personal.ready()
|
||||
const logs = []
|
||||
const ctx = testCtx(sys, personal)
|
||||
ctx.execLine = async () => {}
|
||||
await startBareInitd(ctx)
|
||||
ctx.console = {
|
||||
log: (...a) => logs.push(a.join(' ')),
|
||||
error: (...a) => logs.push(a.join(' '))
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['systemctl', 'list-units'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(logs.join('\n').includes('bare-cron'))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('systemctl restart bare-cron succeeds', async (t) => {
|
||||
const dir = testCorestoreDir('initctlrst')
|
||||
const store = new Corestore(dir)
|
||||
const sys = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('picr'))
|
||||
await sys.ready()
|
||||
await personal.ready()
|
||||
const logs = []
|
||||
const ctx = testCtx(sys, personal)
|
||||
ctx.execLine = async () => {}
|
||||
await startBareInitd(ctx)
|
||||
ctx.console = {
|
||||
log: (...a) => logs.push(a.join(' ')),
|
||||
error: (...a) => logs.push(a.join(' '))
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['systemctl', 'restart', 'bare-cron'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(logs.some((l) => /restarted bare-cron/i.test(l)))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('stopBareInitd runs registered disposers', async (t) => {
|
||||
let n = 0
|
||||
registerBareInitdDisposer(() => {
|
||||
|
||||
@@ -72,6 +72,7 @@ export const COREUTILS_COMMANDS = [
|
||||
/** Extra manual pages not built as /bin scripts on the system drive. */
|
||||
export const MAN_EXTRA_PAGES = [
|
||||
'bare-os-shell',
|
||||
'systemctl',
|
||||
'curl',
|
||||
'git',
|
||||
'wget'
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "systemctl",
|
||||
"section": 1,
|
||||
"title": "bare-initd service control (systemd-like subset)",
|
||||
"synopsis": [
|
||||
"systemctl list|list-units",
|
||||
"systemctl status [UNIT] [--lines N]",
|
||||
"systemctl logs UNIT [--lines N]",
|
||||
"systemctl start|stop|restart UNIT",
|
||||
"journalctl -u UNIT [--lines N]"
|
||||
],
|
||||
"description": "Lists and manages session-scoped bare-initd units (kernel-logger, bare-cron, …). Implemented by the booter (kernel-runner); /bin stubs exist for PATH and man(1). Units are not persistent: there is no enable/disable. Logs live under /var/log/bare-os/ when the unit defines a logPath. The legacy name bare-initctl is still accepted by the booter as an alias.",
|
||||
"options": [
|
||||
{
|
||||
"flag": "--lines N",
|
||||
"meaning": "Tail N lines from the unit log (status, logs, journalctl)"
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"bare-initctl"
|
||||
],
|
||||
"keywords": [
|
||||
"bare-initd",
|
||||
"initctl",
|
||||
"service",
|
||||
"supervisor",
|
||||
"cron",
|
||||
"systemd"
|
||||
],
|
||||
"bareOsNotes": "journalctl only supports -u UNIT and optional --lines / -n. Unknown systemd verbs are not implemented.",
|
||||
"seeAlso": [
|
||||
{
|
||||
"name": "crontab",
|
||||
"section": 1
|
||||
},
|
||||
{
|
||||
"name": "bare-os-shell",
|
||||
"section": 1
|
||||
}
|
||||
],
|
||||
"examples": [
|
||||
{
|
||||
"caption": "list units",
|
||||
"code": "systemctl list-units"
|
||||
},
|
||||
{
|
||||
"caption": "restart scheduler",
|
||||
"code": "systemctl restart bare-cron"
|
||||
},
|
||||
{
|
||||
"caption": "tail cron errors",
|
||||
"code": "journalctl -u bare-cron --lines 20"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.log(
|
||||
'Bare OS — default user: guest | shell builtins: alias, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab curl cut date dirname du echo env exit false find getconf grep head hdms help hostname id jq ln login logout logname ls man mkdir mkfifo mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat tail tee test time touch tr true tty uname wc wget which whoami xargs'
|
||||
'Bare OS — default user: guest | shell builtins: alias, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab curl cut date dirname du echo env exit false find getconf grep head hdms help hostname id journalctl jq ln login logout logname ls man mkdir mkfifo mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat systemctl tail tee test time touch tr true tty uname wc wget which whoami xargs'
|
||||
)
|
||||
ctx.console.log(
|
||||
'Docs: man <command> | man handbook (full handbook, section 7) | man bare-os-shell | man -k <word> | man -l'
|
||||
|
||||
@@ -62,7 +62,7 @@ function barePosixBlocks(size) {
|
||||
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.log(
|
||||
'Bare OS — default user: guest | shell builtins: alias, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab curl cut date dirname du echo env exit false find getconf grep head hdms help hostname id jq ln login logout logname ls man mkdir mkfifo mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat tail tee test time touch tr true tty uname wc wget which whoami xargs'
|
||||
'Bare OS — default user: guest | shell builtins: alias, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab curl cut date dirname du echo env exit false find getconf grep head hdms help hostname id journalctl jq ln login logout logname ls man mkdir mkfifo mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat systemctl tail tee test time touch tr true tty uname wc wget which whoami xargs'
|
||||
)
|
||||
ctx.console.log(
|
||||
'Docs: man <command> | man handbook (full handbook, section 7) | man bare-os-shell | man -k <word> | man -l'
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/** Booter delegates journalctl to systemctl-cli.js (kernel-runner). */
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.error(
|
||||
'journalctl: not executed in host booter (delegation missing)'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/** Booter delegates systemctl to systemctl-cli.js (kernel-runner). */
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.error(
|
||||
'systemctl: not executed in host booter (delegation missing)'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user