181 lines
4.9 KiB
JavaScript
181 lines
4.9 KiB
JavaScript
/**
|
|
* Hyperdrive-resident kernel (staged as /boot/init.js).
|
|
* Loaded by the booter with an injected ctx object (trusted replication source).
|
|
*
|
|
* Boot order: /etc/os-release → /etc/motd → /etc/bare-os/rc → /etc/bare-os/rc.d/*
|
|
* (sorted; digit-prefixed snippet names) → session banner → interactive loop.
|
|
* Use ctx.registerKernelShutdownHook(fn) for teardown before initd disposers.
|
|
*/
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
*/
|
|
function wantBootTrace(ctx) {
|
|
const v = ctx.env && ctx.env.BARE_OS_BOOT_TRACE
|
|
return v === '1' || v === 'true'
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} label
|
|
* @param {() => void | Promise<void>} fn
|
|
*/
|
|
async function bootTimed(ctx, label, fn) {
|
|
const t0 = Date.now()
|
|
await fn()
|
|
if (wantBootTrace(ctx)) {
|
|
ctx.console.error(`[boot] ${label}: ${Date.now() - t0}ms`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} text
|
|
*/
|
|
async function runRcLines(ctx, text) {
|
|
const { execLine, console } = ctx
|
|
for (const line of text.split(/\r?\n/)) {
|
|
const t = line.trim()
|
|
if (!t || t.startsWith('#')) continue
|
|
try {
|
|
await execLine(t)
|
|
} catch (e) {
|
|
console.error((e && e.message) || String(e))
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
*/
|
|
async function printOsRelease(ctx) {
|
|
const { drive, b4a, console } = ctx
|
|
try {
|
|
const rel = await drive.get('/etc/os-release')
|
|
if (rel) console.log(b4a.toString(rel))
|
|
} catch (e) {
|
|
console.error((e && e.message) || String(e))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
*/
|
|
async function printMotd(ctx) {
|
|
const { drive, b4a, console } = ctx
|
|
try {
|
|
const motd = await drive.get('/etc/motd')
|
|
if (motd) console.log(b4a.toString(motd).trimEnd())
|
|
} catch (e) {
|
|
console.error((e && e.message) || String(e))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} drivePath absolute path on system drive
|
|
* @param {string} label for errors
|
|
*/
|
|
async function runRcFileAt(ctx, drivePath, label) {
|
|
const { drive, b4a, console } = ctx
|
|
try {
|
|
const buf = await drive.get(drivePath)
|
|
if (!buf) return
|
|
await runRcLines(ctx, b4a.toString(buf))
|
|
} catch (e) {
|
|
console.error(`${label}: ` + ((e && e.message) || String(e)))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Only run rc.d files whose name starts with a digit (e.g. `10-local`).
|
|
* Skips README*, *.md, dotfiles, and *~ so documentation is never exec'd as shell.
|
|
* @param {string} name basename from readdir
|
|
*/
|
|
function isBareOsRcSnippetFile(name) {
|
|
if (!name || name.startsWith('.') || name.endsWith('~')) return false
|
|
if (/^README(\.|$)/i.test(name)) return false
|
|
if (/\.md$/i.test(name)) return false
|
|
return /^[0-9]/.test(name)
|
|
}
|
|
|
|
/**
|
|
* Optional snippets under /etc/bare-os/rc.d/ — executed in lexicographic order.
|
|
* @param {Record<string, unknown>} ctx
|
|
*/
|
|
async function runBareOsRcDir(ctx) {
|
|
const { drive, b4a, console } = ctx
|
|
try {
|
|
/** @type {string[]} */
|
|
const names = []
|
|
try {
|
|
for await (const n of drive.readdir('/etc/bare-os/rc.d')) names.push(n)
|
|
} catch {
|
|
return
|
|
}
|
|
names.sort()
|
|
for (const name of names) {
|
|
if (!isBareOsRcSnippetFile(name)) continue
|
|
const p = `/etc/bare-os/rc.d/${name}`
|
|
try {
|
|
const buf = await drive.get(p)
|
|
if (!buf) continue
|
|
await runRcLines(ctx, b4a.toString(buf))
|
|
} catch (e) {
|
|
console.error(`rc.d/${name}: ` + ((e && e.message) || String(e)))
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error((e && e.message) || String(e))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
*/
|
|
async function printSessionBanner(ctx) {
|
|
const { drive, b4a, console } = ctx
|
|
for (const p of ['/etc/bare-os/banner', '/etc/issue']) {
|
|
try {
|
|
const buf = await drive.get(p)
|
|
if (buf) {
|
|
console.log(b4a.toString(buf).trimEnd())
|
|
return
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
const defaultBanner =
|
|
'Bare operating system — guest session (login [--new] <passphrase> to unlock) | shell: cd, export, if/fi, && || ;, |, exit | services: systemctl list-units, journalctl -u UNIT | try: help, ls /bin, crontab -l'
|
|
if (ctx.bareOsSkipRepl) {
|
|
console.log(
|
|
'Bare operating system — non-interactive session (BARE_OS_SKIP_REPL).'
|
|
)
|
|
return
|
|
}
|
|
console.log(defaultBanner)
|
|
}
|
|
|
|
async function start(ctx) {
|
|
const { readLine, execLine, console } = ctx
|
|
await bootTimed(ctx, 'os-release', () => printOsRelease(ctx))
|
|
await bootTimed(ctx, 'motd', () => printMotd(ctx))
|
|
await bootTimed(ctx, 'rc', () => runRcFileAt(ctx, '/etc/bare-os/rc', 'rc'))
|
|
await bootTimed(ctx, 'rc.d', () => runBareOsRcDir(ctx))
|
|
await printSessionBanner(ctx)
|
|
while (true) {
|
|
const line = await readLine('')
|
|
if (line == null) break
|
|
const t = line.trim()
|
|
if (t === '') continue
|
|
let status = 'ok'
|
|
try {
|
|
status = await execLine(t)
|
|
} catch (e) {
|
|
console.error((e && e.message) || String(e))
|
|
}
|
|
if (status === 'exit') break
|
|
}
|
|
}
|