booter updates
This commit is contained in:
@@ -155,6 +155,7 @@ import { buildPearIpcRegistryJson } from './lib/bare-os-pear-ipc-registry.js'
|
||||
import {
|
||||
appendVarLog,
|
||||
AUDIT_LOG,
|
||||
BOOT_LOG,
|
||||
LOGGER_JSON_LOG,
|
||||
mirrorBareOsTelemetryNdjson
|
||||
} from './lib/bare-os-var-log.js'
|
||||
@@ -4472,14 +4473,44 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
* @param {string} passphrase
|
||||
*/
|
||||
async applyUnlock(passphrase) {
|
||||
await unlockIdentity(this, passphrase)
|
||||
try {
|
||||
await unlockIdentity(this, passphrase)
|
||||
void appendVarLog(this, BOOT_LOG, 'boot', JSON.stringify({
|
||||
event: 'login_success',
|
||||
mode: 'unlock',
|
||||
atMs: Date.now()
|
||||
}))
|
||||
} catch (e) {
|
||||
void appendVarLog(this, BOOT_LOG, 'boot', JSON.stringify({
|
||||
event: 'login_failure',
|
||||
mode: 'unlock',
|
||||
atMs: Date.now(),
|
||||
error: e && e.message ? e.message : String(e)
|
||||
}))
|
||||
throw e
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Create account and unlock session.
|
||||
* @param {string} passphrase
|
||||
*/
|
||||
async applyRegister(passphrase) {
|
||||
await registerIdentity(this, passphrase)
|
||||
try {
|
||||
await registerIdentity(this, passphrase)
|
||||
void appendVarLog(this, BOOT_LOG, 'boot', JSON.stringify({
|
||||
event: 'login_success',
|
||||
mode: 'register',
|
||||
atMs: Date.now()
|
||||
}))
|
||||
} catch (e) {
|
||||
void appendVarLog(this, BOOT_LOG, 'boot', JSON.stringify({
|
||||
event: 'login_failure',
|
||||
mode: 'register',
|
||||
atMs: Date.now(),
|
||||
error: e && e.message ? e.message : String(e)
|
||||
}))
|
||||
throw e
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {{ publicKey: Uint8Array, secretKey: Uint8Array }} keys
|
||||
@@ -10055,9 +10086,23 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
ctx.writeScreen = session.writeScreen
|
||||
ctx.console = session.console
|
||||
|
||||
void appendVarLog(ctx, BOOT_LOG, 'boot', JSON.stringify({
|
||||
event: 'boot_start',
|
||||
atMs: Date.now(),
|
||||
bootStartedMs
|
||||
}))
|
||||
|
||||
emitBooterBootPhase('repl')
|
||||
void appendVarLog(ctx, BOOT_LOG, 'boot', JSON.stringify({
|
||||
event: 'kernel_ready',
|
||||
atMs: Date.now()
|
||||
}))
|
||||
|
||||
await maybeStartBareHolesailKernelFromBooter(ctx)
|
||||
void appendVarLog(ctx, BOOT_LOG, 'boot', JSON.stringify({
|
||||
event: 'initd_start',
|
||||
atMs: Date.now()
|
||||
}))
|
||||
|
||||
await startBareInitd(ctx)
|
||||
{
|
||||
@@ -10065,6 +10110,24 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
.filter((s) => getBareServiceRuntime(s.name)?.phase === 'active')
|
||||
.map((s) => s.name)
|
||||
Object.assign(bootReadyStateRef.subsystems, { initdActiveUnits: active })
|
||||
void appendVarLog(ctx, BOOT_LOG, 'boot', JSON.stringify({
|
||||
event: 'unit_active',
|
||||
atMs: Date.now(),
|
||||
units: active
|
||||
}))
|
||||
if (
|
||||
active.some((n) =>
|
||||
n === 'bare-openssh' || n === 'bare-holesail' || n === 'bare-os-www'
|
||||
)
|
||||
) {
|
||||
void appendVarLog(ctx, BOOT_LOG, 'boot', JSON.stringify({
|
||||
event: 'network_ready',
|
||||
atMs: Date.now(),
|
||||
units: active.filter((n) =>
|
||||
n === 'bare-openssh' || n === 'bare-holesail' || n === 'bare-os-www'
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (vfs && typeof vfs.writeFile === 'function') {
|
||||
@@ -10084,6 +10147,10 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
}
|
||||
|
||||
emitBooterBootPhase('initd')
|
||||
void appendVarLog(ctx, BOOT_LOG, 'boot', JSON.stringify({
|
||||
event: 'login_prompt_ready',
|
||||
atMs: Date.now()
|
||||
}))
|
||||
|
||||
const peerSeedElig = computePeerSystemSeedEligibility({
|
||||
disk,
|
||||
|
||||
@@ -61,6 +61,8 @@ const unitHealthTimers = new Map()
|
||||
|
||||
/** @type {(() => void)[]} */
|
||||
const disposers = []
|
||||
/** @type {Set<AbortController>} */
|
||||
const socketLoopAbortControllers = new Set()
|
||||
|
||||
/** @type {(() => void | Promise<void>)[]} */
|
||||
const kernelShutdownHooks = []
|
||||
@@ -220,6 +222,14 @@ export async function bareInitdShutdownActiveUnitsReverse(ctx) {
|
||||
}
|
||||
|
||||
export function stopBareInitd() {
|
||||
for (const ac of socketLoopAbortControllers) {
|
||||
try {
|
||||
ac.abort()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
socketLoopAbortControllers.clear()
|
||||
for (const t of unitHealthTimers.values()) clearInterval(t)
|
||||
unitHealthTimers.clear()
|
||||
for (const fn of disposers) {
|
||||
@@ -949,9 +959,26 @@ export async function startBareInitd(ctx) {
|
||||
for (;;) {
|
||||
appendBareInitdJournal(s.name, { event: 'socket_wait' })
|
||||
runtime.set(s.name, { phase: 'inactive', startedAtMs: Date.now() })
|
||||
const outerWaitAc = new AbortController()
|
||||
socketLoopAbortControllers.add(outerWaitAc)
|
||||
try {
|
||||
await ctx.bareOsIpc.take(ipcName)
|
||||
await ctx.bareOsIpc.take(ipcName, { signal: outerWaitAc.signal })
|
||||
} catch (e) {
|
||||
socketLoopAbortControllers.delete(outerWaitAc)
|
||||
if (
|
||||
e &&
|
||||
typeof e === 'object' &&
|
||||
/** @type {Error} */ (e).name === 'AbortError'
|
||||
) {
|
||||
runtime.set(s.name, {
|
||||
phase: 'inactive',
|
||||
startedAtMs: Date.now()
|
||||
})
|
||||
appendBareInitdJournal(s.name, {
|
||||
event: 'socket_loop_stopped'
|
||||
})
|
||||
return
|
||||
}
|
||||
const msg = e?.message || String(e)
|
||||
runtime.set(s.name, {
|
||||
phase: 'failed',
|
||||
@@ -970,6 +997,8 @@ export async function startBareInitd(ctx) {
|
||||
})
|
||||
await runOnFailureHookForUnit(ctx, s, di)
|
||||
return
|
||||
} finally {
|
||||
socketLoopAbortControllers.delete(outerWaitAc)
|
||||
}
|
||||
const t1 = Date.now()
|
||||
try {
|
||||
@@ -1013,6 +1042,7 @@ export async function startBareInitd(ctx) {
|
||||
if (!idleStopOk) return
|
||||
while (runtime.get(s.name)?.phase === 'active') {
|
||||
const ac = new AbortController()
|
||||
socketLoopAbortControllers.add(ac)
|
||||
const takeP = ctx.bareOsIpc.take(ipcName, { signal: ac.signal })
|
||||
const idleMs = idleSec * 1000
|
||||
const race = await Promise.race([
|
||||
@@ -1021,6 +1051,7 @@ export async function startBareInitd(ctx) {
|
||||
])
|
||||
if (race === 'idle') {
|
||||
ac.abort()
|
||||
socketLoopAbortControllers.delete(ac)
|
||||
try {
|
||||
await takeP
|
||||
} catch {
|
||||
@@ -1044,6 +1075,7 @@ export async function startBareInitd(ctx) {
|
||||
})
|
||||
break
|
||||
}
|
||||
socketLoopAbortControllers.delete(ac)
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e?.message || String(e)
|
||||
|
||||
@@ -139,7 +139,7 @@ function assertSafeIpcName(name) {
|
||||
const DEFAULT_JSON_RPC_MAX = 256 * 1024
|
||||
|
||||
/**
|
||||
* @param {{ maxFifoBytes?: number, maxChannels?: number, perChannelMaxBytes?: Map<string, number>, ipcRpcToken?: string | null, enableFanout?: boolean, maxJsonRpcLineBytes?: number, posixMqDefaultMaxmsg?: number, posixMqDefaultMaxBytes?: number, posixMqMaxmsgCeiling?: number }} [opts]
|
||||
* @param {{ maxFifoBytes?: number, maxChannels?: number, perChannelMaxBytes?: Map<string, number>, ipcRpcToken?: string | null, enableFanout?: boolean, maxJsonRpcLineBytes?: number, maxWaitersPerChannel?: number, posixMqDefaultMaxmsg?: number, posixMqDefaultMaxBytes?: number, posixMqMaxmsgCeiling?: number }} [opts]
|
||||
*/
|
||||
export function createBareOsIpc(opts = {}) {
|
||||
const maxFifoBytes =
|
||||
@@ -155,12 +155,18 @@ export function createBareOsIpc(opts = {}) {
|
||||
const perChannelMax =
|
||||
opts.perChannelMaxBytes instanceof Map ? opts.perChannelMaxBytes : null
|
||||
|
||||
/** @type {{ fifoCreates: number, fifoCreateDeniedQuota: number, fifoPushDenied: number, fifoTakeAborted: number, fanoutDrop: number, mqFull: number }} */
|
||||
const maxWaitersPerChannel =
|
||||
typeof opts.maxWaitersPerChannel === 'number' && opts.maxWaitersPerChannel > 0
|
||||
? Math.min(65536, Math.floor(opts.maxWaitersPerChannel))
|
||||
: 1024
|
||||
|
||||
/** @type {{ fifoCreates: number, fifoCreateDeniedQuota: number, fifoPushDenied: number, fifoTakeAborted: number, fifoTakeDeniedWaiters: number, fanoutDrop: number, mqFull: number }} */
|
||||
const telemetry = {
|
||||
fifoCreates: 0,
|
||||
fifoCreateDeniedQuota: 0,
|
||||
fifoPushDenied: 0,
|
||||
fifoTakeAborted: 0,
|
||||
fifoTakeDeniedWaiters: 0,
|
||||
fanoutDrop: 0,
|
||||
mqFull: 0
|
||||
}
|
||||
@@ -329,6 +335,14 @@ export function createBareOsIpc(opts = {}) {
|
||||
take(name, takeOpts) {
|
||||
const ch = channels.get(name)
|
||||
if (!ch) return Promise.reject(new Error('no such fifo: ' + name))
|
||||
if (ch.waiters.length >= maxWaitersPerChannel) {
|
||||
telemetry.fifoTakeDeniedWaiters++
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
'bare-os ipc: waiter backlog exceeds maxWaitersPerChannel'
|
||||
)
|
||||
)
|
||||
}
|
||||
return ch.take(takeOpts?.signal).catch((e) => {
|
||||
if (e && typeof e === 'object' && /** @type {Error} */ (e).name === 'AbortError') {
|
||||
telemetry.fifoTakeAborted++
|
||||
@@ -505,10 +519,15 @@ export function createBareOsIpc(opts = {}) {
|
||||
throw new Error('bare-os ipc mq: queue byte budget exceeded')
|
||||
}
|
||||
const seq = q.nextSeq++
|
||||
q.msgs.push({ prio: p, seq, data: u8 })
|
||||
q.msgs.sort(
|
||||
(a, b) => b.prio - a.prio || a.seq - b.seq
|
||||
)
|
||||
const row = { prio: p, seq, data: u8 }
|
||||
let idx = q.msgs.length
|
||||
while (idx > 0) {
|
||||
const prev = q.msgs[idx - 1]
|
||||
if (prev.prio > row.prio) break
|
||||
if (prev.prio === row.prio && prev.seq < row.seq) break
|
||||
idx--
|
||||
}
|
||||
q.msgs.splice(idx, 0, row)
|
||||
q.curBytes += u8.byteLength
|
||||
return { ok: true }
|
||||
},
|
||||
@@ -559,6 +578,10 @@ export function createBareOsIpc(opts = {}) {
|
||||
channelCount: channels.size,
|
||||
maxChannels,
|
||||
telemetry: { ...telemetry },
|
||||
waiterLimits: {
|
||||
schema: 1,
|
||||
maxWaitersPerChannel
|
||||
},
|
||||
queuedBytesTotal,
|
||||
fanoutTopicCount: fanouts.size,
|
||||
fanoutSubscribersTotal,
|
||||
|
||||
@@ -7,6 +7,10 @@ const INITD_SYNTH_PID_BASE = 5100
|
||||
/** @type {Map<string, number>} */
|
||||
const initdSyntheticPidByName = new Map()
|
||||
let nextInitdSyntheticPid = INITD_SYNTH_PID_BASE
|
||||
/** @type {Map<number, { createdAtMs: number, lastStateChangeAtMs: number, lastState: string, completedAtMs?: number }>} */
|
||||
const shellJobLifecycleById = new Map()
|
||||
/** @type {Map<string, { createdAtMs: number, lastStateChangeAtMs: number, lastState: string, completedAtMs?: number }>} */
|
||||
const initdLifecycleByName = new Map()
|
||||
|
||||
/**
|
||||
* @param {string} unitName
|
||||
@@ -21,6 +25,59 @@ function syntheticInitdPidFor(unitName) {
|
||||
return pid
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @param {string} state
|
||||
* @param {number} now
|
||||
*/
|
||||
function observeShellJobLifecycle(id, state, now) {
|
||||
const cur = shellJobLifecycleById.get(id)
|
||||
if (!cur) {
|
||||
const row = { createdAtMs: now, lastStateChangeAtMs: now, lastState: state }
|
||||
if (state === 'zombie') row.completedAtMs = now
|
||||
shellJobLifecycleById.set(id, row)
|
||||
return row
|
||||
}
|
||||
if (cur.lastState !== state) {
|
||||
cur.lastState = state
|
||||
cur.lastStateChangeAtMs = now
|
||||
if (state === 'zombie' && !Number.isFinite(cur.completedAtMs)) cur.completedAtMs = now
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} unit
|
||||
* @param {string} state
|
||||
* @param {number} startedAtMs
|
||||
* @param {number} now
|
||||
*/
|
||||
function observeInitdLifecycle(unit, state, startedAtMs, now) {
|
||||
const cur = initdLifecycleByName.get(unit)
|
||||
if (!cur) {
|
||||
const row = {
|
||||
createdAtMs: Number.isFinite(startedAtMs) ? startedAtMs : now,
|
||||
lastStateChangeAtMs: now,
|
||||
lastState: state
|
||||
}
|
||||
if (state === 'failed' || state === 'dead' || state === 'inactive') row.completedAtMs = now
|
||||
initdLifecycleByName.set(unit, row)
|
||||
return row
|
||||
}
|
||||
if (cur.lastState !== state) {
|
||||
cur.lastState = state
|
||||
cur.lastStateChangeAtMs = now
|
||||
if (
|
||||
(state === 'failed' || state === 'dead' || state === 'inactive') &&
|
||||
!Number.isFinite(cur.completedAtMs)
|
||||
) {
|
||||
cur.completedAtMs = now
|
||||
}
|
||||
if (state === 'active' || state === 'starting') cur.completedAtMs = undefined
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, string | undefined> | null | undefined} env
|
||||
*/
|
||||
@@ -238,6 +295,7 @@ export function bareOsProcessTableSnapshot(opts = {}) {
|
||||
const pid = 4100 + j.id
|
||||
const pgid = typeof j.pgid === 'number' ? j.pgid : pid
|
||||
const session = typeof j.sid === 'number' ? j.sid : 1
|
||||
const life = observeShellJobLifecycle(j.id, j.stopped ? 'stopped' : 'running', now)
|
||||
return {
|
||||
pid,
|
||||
ppid: 3,
|
||||
@@ -245,7 +303,10 @@ export function bareOsProcessTableSnapshot(opts = {}) {
|
||||
sid: session,
|
||||
name: 'bare-os-shell-job',
|
||||
state: j.stopped ? 'stopped' : 'running',
|
||||
startedAtMs: now,
|
||||
startedAtMs: life.createdAtMs,
|
||||
createdAtMs: life.createdAtMs,
|
||||
lastStateChangeAtMs: life.lastStateChangeAtMs,
|
||||
completedAtMs: life.completedAtMs,
|
||||
jobId: j.id,
|
||||
role: 'shell_job',
|
||||
label: typeof j.label === 'string' ? j.label.slice(0, 256) : undefined,
|
||||
@@ -259,6 +320,7 @@ export function bareOsProcessTableSnapshot(opts = {}) {
|
||||
const pid = 4200 + jid
|
||||
const pgid = typeof j.pgid === 'number' ? j.pgid : pid
|
||||
const session = typeof j.sid === 'number' ? j.sid : 1
|
||||
const life = observeShellJobLifecycle(jid, 'zombie', now)
|
||||
return {
|
||||
pid,
|
||||
ppid: 3,
|
||||
@@ -266,7 +328,10 @@ export function bareOsProcessTableSnapshot(opts = {}) {
|
||||
sid: session,
|
||||
name: 'bare-os-shell-job',
|
||||
state: 'zombie',
|
||||
startedAtMs: now,
|
||||
startedAtMs: life.createdAtMs,
|
||||
createdAtMs: life.createdAtMs,
|
||||
lastStateChangeAtMs: life.lastStateChangeAtMs,
|
||||
completedAtMs: life.completedAtMs,
|
||||
jobId: jid,
|
||||
role: 'shell_job',
|
||||
label: typeof j.label === 'string' ? j.label.slice(0, 256) : undefined,
|
||||
@@ -316,6 +381,12 @@ export function bareOsProcessTableSnapshot(opts = {}) {
|
||||
/repl|swarm|net|peer|dht|hdms|hyperswarm/.test(nmLower)
|
||||
? 'net-related-initd-failure'
|
||||
: undefined
|
||||
const life = observeInitdLifecycle(
|
||||
u.name,
|
||||
String(u.phase || ''),
|
||||
typeof u.startedAtMs === 'number' ? u.startedAtMs : now,
|
||||
now
|
||||
)
|
||||
initdLogicalProcs.push({
|
||||
pid,
|
||||
ppid: 2,
|
||||
@@ -335,6 +406,9 @@ export function bareOsProcessTableSnapshot(opts = {}) {
|
||||
? 'sleeping'
|
||||
: 'sleeping',
|
||||
startedAtMs: typeof u.startedAtMs === 'number' ? u.startedAtMs : now,
|
||||
createdAtMs: life.createdAtMs,
|
||||
lastStateChangeAtMs: life.lastStateChangeAtMs,
|
||||
completedAtMs: life.completedAtMs,
|
||||
role: 'initd_unit',
|
||||
initdUnit: u.name.slice(0, 128),
|
||||
initdPhase: String(u.phase || '').slice(0, 32),
|
||||
@@ -383,8 +457,8 @@ export function bareOsProcessTableSnapshot(opts = {}) {
|
||||
|
||||
return {
|
||||
schema: 9,
|
||||
schemaVersion: 9,
|
||||
note: 'Synthetic rows; Bare OS guests do not expose host OS processes. v9 adds per-row nice (-20..19) via ctx.bareOsRenice, accountingSource on accounting fields, and companions process_maps.json / process_threads.json (logical models). v8 optional accounting + initd replicationHint retained.',
|
||||
schemaVersion: 10,
|
||||
note: 'Synthetic rows; Bare OS guests do not expose host OS processes. v10 adds lifecycle timestamps (createdAtMs/lastStateChangeAtMs/completedAtMs) for shell jobs and initd units. v9 adds per-row nice (-20..19) via ctx.bareOsRenice, accountingSource on accounting fields, and companions process_maps.json / process_threads.json (logical models). v8 optional accounting + initd replicationHint retained.',
|
||||
signalModel: {
|
||||
schema: 1,
|
||||
note: 'Virtual deliveries via ctx.bareOsSendSignal (not full sigaction/sigprocmask).',
|
||||
|
||||
@@ -64,7 +64,8 @@ function readSwarmPolicyEnv(env) {
|
||||
* lastLatencyMs: number | null,
|
||||
* reconnectBudget: number,
|
||||
* earliestRetryAtMs: number,
|
||||
* retryTier: 'none' | 'short' | 'medium' | 'long' | 'xlong'
|
||||
* retryTier: 'none' | 'short' | 'medium' | 'long' | 'xlong',
|
||||
* failByClass: Record<string, number>
|
||||
* }} PeerState
|
||||
*/
|
||||
|
||||
@@ -103,7 +104,8 @@ export class BareOsSwarmPeerPolicyEngine {
|
||||
lastLatencyMs: null,
|
||||
reconnectBudget: 32,
|
||||
earliestRetryAtMs: 0,
|
||||
retryTier: 'none'
|
||||
retryTier: 'none',
|
||||
failByClass: {}
|
||||
}
|
||||
this._peers.set(k, st)
|
||||
}
|
||||
@@ -131,7 +133,7 @@ export class BareOsSwarmPeerPolicyEngine {
|
||||
/**
|
||||
* @param {string} peerKey
|
||||
* @param {boolean} ok
|
||||
* @param {{ latencyMs?: number }} [probeMeta]
|
||||
* @param {{ latencyMs?: number, failClass?: string }} [probeMeta]
|
||||
*/
|
||||
noteProbe(peerKey, ok, probeMeta = {}) {
|
||||
const k = this._key(peerKey)
|
||||
@@ -161,6 +163,10 @@ export class BareOsSwarmPeerPolicyEngine {
|
||||
} else {
|
||||
st.fail++
|
||||
st.consecutiveFail++
|
||||
const failClass = String(probeMeta.failClass || 'unknown')
|
||||
.trim()
|
||||
.slice(0, 32) || 'unknown'
|
||||
st.failByClass[failClass] = (st.failByClass[failClass] || 0) + 1
|
||||
const retry = this._retryDelayFor(st)
|
||||
st.retryTier = retry.tier
|
||||
st.earliestRetryAtMs = now + retry.delayMs
|
||||
@@ -202,6 +208,7 @@ export class BareOsSwarmPeerPolicyEngine {
|
||||
const st = this._peers.get(k)
|
||||
const now = Date.now()
|
||||
if (!st) return true
|
||||
if (this._globalReconnectBudget <= 0) return false
|
||||
if (st.banUntilMs > now) return false
|
||||
if (st.earliestRetryAtMs > now) return false
|
||||
if (st.reconnectBudget <= 0) return false
|
||||
@@ -259,12 +266,15 @@ export class BareOsSwarmPeerPolicyEngine {
|
||||
if (!total) return 0
|
||||
const successRate = st.ok / total
|
||||
const failPenalty = Math.min(0.5, st.fail * 0.05)
|
||||
const protocolPenalty = Math.min(0.25, (st.failByClass.protocol || 0) * 0.03)
|
||||
const policyPenalty = Math.min(0.2, (st.failByClass.policy || 0) * 0.02)
|
||||
const timeoutPenalty = Math.min(0.15, (st.failByClass.timeout || 0) * 0.01)
|
||||
const now = Date.now()
|
||||
const banned = st.banUntilMs > now ? 0.35 : 0
|
||||
const lat = st.ewmaLatencyMs
|
||||
const latPenalty =
|
||||
lat > 0 ? Math.min(0.25, Math.log10(1 + lat / 50) * 0.08) : 0
|
||||
return successRate - failPenalty - banned - latPenalty
|
||||
return successRate - failPenalty - protocolPenalty - policyPenalty - timeoutPenalty - banned - latPenalty
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
|
||||
@@ -55,6 +55,7 @@ export const CRON_LOG = `${BARE_OS_VAR_LOG_DIR}/cron.log`
|
||||
export const INITD_LOG = `${BARE_OS_VAR_LOG_DIR}/initd.log`
|
||||
|
||||
export const AUDIT_LOG = `${BARE_OS_VAR_LOG_DIR}/audit.log`
|
||||
export const BOOT_LOG = `${BARE_OS_VAR_LOG_DIR}/boot.log`
|
||||
|
||||
/** Structured JSON lines from `/bin/logger` (syslog-style metadata). */
|
||||
export const LOGGER_JSON_LOG = `${BARE_OS_VAR_LOG_DIR}/logger.jsonl`
|
||||
@@ -69,6 +70,7 @@ const README_TEXT = `Bare OS session logs (mirrored on your personal drive under
|
||||
www.log — bare-os-www static HTTP for ~/.www
|
||||
initd.log — bare-initd service start failures
|
||||
audit.log — optional execLine audit when BARE_OS_AUDIT=1
|
||||
boot.log — boot-to-login timeline milestones (best-effort)
|
||||
`
|
||||
|
||||
/**
|
||||
@@ -101,6 +103,29 @@ const LOG_MAX_BYTES = 512 * 1024
|
||||
|
||||
/** After trim, keep this many trailing bytes plus a notice line. */
|
||||
const LOG_KEEP_BYTES = 256 * 1024
|
||||
const TELEMETRY_LOG_CAP_BYTES = 2 * 1024 * 1024
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} path
|
||||
* @param {Uint8Array} chunk
|
||||
* @param {number} capBytes
|
||||
*/
|
||||
async function appendLineWithOptionalCap(ctx, path, chunk, capBytes) {
|
||||
const vfs = ctx.vfs
|
||||
if (!vfs) return
|
||||
if (typeof vfs.appendFile === 'function') {
|
||||
await vfs.appendFile(path, chunk)
|
||||
return
|
||||
}
|
||||
if (typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') return
|
||||
const prev = await vfs.readFile(path)
|
||||
let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk
|
||||
if (merged.length > capBytes) {
|
||||
merged = merged.subarray(merged.length - capBytes)
|
||||
}
|
||||
await vfs.writeFile(path, merged)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure `/var/log/bare-os` exists and a short README is present. Best-effort; never throws.
|
||||
@@ -153,7 +178,6 @@ export async function appendVarLog(ctx, logicalFilePath, kind, line) {
|
||||
)
|
||||
return
|
||||
}
|
||||
const prev = await vfs.readFile(logicalFilePath)
|
||||
const ts = new Date().toISOString()
|
||||
const chunk = ctx.b4a.from(`[${ts}] [${kind}] ${line}\n`)
|
||||
const fastAppend =
|
||||
@@ -161,7 +185,7 @@ export async function appendVarLog(ctx, logicalFilePath, kind, line) {
|
||||
ctx.env?.BARE_OS_VAR_LOG_FAST_APPEND === 'true') &&
|
||||
typeof vfs.appendFile === 'function'
|
||||
if (fastAppend) {
|
||||
await vfs.appendFile(logicalFilePath, chunk)
|
||||
await appendLineWithOptionalCap(ctx, logicalFilePath, chunk, LOG_MAX_BYTES)
|
||||
await mirrorBareOsTelemetryNdjson(ctx, {
|
||||
type: 'varLog',
|
||||
path: logicalFilePath,
|
||||
@@ -175,6 +199,7 @@ export async function appendVarLog(ctx, logicalFilePath, kind, line) {
|
||||
})
|
||||
return
|
||||
}
|
||||
const prev = await vfs.readFile(logicalFilePath)
|
||||
let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk
|
||||
if (merged.length > LOG_MAX_BYTES) {
|
||||
const start = Math.max(0, merged.length - LOG_KEEP_BYTES)
|
||||
@@ -222,7 +247,7 @@ export async function mirrorBareOsTelemetryNdjson(ctx, rec) {
|
||||
const dest = String(raw).trim()
|
||||
if (!dest.startsWith('/') && !dest.startsWith('~/')) return
|
||||
const vfs = ctx.vfs
|
||||
if (!vfs || typeof vfs.readFile !== 'function') return
|
||||
if (!vfs) return
|
||||
const lineage = String(env.BARE_OS_TELEMETRY_SESSION_LINEAGE_ID || '').trim()
|
||||
const bootAttemptId = String(env.BARE_OS_BOOT_ATTEMPT_ID || '').trim()
|
||||
const bareModuleCryptoStagingProbeId = String(env.BARE_OS_PROBE_ID_BARE_MODULE_CRYPTO_STAGING || '').trim()
|
||||
@@ -261,13 +286,7 @@ export async function mirrorBareOsTelemetryNdjson(ctx, rec) {
|
||||
...safeRec
|
||||
}) + '\n'
|
||||
const chunk = ctx.b4a.from(line)
|
||||
const prev = await vfs.readFile(dest)
|
||||
let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk
|
||||
const cap = 2 * 1024 * 1024
|
||||
if (merged.length > cap) {
|
||||
merged = merged.subarray(merged.length - cap)
|
||||
}
|
||||
await vfs.writeFile(dest, merged)
|
||||
await appendLineWithOptionalCap(ctx, dest, chunk, TELEMETRY_LOG_CAP_BYTES)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -289,7 +308,7 @@ async function mirrorBareOsOtelJsonl(ctx, eventName, attrs) {
|
||||
const dest = String(raw).trim()
|
||||
if (!dest.startsWith('/') && !dest.startsWith('~/')) return
|
||||
const vfs = ctx.vfs
|
||||
if (!vfs || typeof vfs.readFile !== 'function') return
|
||||
if (!vfs) return
|
||||
const safeAttrs = bareOsTelemetrySanitizeRec(
|
||||
/** @type {Record<string, unknown>} */ (attrs)
|
||||
)
|
||||
@@ -334,13 +353,7 @@ async function mirrorBareOsOtelJsonl(ctx, eventName, attrs) {
|
||||
]
|
||||
}) + '\n'
|
||||
const chunk = ctx.b4a.from(line)
|
||||
const prev = await vfs.readFile(dest)
|
||||
let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk
|
||||
const cap = 2 * 1024 * 1024
|
||||
if (merged.length > cap) {
|
||||
merged = merged.subarray(merged.length - cap)
|
||||
}
|
||||
await vfs.writeFile(dest, merged)
|
||||
await appendLineWithOptionalCap(ctx, dest, chunk, TELEMETRY_LOG_CAP_BYTES)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
@@ -126,7 +126,8 @@ import {
|
||||
import {
|
||||
appendVarLog,
|
||||
ensureBareOsVarLogTree,
|
||||
KERNEL_CONSOLE_LOG
|
||||
KERNEL_CONSOLE_LOG,
|
||||
BOOT_LOG
|
||||
} from './lib/bare-os-var-log.js'
|
||||
import {
|
||||
bareOsKernelMetricsReset,
|
||||
@@ -7018,6 +7019,20 @@ test('bare-os-ipc pushJson requires token when configured', async (t) => {
|
||||
t.is(j.x, 1)
|
||||
})
|
||||
|
||||
test('bare-os-ipc enforces waiter cap and reports telemetry', async (t) => {
|
||||
const ipc = createBareOsIpc({ maxWaitersPerChannel: 1 })
|
||||
ipc.create('cap')
|
||||
const p1 = ipc.take('cap')
|
||||
await Promise.resolve()
|
||||
await t.exception(async () => {
|
||||
await ipc.take('cap')
|
||||
}, /maxWaitersPerChannel/)
|
||||
const st = ipc.stats()
|
||||
t.ok(st && st.telemetry && st.telemetry.fifoTakeDeniedWaiters >= 1)
|
||||
ipc.push('cap', b4a.from('x'))
|
||||
t.is(b4a.toString(await p1), 'x')
|
||||
})
|
||||
|
||||
test('bareOsHttpUrlAllowed allowlist and denylist', async (t) => {
|
||||
t.ok(bareOsHttpUrlAllowed('https://a.example/x', { allow: [], deny: [] }).ok)
|
||||
t.ok(
|
||||
@@ -7558,7 +7573,7 @@ test('process table snapshot includes processGroups metadata', async (t) => {
|
||||
t.ok(s.processGroups)
|
||||
t.is(s.processGroups.schema, 1)
|
||||
t.ok(String(s.processGroups.killpgAnalog).includes('killpg'))
|
||||
t.is(s.schemaVersion, 9)
|
||||
t.is(s.schemaVersion, 10)
|
||||
t.ok(s.jobControlSemantics && s.jobControlSemantics.pipefail === true)
|
||||
const booter = s.processes.find((p) => p && p.pid === 2)
|
||||
t.ok(booter && booter.parentName === 'bare-os-kernel')
|
||||
@@ -7570,6 +7585,48 @@ test('process table snapshot includes processGroups metadata', async (t) => {
|
||||
t.ok(s.exitStatusModel && s.exitStatusModel.schema === 1)
|
||||
})
|
||||
|
||||
test('process table lifecycle timestamps remain stable across snapshots', async (t) => {
|
||||
const running = bareOsProcessTableSnapshot({
|
||||
shellJobs: [{ id: 7, done: false, stopped: false, label: 'demo' }]
|
||||
})
|
||||
const row1 = running.processes.find((p) => p && p.jobId === 7)
|
||||
t.ok(row1 && Number.isFinite(row1.createdAtMs))
|
||||
const stopped = bareOsProcessTableSnapshot({
|
||||
shellJobs: [{ id: 7, done: false, stopped: true, label: 'demo' }]
|
||||
})
|
||||
const row2 = stopped.processes.find((p) => p && p.jobId === 7)
|
||||
t.is(row2.createdAtMs, row1.createdAtMs)
|
||||
t.ok(row2.lastStateChangeAtMs >= row1.createdAtMs)
|
||||
const done = bareOsProcessTableSnapshot({
|
||||
shellJobs: [{ id: 7, done: true, stopped: false, label: 'demo' }]
|
||||
})
|
||||
const row3 = done.processes.find((p) => p && p.jobId === 7)
|
||||
t.is(row3.createdAtMs, row1.createdAtMs)
|
||||
t.ok(Number.isFinite(row3.completedAtMs))
|
||||
})
|
||||
|
||||
test('boot log writes timeline rows into personal backing', async (t) => {
|
||||
const dir = testCorestoreDir('bootlog')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pbootlog'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const ctx = testCtx(drive, personal)
|
||||
await ensureBareOsVarLogTree(ctx)
|
||||
await appendVarLog(
|
||||
ctx,
|
||||
BOOT_LOG,
|
||||
'boot',
|
||||
JSON.stringify({ event: 'login_prompt_ready', atMs: Date.now() })
|
||||
)
|
||||
const rel = personalHomeBacking('/home/user', 'var/log/bare-os/boot.log')
|
||||
const raw = b4a.toString(await personal.readFile(rel), 'utf8')
|
||||
t.ok(raw.includes('login_prompt_ready'))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('bare_acl other:: and mask interact like POSIX ACL classes', async (t) => {
|
||||
const env = { UID: '1000', GID: '1000' }
|
||||
const p1 = bareOsParseBareAclText('other::---\n')
|
||||
|
||||
Reference in New Issue
Block a user