updates
CI / test (push) Failing after 4m58s

This commit is contained in:
Raven Scott
2026-04-02 23:37:27 -04:00
parent 45cf20ea3c
commit 3b6cb97997
6 changed files with 217 additions and 26 deletions
+2 -2
View File
@@ -344,7 +344,7 @@ Pear-safe path resolution (same idea as Holepunch [pear-rti](https://github.com/
**`main()`** **`main()`**
- `Corestore(bootStorePath())`, `Hyperswarm`, `SwarmDisk`, join `topicKey()`. - `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`. - If peers: `bootFromPeers`; else `bootLocal`.
- **`finally`**: close `personalDrive`, `disk.drive`, `swarm.destroy()`, `store.close()` (each in try/catch). - **`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_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_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_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_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` | 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`) | | `BARE_OS_FISH_HISTORY_MAX` | Booter / fish-readline | Max history entries persisted to `/.bare_history` (default `1000`) |
+1 -1
View File
@@ -58,7 +58,7 @@ Runs workspace tests (`brittle-node` / `brittle-bare` where configured).
Optional: 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_LOCAL_SEED` — Corestore path for **local boot** when no peers (default `./data/corestore-seeder`).
- `BARE_OS_SKIP_REPL=1` — non-interactive kernel (CI / automation). - `BARE_OS_SKIP_REPL=1` — non-interactive kernel (CI / automation).
+116 -22
View File
@@ -21,6 +21,12 @@ import {
} from './lib/cli-readline.js' } from './lib/cli-readline.js'
import { resolveStdio } from './lib/resolve-stdio.js' import { resolveStdio } from './lib/resolve-stdio.js'
import { createFishReadLine, disableFishRawMode } from './lib/fish-readline.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) const _pkg = packageRootDir(import.meta.url)
@@ -28,6 +34,23 @@ function bootStorePath() {
return defaultBootCorestorePath(_pkg, import.meta.url) 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 }>} */ /** @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() { async function createReadLine() {
const { stdin, stdout } = await resolveStdio() const { stdin, stdout } = await resolveStdio()
@@ -148,6 +171,15 @@ async function executeKernel(disk, store, swarm, initSource) {
globalThis.console.clear?.() globalThis.console.clear?.()
} }
if (
interactiveAvailable &&
!skipInteractive &&
sessionStdout &&
typeof sessionStdout.write === 'function'
) {
writeScreen('\x1b[H\x1b[2J\x1b[3J')
}
const shellEnv = { const shellEnv = {
HOME: '/home/user', HOME: '/home/user',
PATH: '/bin', 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() const topic = topicKey()
console.log('Loading MBR from peers...') log('Loading MBR from peers...')
const mbr = await disk.read(0) const mbr = await disk.read(0)
const { keys } = parseMbr(mbr) const { keys } = parseMbr(mbr)
let initSource = null let initSource = null
for (const driveKey of keys) { for (const driveKey of keys) {
try { try {
console.log( log(
'Mounting drive', 'Mounting drive',
b4a.toString(driveKey, 'hex').slice(0, 16) + '...' b4a.toString(driveKey, 'hex').slice(0, 16) + '...'
) )
@@ -259,22 +301,32 @@ async function bootFromPeers(disk, store, swarm) {
} }
if (initSource) break 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) { } catch (err) {
console.log('Drive error:', err.message) log('Drive error:', err.message)
} }
} }
if (!initSource) throw new Error('Kernel not found after replication') if (!initSource) throw new Error('Kernel not found after replication')
await disk.initPersonalDrive(store, swarm, Hyperdrive) await disk.initPersonalDrive(store, swarm, Hyperdrive)
console.log('Starting kernel...') log('Starting kernel...')
await executeKernel(disk, store, swarm, initSource) 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) const seedPath = defaultLocalSeedCorestorePath(_pkg, import.meta.url)
console.log('Local boot from', seedPath) log('Local boot from', seedPath)
const seedStore = new Corestore(seedPath) const seedStore = new Corestore(seedPath)
disk.drive = new Hyperdrive(seedStore) disk.drive = new Hyperdrive(seedStore)
await disk.drive.ready() 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') if (!initSource) throw new Error('Kernel missing in local seed store')
await disk.initPersonalDrive(store, swarm, Hyperdrive) await disk.initPersonalDrive(store, swarm, Hyperdrive)
await executeKernel(disk, store, swarm, initSource) return initSource
} }
async function main() { async function main() {
console.log('--- bare-os-booter ---') 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 store = new Corestore(bootStorePath())
const swarm = new Hyperswarm() const swarm = new Hyperswarm()
const disk = new SwarmDisk() const disk = new SwarmDisk()
const topic = topicKey() const topic = topicKey()
/** @type {string} */
let bootStatus = 'joining swarm'
const stopSplash = useSplash
? startBootSplash(stdout, {
peerCount: () => disk.peers.size,
status: () => bootStatus
})
: () => {}
swarm.on('connection', (socket) => { swarm.on('connection', (socket) => {
const mux = new Protomux(socket) const mux = new Protomux(socket)
disk.addPeer(mux, socket) disk.addPeer(mux, socket)
@@ -306,21 +371,50 @@ async function main() {
swarm.join(topic) swarm.join(topic)
const maxWait = Number(globalThis.process?.env?.BARE_OS_PEER_WAIT_MS || 8000) const loadTimeoutMs = parseLoadTimeoutMs()
let waited = 0 const peerWaitMs = parsePeerWaitMs()
while (disk.peers.size === 0 && waited < maxWait) { /** Room for full peer wait plus replication/local load without racing too early. */
await new Promise((r) => setTimeout(r, 500)) const totalBootRaceMs = Math.min(peerWaitMs + loadTimeoutMs, 900_000)
waited += 500
}
console.log('Peers:', disk.peers.size)
try { try {
if (disk.peers.size > 0) { const initSource = await Promise.race([
await bootFromPeers(disk, store, swarm) (async () => {
} else { let waited = 0
await bootLocal(disk, store, swarm) 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 { } finally {
try { try {
if (disk.personalDrive) await disk.personalDrive.close() 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 let prevCursorRows = 0
/** Rows spanned by last painted prompt+input+ghost (>=1); forces full refresh when >1. */ /** Rows spanned by last painted prompt+input+ghost (>=1); forces full refresh when >1. */
let prevPaintRowCount = 0 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} */ /** @type {((v: string | null) => void) | null} */
let pendingResolve = null let pendingResolve = null
@@ -298,11 +304,14 @@ export async function createFishReadLine(ctx, opts) {
const curEnd = displayPos(strBefore, termCols) const curEnd = displayPos(strBefore, termCols)
const paintRowCount = lineEnd.rows + 1 const paintRowCount = lineEnd.rows + 1
const needFullRefresh = const needFullRefresh =
firstPaintThisReadLine ||
lineEnd.rows > 0 || lineEnd.rows > 0 ||
curEnd.rows > 0 || curEnd.rows > 0 ||
prevCursorRows > 0 || prevCursorRows > 0 ||
prevPaintRowCount > 1 prevPaintRowCount > 1
if (firstPaintThisReadLine) firstPaintThisReadLine = false
const out = prompt + coloredLine + ghostWrite const out = prompt + coloredLine + ghostWrite
function moveUp(n) { function moveUp(n) {
@@ -694,6 +703,7 @@ export async function createFishReadLine(ctx, opts) {
pendingResolve = resolve pendingResolve = resolve
prevCursorRows = 0 prevCursorRows = 0
prevPaintRowCount = 0 prevPaintRowCount = 0
firstPaintThisReadLine = true
line = '' line = ''
lines = [''] lines = ['']
currentLineIndex = 0 currentLineIndex = 0
+9 -1
View File
@@ -1,6 +1,14 @@
import b4a from 'b4a' import b4a from 'b4a'
import c from 'compact-encoding' import c from 'compact-encoding'
import { PROTOCOL_NAME } from 'bare-os-protocol/constants.js' 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 { export class SwarmDisk {
constructor() { constructor() {
@@ -255,7 +263,7 @@ export class SwarmDisk {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeout = setTimeout( const timeout = setTimeout(
() => reject(new Error('MBR read timeout')), () => reject(new Error('MBR read timeout')),
10000 mbrReadTimeoutMs()
) )
this.pendingReads.set(index, (data) => { this.pendingReads.set(index, (data) => {
clearTimeout(timeout) clearTimeout(timeout)