boot splash

This commit is contained in:
Raven Scott
2026-04-03 01:45:43 -04:00
parent eca209b663
commit 4c8d50b1c9
4 changed files with 243 additions and 68 deletions
+69 -50
View File
@@ -9,11 +9,7 @@ import { SwarmDisk } from './lib/swarm-disk.js'
import { runKernelFromSource } from './lib/kernel-runner.js'
import { createVfs } from './lib/vfs.js'
import { execShellLine } from './lib/shell.js'
import {
packageRootDir,
defaultBootCorestorePath,
defaultLocalSeedCorestorePath
} from './lib/paths.js'
import { packageRootDir, defaultBootCorestorePath } from './lib/paths.js'
import {
createBareReadlineQuestion,
createStreamLineReader,
@@ -21,6 +17,7 @@ import {
} from './lib/cli-readline.js'
import { resolveStdio } from './lib/resolve-stdio.js'
import { createKernelReplSession } from './lib/repl-session.js'
import { createBootSplash } from './lib/boot-splash.js'
const _pkg = packageRootDir(import.meta.url)
@@ -32,7 +29,11 @@ const _pkg = packageRootDir(import.meta.url)
function exitHostProcess(code) {
const Bare = globalThis.Bare
if (Bare && typeof Bare.exit === 'function') {
Bare.exit(code)
/* Defer so Hyperdrive/swarm/native stdio teardown is not on the same stack as exit
* (avoids malloc "pointer being freed was not allocated" under Pear on macOS). */
const run = () => Bare.exit(code)
if (typeof setImmediate === 'function') setImmediate(run)
else Promise.resolve().then(run)
return
}
const p = globalThis.process
@@ -275,22 +276,28 @@ async function executeKernel(disk, store, swarm, initSource) {
return sessionExitCode
}
async function bootFromPeers(disk, store, swarm) {
const topic = topicKey()
console.log('Loading MBR from peers...')
/**
* @param {SwarmDisk} disk
* @param {import('corestore').default} store
* @param {import('hyperswarm').default} swarm
* @param {ReturnType<typeof createBootSplash>} splash
*/
async function bootFromPeers(disk, store, swarm, splash) {
splash.setPhase('Reading MBR from swarm…')
splash.log('Loading block 0 (MBR)')
const mbr = await disk.read(0)
const { keys } = parseMbr(mbr)
let initSource = null
for (const driveKey of keys) {
try {
console.log(
'Mounting drive',
b4a.toString(driveKey, 'hex').slice(0, 16) + '...'
)
const hex = b4a.toString(driveKey, 'hex')
splash.setPhase('Opening system Hyperdrive')
splash.log(`Drive key ${hex.slice(0, 16)}`)
disk.drive = new Hyperdrive(store, driveKey)
await disk.drive.ready()
splash.setPhase('Replicating with peers…')
for (const peer of disk.peers) {
disk.drive.replicate(peer.mux.stream, { live: true, download: true })
}
@@ -298,6 +305,7 @@ async function bootFromPeers(disk, store, swarm) {
const done = disk.drive.findingPeers()
swarm.flush().then(done, done)
splash.setPhase('Downloading /boot/init.js…')
for (let i = 0; i < 30; i++) {
initSource = await disk.drive.get('/boot/init.js')
if (initSource) break
@@ -305,40 +313,27 @@ async function bootFromPeers(disk, store, swarm) {
}
if (initSource) break
console.log('Kernel not ready on this drive key, trying next...')
splash.log('Kernel not on this key trying next MBR entry')
} catch (err) {
console.log('Drive error:', err.message)
splash.log(`Drive error: ${err?.message ?? err}`)
}
}
if (!initSource) throw new Error('Kernel not found after replication')
splash.setPhase('Mounting personal Hyperdrive…')
await disk.initPersonalDrive(store, swarm, Hyperdrive)
console.log('Starting kernel...')
return await executeKernel(disk, store, swarm, initSource)
}
async function bootLocal(disk, store, swarm) {
const seedPath = defaultLocalSeedCorestorePath(_pkg, import.meta.url)
console.log('Local boot from', seedPath)
const seedStore = new Corestore(seedPath)
disk.drive = new Hyperdrive(seedStore)
await disk.drive.ready()
let initSource = null
for (let i = 0; i < 10; i++) {
initSource = await disk.drive.get('/boot/init.js')
if (initSource) break
await new Promise((r) => setTimeout(r, 200))
}
if (!initSource) throw new Error('Kernel missing in local seed store')
await disk.initPersonalDrive(store, swarm, Hyperdrive)
splash.log('Personal drive ready')
splash.setPhase('Starting shell…')
splash.prepareForKernel()
return await executeKernel(disk, store, swarm, initSource)
}
async function main() {
console.log('--- bare-os-booter ---')
const bootLimitMs = Number(globalThis.process?.env?.BARE_OS_BOOT_TIMEOUT_MS) || 60_000
const { stdout } = await resolveStdio()
const splash = createBootSplash(stdout, { bootLimitMs })
splash.start()
const store = new Corestore(bootStorePath())
const swarm = new Hyperswarm()
@@ -352,39 +347,63 @@ async function main() {
swarm.join(topic)
const maxWait = Number(globalThis.process?.env?.BARE_OS_PEER_WAIT_MS || 8000)
let waited = 0
while (disk.peers.size === 0 && waited < maxWait) {
await new Promise((r) => setTimeout(r, 500))
waited += 500
const deadline = Date.now() + bootLimitMs
splash.setPhase('Waiting for swarm peers…')
while (disk.peers.size === 0 && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 400))
}
console.log('Peers:', disk.peers.size)
if (disk.peers.size === 0) {
throw new Error(
'No swarm peers within ' +
(bootLimitMs / 1000).toFixed(0) +
's. Run the seeder on the same topic, or raise BARE_OS_BOOT_TIMEOUT_MS.'
)
}
splash.log(`${disk.peers.size} peer(s) connected`)
splash.setPhase('Booting from network…')
let exitCode = 0
try {
const kernelExit =
disk.peers.size > 0
? await bootFromPeers(disk, store, swarm)
: await bootLocal(disk, store, swarm)
const msLeft = Math.max(1, deadline - Date.now())
const kernelExit = await Promise.race([
bootFromPeers(disk, store, swarm, splash),
new Promise((_, rej) =>
setTimeout(
() =>
rej(
new Error(
`Boot exceeded ${(bootLimitMs / 1000).toFixed(0)}s before kernel was ready`
)
),
msLeft
)
)
])
if (typeof kernelExit === 'number' && Number.isFinite(kernelExit)) {
exitCode = kernelExit
}
} catch (err) {
console.error('bare-os-booter failed:', err?.message ?? err)
const msg = err?.message ?? String(err)
splash.fail(msg)
console.error('bare-os-booter failed:', msg)
if (err?.stack) console.error(err.stack)
safetyCatch(err)
exitCode = 1
} finally {
/* Stop replication before closing drives — closing Hyperdrive while Protomux streams
* are still live can corrupt native heaps under Pear. */
try {
await swarm.destroy()
} catch (_) {}
try {
if (disk.personalDrive) await disk.personalDrive.close()
} catch (_) {}
try {
if (disk.drive) await disk.drive.close()
} catch (_) {}
try {
await swarm.destroy()
} catch (_) {}
try {
await store.close()
} catch (_) {}
+156
View File
@@ -0,0 +1,156 @@
/**
* TTY boot splash: timer, spinner, status lines, full-screen redraw.
* Disable with BARE_OS_NO_SPLASH=1 or when stdout is not a TTY (falls back to no-op).
*/
const CLEAR = '\x1b[3J\x1b[2J\x1b[H'
const HIDE_CURSOR = '\x1b[?25l'
const SHOW_CURSOR = '\x1b[?25h'
const RESET = '\x1b[0m'
const DIM = '\x1b[2m'
const BOLD = '\x1b[1m'
const GREEN = '\x1b[32m'
const MAGENTA = '\x1b[35m'
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
/** @returns {boolean} */
function splashDisabled() {
return globalThis.process?.env?.BARE_OS_NO_SPLASH === '1'
}
/**
* @param {import('stream').Writable | null | undefined} stdout
* @param {{ bootLimitMs?: number }} [opts]
*/
export function createBootSplash(stdout, opts = {}) {
const bootLimitMs = opts.bootLimitMs ?? 60_000
if (
splashDisabled() ||
!stdout ||
typeof stdout.write !== 'function' ||
!stdout.isTTY
) {
return {
start() {},
stop() {},
setPhase() {},
log() {},
prepareForKernel() {},
fail() {}
}
}
let t0 = 0
let frame = 0
/** @type {ReturnType<typeof setInterval> | null} */
let tick = null
let phase = 'Starting…'
/** @type {string[]} */
const lines = []
const maxLines = 8
function elapsedSec() {
return (Date.now() - t0) / 1000
}
function draw() {
const w = Math.max(40, Number(stdout.columns) || 72)
const el = elapsedSec()
const sp = SPINNER_FRAMES[frame % SPINNER_FRAMES.length]
frame++
const barW = Math.min(36, w - 8)
const progress = Math.min(1, el / (bootLimitMs / 1000))
const filled = Math.round(barW * progress)
const bar =
GREEN + '█'.repeat(filled) + DIM + '░'.repeat(Math.max(0, barW - filled)) + RESET
const hue = [36, 35, 34, 33, 32][frame % 5]
const title = `\x1b[${hue}m${BOLD} ◆ BARE-OS ◆${RESET}`
const sub = `${DIM} network boot · ${(bootLimitMs / 1000).toFixed(0)}s limit${RESET}`
const logBlock = lines.length
? lines.map((l) => ` ${DIM}${RESET} ${l}`).join('\n') + '\n'
: ''
const timeLine = ` ${BOLD}Boot${RESET} ${el.toFixed(1)}s ${sp} ${phase}`
const art = [
'',
title,
sub,
'',
` ${bar}`,
'',
logBlock,
timeLine,
''
].join('\n')
const out = CLEAR + HIDE_CURSOR + art
try {
stdout.write(out)
} catch {
/* ignore */
}
}
return {
start() {
t0 = Date.now()
frame = 0
phase = 'Initializing…'
lines.length = 0
if (tick) clearInterval(tick)
draw()
tick = setInterval(draw, 90)
},
stop() {
if (tick) {
clearInterval(tick)
tick = null
}
},
/** @param {string} msg */
setPhase(msg) {
phase = msg
},
/** @param {string} msg */
log(msg) {
lines.push(msg)
while (lines.length > maxLines) lines.shift()
},
/** Second clear before interactive shell / fish. */
prepareForKernel() {
this.stop()
try {
stdout.write(CLEAR + SHOW_CURSOR + RESET)
} catch {
/* ignore */
}
},
/** Show error then restore cursor. */
fail(msg) {
this.stop()
try {
stdout.write(
CLEAR +
SHOW_CURSOR +
MAGENTA +
msg +
RESET +
'\n'
)
} catch {
/* ignore */
}
}
}
}