251 lines
9.0 KiB
JavaScript
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'
|
|
}
|
|
}
|
|
}
|
|
}
|