/** * 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, * 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' } } } }