+2
-2
@@ -344,7 +344,7 @@ Pear-safe path resolution (same idea as Holepunch [pear-rti](https://github.com/
|
||||
**`main()`**
|
||||
|
||||
- `Corestore(bootStorePath())`, `Hyperswarm`, `SwarmDisk`, join `topicKey()`.
|
||||
- Wait loop: 500ms steps until `disk.peers.size > 0` or `BARE_OS_PEER_WAIT_MS` (default 8000).
|
||||
- Wait loop: 500ms steps until `disk.peers.size > 0` or `BARE_OS_PEER_WAIT_MS` (default 60000, same as boot budget).
|
||||
- If peers: `bootFromPeers`; else `bootLocal`.
|
||||
- **`finally`**: close `personalDrive`, `disk.drive`, `swarm.destroy()`, `store.close()` (each in try/catch).
|
||||
|
||||
@@ -467,7 +467,7 @@ sequenceDiagram
|
||||
| `BARE_OS_SEED_STORE` | Seeder | Corestore directory (default: `repo/data/corestore-seeder`) |
|
||||
| `BARE_OS_BOOT_STORE` | Booter | Corestore for boot side (default: `repo/data/corestore-booter`) |
|
||||
| `BARE_OS_LOCAL_SEED` | Booter | Corestore path for local boot (default: `repo/data/corestore-seeder`) |
|
||||
| `BARE_OS_PEER_WAIT_MS` | Booter | Max ms to wait for ≥1 peer before local boot (default `8000`) |
|
||||
| `BARE_OS_PEER_WAIT_MS` | Booter | Max ms to wait for ≥1 peer before local boot (default `60000`) |
|
||||
| `BARE_OS_SKIP_REPL` | Booter | If `1`, readline returns null — non-interactive exit |
|
||||
| `BARE_OS_FISH` | Booter | If `0`, disable Fish-style raw TTY line editor (use classic readline) |
|
||||
| `BARE_OS_FISH_HISTORY_MAX` | Booter / fish-readline | Max history entries persisted to `/.bare_history` (default `1000`) |
|
||||
|
||||
@@ -58,7 +58,7 @@ Runs workspace tests (`brittle-node` / `brittle-bare` where configured).
|
||||
|
||||
Optional:
|
||||
|
||||
- `BARE_OS_PEER_WAIT_MS` — ms to wait for peers (default `8000`).
|
||||
- `BARE_OS_PEER_WAIT_MS` — ms to wait for peers before local seed (default `60000`).
|
||||
- `BARE_OS_LOCAL_SEED` — Corestore path for **local boot** when no peers (default `./data/corestore-seeder`).
|
||||
- `BARE_OS_SKIP_REPL=1` — non-interactive kernel (CI / automation).
|
||||
|
||||
|
||||
@@ -21,6 +21,12 @@ import {
|
||||
} from './lib/cli-readline.js'
|
||||
import { resolveStdio } from './lib/resolve-stdio.js'
|
||||
import { createFishReadLine, disableFishRawMode } from './lib/fish-readline.js'
|
||||
import {
|
||||
BOOT_TIMEOUT_MESSAGE,
|
||||
BOOT_TIMEOUT_MS,
|
||||
sleepReject,
|
||||
startBootSplash
|
||||
} from './lib/boot-splash.js'
|
||||
|
||||
const _pkg = packageRootDir(import.meta.url)
|
||||
|
||||
@@ -28,6 +34,23 @@ function bootStorePath() {
|
||||
return defaultBootCorestorePath(_pkg, import.meta.url)
|
||||
}
|
||||
|
||||
/** Kernel / replication phase budget (after peer wait). */
|
||||
function parseLoadTimeoutMs() {
|
||||
const raw = globalThis.process?.env?.BARE_OS_BOOT_TIMEOUT_MS
|
||||
const n = Number(raw)
|
||||
if (Number.isFinite(n) && n > 0) return Math.min(Math.trunc(n), 600_000)
|
||||
return BOOT_TIMEOUT_MS
|
||||
}
|
||||
|
||||
/** Max ms to wait for first peer before falling back to local seed (default 1 min). */
|
||||
function parsePeerWaitMs() {
|
||||
const raw = globalThis.process?.env?.BARE_OS_PEER_WAIT_MS
|
||||
if (raw === undefined || raw === '') return BOOT_TIMEOUT_MS
|
||||
const n = Number(raw)
|
||||
if (!Number.isFinite(n) || n < 0) return BOOT_TIMEOUT_MS
|
||||
return Math.min(Math.trunc(n), 600_000)
|
||||
}
|
||||
|
||||
/** @returns {Promise<{ readLine: (p: string) => Promise<string | null>, interactiveAvailable: boolean, skipInteractive: boolean, stdout: import('stream').Writable | null, stdin: import('stream').Readable | null }>} */
|
||||
async function createReadLine() {
|
||||
const { stdin, stdout } = await resolveStdio()
|
||||
@@ -148,6 +171,15 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
globalThis.console.clear?.()
|
||||
}
|
||||
|
||||
if (
|
||||
interactiveAvailable &&
|
||||
!skipInteractive &&
|
||||
sessionStdout &&
|
||||
typeof sessionStdout.write === 'function'
|
||||
) {
|
||||
writeScreen('\x1b[H\x1b[2J\x1b[3J')
|
||||
}
|
||||
|
||||
const shellEnv = {
|
||||
HOME: '/home/user',
|
||||
PATH: '/bin',
|
||||
@@ -229,16 +261,26 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
}
|
||||
}
|
||||
|
||||
async function bootFromPeers(disk, store, swarm) {
|
||||
/**
|
||||
* @param {SwarmDisk} disk
|
||||
* @param {import('corestore').default} store
|
||||
* @param {import('hyperswarm').default} swarm
|
||||
* @param {{ silent?: boolean }} [opts]
|
||||
* @returns {Promise<Uint8Array>}
|
||||
*/
|
||||
async function bootFromPeers(disk, store, swarm, opts = {}) {
|
||||
const { silent = false } = opts
|
||||
const log = silent ? () => {} : console.log.bind(console)
|
||||
|
||||
const topic = topicKey()
|
||||
console.log('Loading MBR from peers...')
|
||||
log('Loading MBR from peers...')
|
||||
const mbr = await disk.read(0)
|
||||
const { keys } = parseMbr(mbr)
|
||||
|
||||
let initSource = null
|
||||
for (const driveKey of keys) {
|
||||
try {
|
||||
console.log(
|
||||
log(
|
||||
'Mounting drive',
|
||||
b4a.toString(driveKey, 'hex').slice(0, 16) + '...'
|
||||
)
|
||||
@@ -259,22 +301,32 @@ async function bootFromPeers(disk, store, swarm) {
|
||||
}
|
||||
|
||||
if (initSource) break
|
||||
console.log('Kernel not ready on this drive key, trying next...')
|
||||
log('Kernel not ready on this drive key, trying next...')
|
||||
} catch (err) {
|
||||
console.log('Drive error:', err.message)
|
||||
log('Drive error:', err.message)
|
||||
}
|
||||
}
|
||||
|
||||
if (!initSource) throw new Error('Kernel not found after replication')
|
||||
|
||||
await disk.initPersonalDrive(store, swarm, Hyperdrive)
|
||||
console.log('Starting kernel...')
|
||||
await executeKernel(disk, store, swarm, initSource)
|
||||
log('Starting kernel...')
|
||||
return initSource
|
||||
}
|
||||
|
||||
async function bootLocal(disk, store, swarm) {
|
||||
/**
|
||||
* @param {SwarmDisk} disk
|
||||
* @param {import('corestore').default} store
|
||||
* @param {import('hyperswarm').default} swarm
|
||||
* @param {{ silent?: boolean }} [opts]
|
||||
* @returns {Promise<Uint8Array>}
|
||||
*/
|
||||
async function bootLocal(disk, store, swarm, opts = {}) {
|
||||
const { silent = false } = opts
|
||||
const log = silent ? () => {} : console.log.bind(console)
|
||||
|
||||
const seedPath = defaultLocalSeedCorestorePath(_pkg, import.meta.url)
|
||||
console.log('Local boot from', seedPath)
|
||||
log('Local boot from', seedPath)
|
||||
const seedStore = new Corestore(seedPath)
|
||||
disk.drive = new Hyperdrive(seedStore)
|
||||
await disk.drive.ready()
|
||||
@@ -288,17 +340,30 @@ async function bootLocal(disk, store, swarm) {
|
||||
if (!initSource) throw new Error('Kernel missing in local seed store')
|
||||
|
||||
await disk.initPersonalDrive(store, swarm, Hyperdrive)
|
||||
await executeKernel(disk, store, swarm, initSource)
|
||||
return initSource
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('--- bare-os-booter ---')
|
||||
|
||||
const { stdout } = await resolveStdio()
|
||||
const useSplash = Boolean(stdout && typeof stdout.write === 'function')
|
||||
const silentBoot = useSplash && stdout.isTTY === true
|
||||
|
||||
const store = new Corestore(bootStorePath())
|
||||
const swarm = new Hyperswarm()
|
||||
const disk = new SwarmDisk()
|
||||
const topic = topicKey()
|
||||
|
||||
/** @type {string} */
|
||||
let bootStatus = 'joining swarm'
|
||||
const stopSplash = useSplash
|
||||
? startBootSplash(stdout, {
|
||||
peerCount: () => disk.peers.size,
|
||||
status: () => bootStatus
|
||||
})
|
||||
: () => {}
|
||||
|
||||
swarm.on('connection', (socket) => {
|
||||
const mux = new Protomux(socket)
|
||||
disk.addPeer(mux, socket)
|
||||
@@ -306,21 +371,50 @@ 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
|
||||
}
|
||||
|
||||
console.log('Peers:', disk.peers.size)
|
||||
const loadTimeoutMs = parseLoadTimeoutMs()
|
||||
const peerWaitMs = parsePeerWaitMs()
|
||||
/** Room for full peer wait plus replication/local load without racing too early. */
|
||||
const totalBootRaceMs = Math.min(peerWaitMs + loadTimeoutMs, 900_000)
|
||||
|
||||
try {
|
||||
if (disk.peers.size > 0) {
|
||||
await bootFromPeers(disk, store, swarm)
|
||||
} else {
|
||||
await bootLocal(disk, store, swarm)
|
||||
const initSource = await Promise.race([
|
||||
(async () => {
|
||||
let waited = 0
|
||||
bootStatus = 'waiting for peers'
|
||||
while (disk.peers.size === 0 && waited < peerWaitMs) {
|
||||
await new Promise((r) => setTimeout(r, 500))
|
||||
waited += 500
|
||||
}
|
||||
|
||||
if (!silentBoot) {
|
||||
console.log('Peers:', disk.peers.size)
|
||||
}
|
||||
|
||||
bootStatus =
|
||||
disk.peers.size > 0 ? 'replicating kernel' : 'loading local seed'
|
||||
if (disk.peers.size > 0) {
|
||||
return await bootFromPeers(disk, store, swarm, {
|
||||
silent: silentBoot
|
||||
})
|
||||
}
|
||||
return await bootLocal(disk, store, swarm, { silent: silentBoot })
|
||||
})(),
|
||||
sleepReject(totalBootRaceMs, () => {
|
||||
stopSplash()
|
||||
})
|
||||
])
|
||||
stopSplash()
|
||||
await executeKernel(disk, store, swarm, initSource)
|
||||
} catch (err) {
|
||||
stopSplash()
|
||||
if (err && err.message === BOOT_TIMEOUT_MESSAGE) {
|
||||
console.error('bare-os-booter:', err.message)
|
||||
if (typeof globalThis.process?.exit === 'function') {
|
||||
globalThis.process.exitCode = 1
|
||||
}
|
||||
return
|
||||
}
|
||||
throw err
|
||||
} finally {
|
||||
try {
|
||||
if (disk.personalDrive) await disk.personalDrive.close()
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/** Default boot attempt timeout (ms). */
|
||||
export const BOOT_TIMEOUT_MS = 60_000
|
||||
|
||||
export const BOOT_TIMEOUT_MESSAGE = 'Boot timed out'
|
||||
|
||||
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
||||
|
||||
/**
|
||||
* @param {import('stream').Writable | null | undefined} stdout
|
||||
* @param {{ peerCount?: () => number, status?: () => string }} opts
|
||||
* @returns {() => void} stop — clears the line and restores cursor visibility
|
||||
*/
|
||||
export function startBootSplash(stdout, opts = {}) {
|
||||
const start = Date.now()
|
||||
let frame = 0
|
||||
let stopped = false
|
||||
|
||||
function tick() {
|
||||
if (stopped || !stdout?.write) return
|
||||
const elapsedMs = Date.now() - start
|
||||
const totalSec = Math.floor(elapsedMs / 1000)
|
||||
const m = Math.floor(totalSec / 60)
|
||||
const s = totalSec % 60
|
||||
const clock = `${m}:${s.toString().padStart(2, '0')}`
|
||||
const spin = SPINNER[frame++ % SPINNER.length]
|
||||
let extra = ''
|
||||
try {
|
||||
const n = opts.peerCount?.()
|
||||
if (typeof n === 'number') extra += ` peers ${n}`
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
const st = opts.status?.()
|
||||
if (st) extra += ` · ${st}`
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
stdout.write(
|
||||
`\r\x1b[K\x1b[36m${spin}\x1b[0m \x1b[1mbare-os\x1b[0m booting \x1b[33m${clock}\x1b[0m${extra}`
|
||||
)
|
||||
}
|
||||
|
||||
if (stdout?.write) stdout.write('\x1b[?25l')
|
||||
tick()
|
||||
const id = setInterval(tick, 100)
|
||||
|
||||
return function stopBootSplash() {
|
||||
if (stopped) return
|
||||
stopped = true
|
||||
clearInterval(id)
|
||||
if (stdout?.write) {
|
||||
stdout.write('\r\x1b[K')
|
||||
stdout.write('\x1b[?25h')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function bootTimedOutError() {
|
||||
return new Error(BOOT_TIMEOUT_MESSAGE)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} ms
|
||||
* @param {() => void} [onTimeout]
|
||||
*/
|
||||
export function sleepReject(ms, onTimeout) {
|
||||
return new Promise((_, reject) => {
|
||||
const t = setTimeout(() => {
|
||||
try {
|
||||
onTimeout?.()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
reject(bootTimedOutError())
|
||||
}, ms)
|
||||
if (t.unref) t.unref()
|
||||
})
|
||||
}
|
||||
@@ -205,6 +205,12 @@ export async function createFishReadLine(ctx, opts) {
|
||||
let prevCursorRows = 0
|
||||
/** Rows spanned by last painted prompt+input+ghost (>=1); forces full refresh when >1. */
|
||||
let prevPaintRowCount = 0
|
||||
/**
|
||||
* First draw after each readLine(): kernel/console output may leave the cursor on a
|
||||
* wrapped or partial line; clearEntireLine only wipes one row. One full clear (col 0 +
|
||||
* clearScreenDown) resyncs — same effect as typing ^C (which prints a newline first).
|
||||
*/
|
||||
let firstPaintThisReadLine = false
|
||||
|
||||
/** @type {((v: string | null) => void) | null} */
|
||||
let pendingResolve = null
|
||||
@@ -298,11 +304,14 @@ export async function createFishReadLine(ctx, opts) {
|
||||
const curEnd = displayPos(strBefore, termCols)
|
||||
const paintRowCount = lineEnd.rows + 1
|
||||
const needFullRefresh =
|
||||
firstPaintThisReadLine ||
|
||||
lineEnd.rows > 0 ||
|
||||
curEnd.rows > 0 ||
|
||||
prevCursorRows > 0 ||
|
||||
prevPaintRowCount > 1
|
||||
|
||||
if (firstPaintThisReadLine) firstPaintThisReadLine = false
|
||||
|
||||
const out = prompt + coloredLine + ghostWrite
|
||||
|
||||
function moveUp(n) {
|
||||
@@ -694,6 +703,7 @@ export async function createFishReadLine(ctx, opts) {
|
||||
pendingResolve = resolve
|
||||
prevCursorRows = 0
|
||||
prevPaintRowCount = 0
|
||||
firstPaintThisReadLine = true
|
||||
line = ''
|
||||
lines = ['']
|
||||
currentLineIndex = 0
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import b4a from 'b4a'
|
||||
import c from 'compact-encoding'
|
||||
import { PROTOCOL_NAME } from 'bare-os-protocol/constants.js'
|
||||
import { BOOT_TIMEOUT_MS } from './boot-splash.js'
|
||||
|
||||
/** Default matches boot race; override with BARE_OS_MBR_READ_TIMEOUT_MS (was hardcoded 10s). */
|
||||
function mbrReadTimeoutMs() {
|
||||
const n = Number(globalThis.process?.env?.BARE_OS_MBR_READ_TIMEOUT_MS)
|
||||
if (Number.isFinite(n) && n > 0) return Math.min(Math.trunc(n), 300_000)
|
||||
return BOOT_TIMEOUT_MS
|
||||
}
|
||||
|
||||
export class SwarmDisk {
|
||||
constructor() {
|
||||
@@ -255,7 +263,7 @@ export class SwarmDisk {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error('MBR read timeout')),
|
||||
10000
|
||||
mbrReadTimeoutMs()
|
||||
)
|
||||
this.pendingReads.set(index, (data) => {
|
||||
clearTimeout(timeout)
|
||||
|
||||
Reference in New Issue
Block a user