Files
bare-operating-system/packages/bare-os-booter/lib/posix/bare-os-posix-fd-sim.js
T
2026-08-18 18:11:28 -04:00

251 lines
9.0 KiB
JavaScript

/**
* Simulated POSIX pipe FDs and poll probe methods attached to the kernel `ctx`.
*/
import b4a from 'b4a'
import { wantPosixSocketFdBridge } from './bare-os-posix-syscall-helpers.js'
/**
* @param {{
* env: Record<string, string | undefined>,
* bootHrtimeNowNs: (() => bigint) | null
* }} deps
*/
export function createBareOsPosixFdSimMethods(deps) {
const { env, bootHrtimeNowNs } = deps
return {
bareOsPosixFdSimCapBytes() {
const raw = String(env.BARE_OS_POSIX_FD_SIM_MAX_BYTES || '').trim()
const n = raw ? Number.parseInt(raw, 10) : 1048576
return Number.isFinite(n) && n > 0
? Math.min(n, 16 * 1024 * 1024)
: 1048576
},
bareOsPosixFdSimEnabled() {
const v = env.BARE_OS_POSIX_FD_SIM
return v === '1' || v === 'true'
},
/** @returns {{ readFd: number, writeFd: number, pairId: number } | null} */
bareOsPosixFdSimPipe() {
if (!this.bareOsPosixFdSimEnabled()) return null
const st = this.bareOsPosixFdSimState
if (!st || typeof st !== 'object') return null
const pairId = st.nextPairId++
const readFd = st.nextFd++
const writeFd = st.nextFd++
if (st.nextFd > 65530) {
st.nextFd = 10
return null
}
const pk = String(pairId)
st.queues[pk] = []
st.byteTotals[pk] = 0
this.bareOsRegisterLogicalFd(readFd, `posix-pipe:${pairId}:r`)
this.bareOsRegisterLogicalFd(writeFd, `posix-pipe:${pairId}:w`)
return { readFd, writeFd, pairId }
},
/** @param {number} fd */
bareOsPosixFdSimDup(fd) {
if (!this.bareOsPosixFdSimEnabled()) return null
const k = String(Number(fd) >>> 0)
const target = this.bareOsLogicalFds[k]
if (!target || !String(target).startsWith('posix-pipe:')) return null
const st = this.bareOsPosixFdSimState
const newFd = st.nextFd++
if (st.nextFd > 65530) return null
this.bareOsRegisterLogicalFd(newFd, String(target))
return newFd
},
/**
* @param {number} writeFd
* @param {string | Uint8Array} data
*/
bareOsPosixFdSimWrite(writeFd, data) {
if (!this.bareOsPosixFdSimEnabled())
return { ok: false, reason: 'disabled' }
const k = String(Number(writeFd) >>> 0)
const t = this.bareOsLogicalFds[k]
const m = /^posix-pipe:(\d+):w$/.exec(String(t || ''))
if (!m) return { ok: false, reason: 'not_write_end' }
const pairId = m[1]
const st = this.bareOsPosixFdSimState
const q = st.queues[pairId]
if (!q) return { ok: false, reason: 'bad_pair' }
const buf =
typeof data === 'string'
? b4a.from(data, 'utf8')
: data instanceof Uint8Array
? data
: b4a.from(String(data), 'utf8')
const add = buf.byteLength
const cap = this.bareOsPosixFdSimCapBytes()
const prev = st.byteTotals[pairId] || 0
if (prev + add > cap) return { ok: false, reason: 'EAGAIN' }
st.byteTotals[pairId] = prev + add
q.push(b4a.toString(buf, 'utf8'))
return { ok: true, bytes: add }
},
/**
* @param {number} readFd
* @param {number} [maxBytes]
*/
bareOsPosixFdSimRead(readFd, maxBytes = 65536) {
if (!this.bareOsPosixFdSimEnabled())
return { ok: false, reason: 'disabled' }
const cap = Math.max(1, Math.min(Number(maxBytes) || 65536, 1024 * 1024))
const k = String(Number(readFd) >>> 0)
const t = this.bareOsLogicalFds[k]
const m = /^posix-pipe:(\d+):r$/.exec(String(t || ''))
if (!m) return { ok: false, reason: 'not_read_end' }
const pairId = m[1]
const st = this.bareOsPosixFdSimState
const q = st.queues[pairId]
if (!q || q.length === 0) {
const nonblock = (this.bareOsLogicalFdFlags[k] & 0x800) === 0x800
if (nonblock) return { ok: false, reason: 'EAGAIN' }
return { ok: true, data: '', eof: false }
}
let chunk = q.shift()
if (!chunk) return { ok: true, data: '', eof: false }
if (chunk.length > cap) {
const rest = chunk.slice(cap)
chunk = chunk.slice(0, cap)
q.unshift(rest)
}
const used = b4a.from(chunk, 'utf8').byteLength
st.byteTotals[pairId] = Math.max(0, (st.byteTotals[pairId] || 0) - used)
return { ok: true, data: chunk, eof: false }
},
/**
* Non-blocking readiness probe for logical FDs (simulated pipes + stdout/stderr write).
* @param {{ fds?: Array<{ fd: number, events?: string }>, timeoutMs?: number }} [spec]
* @returns {{ ok: true, ready: Array<{ fd: number, revents: string }>, waitedMs: number, timedOut?: boolean }}
*/
bareOsPosixPollProbe(spec = {}) {
const fds = Array.isArray(spec.fds) ? spec.fds : []
/** @type {Array<{ fd: number, revents: string }>} */
const ready = []
for (const entry of fds.slice(0, 64)) {
const fd = Number(entry && entry.fd)
if (!Number.isFinite(fd) || fd < 0) continue
const ev = String(entry.events || 'rw').toLowerCase()
const wantR = ev.includes('r')
const wantW = ev.includes('w')
const k = String(fd >>> 0)
const t = this.bareOsLogicalFds[k]
const pipeR = /^posix-pipe:(\d+):r$/.exec(String(t || ''))
const pipeW = /^posix-pipe:(\d+):w$/.exec(String(t || ''))
if (wantR && pipeR) {
const st = this.bareOsPosixFdSimState
const q = st.queues[pipeR[1]]
if (q && q.length > 0) ready.push({ fd, revents: 'r' })
}
if (wantW && pipeW) {
const st = this.bareOsPosixFdSimState
const pairId = pipeW[1]
const prev = st.byteTotals[pairId] || 0
const cap = this.bareOsPosixFdSimCapBytes()
if (prev < cap) ready.push({ fd, revents: 'w' })
}
if (wantW && (k === '1' || k === '2')) {
ready.push({ fd, revents: 'w' })
}
if (wantR && wantPosixSocketFdBridge(this.env)) {
const bridge =
this.bareOsSocketBridgeByFd &&
typeof this.bareOsSocketBridgeByFd === 'object'
? this.bareOsSocketBridgeByFd[k]
: null
if (
bridge &&
(bridge.state === 'connected' || bridge.state === 'udp_bound') &&
bridge.transport === 'udp' &&
Array.isArray(bridge.dgramRecvQueue) &&
bridge.dgramRecvQueue.length > 0
) {
ready.push({ fd, revents: 'r' })
}
if (
bridge &&
bridge.state === 'connected' &&
bridge.transport === 'tcp'
) {
const tq = bridge.tcpRecvQueue
const tqlen = Array.isArray(tq) ? tq.length : 0
if (
wantR &&
(tqlen > 0 || bridge.tcpPeerEnded || bridge.tcpSockError)
) {
ready.push({ fd, revents: 'r' })
}
if (wantW && !bridge.tcpShutWr) {
ready.push({ fd, revents: 'w' })
}
}
if (
bridge &&
bridge.state === 'listening' &&
Array.isArray(bridge.acceptQueue) &&
bridge.acceptQueue.length > 0
) {
ready.push({ fd, revents: 'r' })
}
}
}
return { ok: true, ready, waitedMs: 0 }
},
/**
* Poll logical FDs with bounded wait (simulated **`poll(2)`** subset for **`BARE_OS_POSIX_FD_SIM`** pipes).
* @param {{ fds?: Array<{ fd: number, events?: string }>, timeoutMs?: number }} [spec]
*/
async bareOsPosixPoll(spec = {}) {
const maxWait = Math.min(
60000,
Math.max(0, Math.floor(Number(spec && spec.timeoutMs) || 0))
)
if (maxWait <= 0) {
const once = this.bareOsPosixPollProbe(spec)
const tProbe = bootHrtimeNowNs ? bootHrtimeNowNs() : null
return {
ok: true,
ready: once.ready,
waitedMs: 0,
timedOut: false,
pollClock: tProbe != null ? 'monotonic_hrtime' : 'Date_now'
}
}
const t0Ns = bootHrtimeNowNs ? bootHrtimeNowNs() : null
const t0Wall = Date.now()
const elapsedMs = () => {
if (t0Ns != null && bootHrtimeNowNs) {
const delta = Number(bootHrtimeNowNs() - t0Ns) / 1e6
return Math.min(60000, Math.max(0, delta))
}
return Math.min(60000, Math.max(0, Date.now() - t0Wall))
}
const interval = 10
while (elapsedMs() <= maxWait) {
const once = this.bareOsPosixPollProbe(spec)
if (once.ready.length) {
return {
ok: true,
ready: once.ready,
waitedMs: Math.round(elapsedMs()),
timedOut: false,
pollClock: t0Ns != null ? 'monotonic_hrtime' : 'Date_now'
}
}
if (elapsedMs() >= maxWait) break
await new Promise((res) => setTimeout(res, interval))
}
return {
ok: true,
ready: [],
waitedMs: Math.round(Math.min(maxWait, elapsedMs())),
timedOut: true,
pollClock: t0Ns != null ? 'monotonic_hrtime' : 'Date_now'
}
}
}
}