boot splash
This commit is contained in:
@@ -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 (_) {}
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user