systemctl

This commit is contained in:
Raven Scott
2026-04-03 18:24:15 -04:00
parent ef9ef9058f
commit b727177842
23 changed files with 843 additions and 108 deletions
+42 -21
View File
@@ -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
})
+168 -58
View File
@@ -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/'
)
}
+69 -14
View File
@@ -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) {
+116
View File
@@ -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 -1
View File
@@ -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'
+1 -1
View File
@@ -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