Pass 7
This commit is contained in:
@@ -166,6 +166,7 @@ import { createBooterBootEmitter } from './lib/bare-os-boot-phases.js'
|
||||
import { createBooterProcHelpers } from './lib/bare-os-booter-proc-helpers.js'
|
||||
import { createBareOsVirtualSignalDeliverer } from './lib/bare-os-virtual-signal.js'
|
||||
import { createKernelLoaderAuditAppend } from './lib/bare-os-loader-audit.js'
|
||||
import { createBareOsPosixFdSimMethods } from './lib/bare-os-posix-fd-sim.js'
|
||||
import { createBareOsQvacBridge } from './lib/bare-os-qvac-host.mjs'
|
||||
import { bareOsQvacEnsureModelsHdms } from './lib/bare-os-qvac-models-store.mjs'
|
||||
import { buildPearIpcRegistryJson } from './lib/bare-os-pear-ipc-registry.js'
|
||||
@@ -317,7 +318,6 @@ import {
|
||||
createReadLine
|
||||
} from './lib/booter-host-session.js'
|
||||
import {
|
||||
wantPosixSocketFdBridge,
|
||||
wantPosixFcntlBlockingWait,
|
||||
bareOsPosixDgramRecvQueueMax,
|
||||
bareOsPosixDgramRecvBlockMsMax,
|
||||
@@ -4486,77 +4486,10 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
delete this.bareOsLogicalFdFlags[k]
|
||||
}
|
||||
},
|
||||
bareOsPosixFdSimCapBytes() {
|
||||
const raw = String(shellEnv.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 = shellEnv.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 }
|
||||
},
|
||||
...createBareOsPosixFdSimMethods({
|
||||
env: shellEnv,
|
||||
bootHrtimeNowNs
|
||||
}),
|
||||
/**
|
||||
* Logical `sigaction`: `IGNORE` suppresses synthetic delivery for this signal.
|
||||
* @param {string} signal
|
||||
@@ -4583,167 +4516,6 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
}
|
||||
throw new Error('bareOsSigaction: mode must be IGNORE or DEFAULT')
|
||||
},
|
||||
/**
|
||||
* @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'
|
||||
}
|
||||
},
|
||||
/** @type {Record<string, unknown>[]} */
|
||||
bareOsSnapshotHandles: [],
|
||||
/** @param {Record<string, unknown>} desc */
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Pipeline child-ctx clone, raw-chunk capture, and optional stage timeout.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Shallow clone for pipeline **`/bin`** execution: session **`env`**, optional **`shellStdin`**, and
|
||||
* **`bareOsStdoutCaptured`** when stdout is captured (pipe to next stage or **`>`** redirect) so
|
||||
* utilities (e.g. **`ls`**) can use one-record-per-line output.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, string>} env
|
||||
* @param {string | null} stdinText
|
||||
* @param {boolean} bareOsStdoutCaptured
|
||||
*/
|
||||
export function bareOsPipelineChildCtx(ctx, env, stdinText, bareOsStdoutCaptured) {
|
||||
const o = Object.assign({}, ctx, {
|
||||
env: env && typeof env === 'object' ? { ...env } : env,
|
||||
bareOsStdoutCaptured
|
||||
})
|
||||
if (stdinText != null) o.shellStdin = stdinText
|
||||
return o
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert raw binary chunks into shell pipeline capture text.
|
||||
* Pipeline transport is text-based, so bytes are mapped 1:1 via char codes.
|
||||
* @param {string | Uint8Array} chunk
|
||||
*/
|
||||
export function bareOsPipelineRawChunkToText(chunk) {
|
||||
if (typeof chunk === 'string') return chunk
|
||||
if (!(chunk instanceof Uint8Array)) return String(chunk)
|
||||
let s = ''
|
||||
for (let i = 0; i < chunk.length; i++) s += String.fromCharCode(chunk[i])
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional timeout guard for potentially stalled pipeline stages.
|
||||
* @param {Record<string, string>} env
|
||||
* @param {() => Promise<unknown>} run
|
||||
* @param {string} label
|
||||
*/
|
||||
export async function runWithShellPipelineStageTimeout(env, run, label) {
|
||||
const raw = Number.parseInt(String(env.BARE_OS_SHELL_PIPELINE_STAGE_TIMEOUT_MS || ''), 10)
|
||||
const timeoutMs = Number.isFinite(raw) && raw > 0 ? Math.min(raw, 120000) : 0
|
||||
if (!timeoutMs) return await run()
|
||||
/** @type {ReturnType<typeof setTimeout> | null} */
|
||||
let timer = null
|
||||
try {
|
||||
return await Promise.race([
|
||||
run(),
|
||||
new Promise((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
reject(
|
||||
new Error(
|
||||
`shell: pipeline stage timeout (${timeoutMs}ms): ${label}`
|
||||
)
|
||||
)
|
||||
}, timeoutMs)
|
||||
})
|
||||
])
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Small statement helpers: local/declare, limited `[[ ]]`, reserved-word
|
||||
* diagnostics, function positional env, and `case` pattern lists.
|
||||
*/
|
||||
import { expandWord } from './shell-expand.js'
|
||||
|
||||
/**
|
||||
* @typedef {{ type: string, value: string }} ShellStmtToken
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {ShellStmtToken[]} rest
|
||||
* @returns {Promise<'exit' | 'ok'>}
|
||||
*/
|
||||
export async function execShellLocalBuiltin(ctx, rest) {
|
||||
const vfs = ctx.vfs
|
||||
const env = vfs?.env
|
||||
if (!env || typeof env !== 'object') {
|
||||
ctx.exitCode = 0
|
||||
return 'ok'
|
||||
}
|
||||
let n = 0
|
||||
for (const t of rest) {
|
||||
if (++n > 48) break
|
||||
if (t.type !== 'word') continue
|
||||
const eq = t.value.indexOf('=')
|
||||
if (eq <= 0) continue
|
||||
const name = t.value.slice(0, eq).trim()
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue
|
||||
env[name] = expandWord(t.value.slice(eq + 1), env)
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {ShellStmtToken[]} rest
|
||||
* @returns {'ok' | null}
|
||||
*/
|
||||
export function tryExecShellDeclareBuiltin(ctx, rest) {
|
||||
const vfs = ctx.vfs
|
||||
const env = vfs?.env
|
||||
if (!env || typeof env !== 'object') return 'ok'
|
||||
if (rest[0]?.type !== 'word' || rest[0].value !== '-r') return null
|
||||
let n = 0
|
||||
for (let i = 1; i < rest.length; i++) {
|
||||
if (++n > 32) break
|
||||
const t = rest[i]
|
||||
if (t.type !== 'word') continue
|
||||
const eq = t.value.indexOf('=')
|
||||
if (eq <= 0) continue
|
||||
const name = t.value.slice(0, eq).trim()
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue
|
||||
env[name] = expandWord(t.value.slice(eq + 1), env)
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/** Reserved words that cannot begin a simple or compound statement (POSIX-style). */
|
||||
export const BARE_OS_SHELL_MISPLACED_STATEMENT_START = new Set([
|
||||
'then',
|
||||
'else',
|
||||
'elif',
|
||||
'fi',
|
||||
'do',
|
||||
'done',
|
||||
'esac',
|
||||
'in'
|
||||
])
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {ShellStmtToken[]} stmt
|
||||
* @returns {boolean} true when an error was reported (caller should return)
|
||||
*/
|
||||
export function tryReportMisplacedReservedStatementStart(ctx, stmt) {
|
||||
const h = stmt[0]
|
||||
if (!h || h.type !== 'word') return false
|
||||
const w = h.value
|
||||
if (!BARE_OS_SHELL_MISPLACED_STATEMENT_START.has(w)) return false
|
||||
ctx.console.error(
|
||||
`shell: syntax error: reserved word '${w}' cannot start a statement`
|
||||
)
|
||||
ctx.exitCode = 2
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal gated **`[[ … ]]`** — only **`[[ WORD == WORD ]]`** and **`[[ WORD != WORD ]]`**.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {ShellStmtToken[]} stmt
|
||||
* @returns {Promise<'exit' | 'ok'>}
|
||||
*/
|
||||
export async function execDoubleBracketLimited(ctx, stmt) {
|
||||
const vfs = ctx.vfs
|
||||
const env = vfs?.env && typeof vfs.env === 'object' ? vfs.env : {}
|
||||
const last = stmt[stmt.length - 1]
|
||||
if (!last || last.type !== 'word' || last.value !== ']]') {
|
||||
ctx.console.error('shell: [[: missing closing ]]')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
const bodyStart = stmt[0]?.value === '[[' ? 1 : 2
|
||||
const inner = stmt.slice(bodyStart, -1)
|
||||
if (
|
||||
inner.length === 3 &&
|
||||
inner[0].type === 'word' &&
|
||||
inner[1].type === 'word' &&
|
||||
inner[2].type === 'word'
|
||||
) {
|
||||
const op = inner[1].value
|
||||
if (op === '==' || op === '!=') {
|
||||
const a = expandWord(inner[0].value, env)
|
||||
const b = expandWord(inner[2].value, env)
|
||||
ctx.exitCode = op === '==' ? (a === b ? 0 : 1) : a !== b ? 0 : 1
|
||||
return 'ok'
|
||||
}
|
||||
}
|
||||
ctx.console.error(
|
||||
'shell: [[: only `[[ WORD == WORD ]]` and `[[ WORD != WORD ]]` are supported'
|
||||
)
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, string>} env
|
||||
* @param {string[]} argv
|
||||
*/
|
||||
export function assignShellFunctionPositionalEnv(env, argv) {
|
||||
env['0'] = argv[0] || ''
|
||||
const fnArgs = argv.slice(1)
|
||||
for (let i = 1; i <= 9; i++) env[String(i)] = fnArgs[i - 1] ?? ''
|
||||
env['#'] = String(fnArgs.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ShellStmtToken[]} toks
|
||||
* @param {Record<string, string>} env
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function casePatternList(toks, env) {
|
||||
/** @type {string[]} */
|
||||
const out = []
|
||||
/** @type {ShellStmtToken[]} */
|
||||
let cur = []
|
||||
for (const t of toks) {
|
||||
if (t.type === 'op' && t.value === '|') {
|
||||
if (cur.length) {
|
||||
const s = cur.map((w) => w.value).join(' ')
|
||||
out.push(expandWord(s.trim(), env))
|
||||
cur = []
|
||||
}
|
||||
} else if (t.type === 'word') {
|
||||
cur.push(t)
|
||||
}
|
||||
}
|
||||
if (cur.length) {
|
||||
const s = cur.map((w) => w.value).join(' ')
|
||||
out.push(expandWord(s.trim(), env))
|
||||
}
|
||||
return out.filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear simulated background jobs on guest ↔ unlocked transitions (POSIX session model).
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
export function bareOsResetShellIdentityState(ctx) {
|
||||
if (
|
||||
ctx.shellBackgroundJobs &&
|
||||
typeof ctx.shellBackgroundJobs === 'object' &&
|
||||
Array.isArray(ctx.shellBackgroundJobs.list)
|
||||
) {
|
||||
ctx.shellBackgroundJobs.list.length = 0
|
||||
ctx.shellBackgroundJobs.nextId = 1
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,19 @@ import {
|
||||
mergePipelineChildCtx
|
||||
} from './shell-runtime.js'
|
||||
import { expandWord, expandShellWordTokens } from './shell-expand.js'
|
||||
import {
|
||||
bareOsPipelineChildCtx,
|
||||
bareOsPipelineRawChunkToText,
|
||||
runWithShellPipelineStageTimeout
|
||||
} from './shell-pipeline.js'
|
||||
import {
|
||||
execShellLocalBuiltin,
|
||||
tryExecShellDeclareBuiltin,
|
||||
tryReportMisplacedReservedStatementStart,
|
||||
execDoubleBracketLimited,
|
||||
assignShellFunctionPositionalEnv,
|
||||
casePatternList
|
||||
} from './shell-stmt.js'
|
||||
|
||||
export { BARE_OS_SHELL_NOUNSET_ERROR }
|
||||
export {
|
||||
@@ -87,6 +100,7 @@ export {
|
||||
planShellRedirections,
|
||||
buildShellExecutionGraph
|
||||
} from './shell-parse.js'
|
||||
export { bareOsResetShellIdentityState } from './shell-stmt.js'
|
||||
|
||||
/** Shell planner/executor continues below. */
|
||||
|
||||
@@ -120,67 +134,6 @@ export async function dispatchShellTrapSignal(ctx, signal) {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Shallow clone for pipeline **`/bin`** execution: session **`env`**, optional **`shellStdin`**, and
|
||||
* **`bareOsStdoutCaptured`** when stdout is captured (pipe to next stage or **`>`** redirect) so
|
||||
* utilities (e.g. **`ls`**) can use one-record-per-line output.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, string>} env
|
||||
* @param {string | null} stdinText
|
||||
* @param {boolean} bareOsStdoutCaptured
|
||||
*/
|
||||
function bareOsPipelineChildCtx(ctx, env, stdinText, bareOsStdoutCaptured) {
|
||||
const o = Object.assign({}, ctx, {
|
||||
env: env && typeof env === 'object' ? { ...env } : env,
|
||||
bareOsStdoutCaptured
|
||||
})
|
||||
if (stdinText != null) o.shellStdin = stdinText
|
||||
return o
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert raw binary chunks into shell pipeline capture text.
|
||||
* Pipeline transport is text-based, so bytes are mapped 1:1 via char codes.
|
||||
* @param {string | Uint8Array} chunk
|
||||
*/
|
||||
function bareOsPipelineRawChunkToText(chunk) {
|
||||
if (typeof chunk === 'string') return chunk
|
||||
if (!(chunk instanceof Uint8Array)) return String(chunk)
|
||||
let s = ''
|
||||
for (let i = 0; i < chunk.length; i++) s += String.fromCharCode(chunk[i])
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional timeout guard for potentially stalled pipeline stages.
|
||||
* @param {Record<string, string>} env
|
||||
* @param {() => Promise<unknown>} run
|
||||
* @param {string} label
|
||||
*/
|
||||
async function runWithShellPipelineStageTimeout(env, run, label) {
|
||||
const raw = Number.parseInt(String(env.BARE_OS_SHELL_PIPELINE_STAGE_TIMEOUT_MS || ''), 10)
|
||||
const timeoutMs = Number.isFinite(raw) && raw > 0 ? Math.min(raw, 120000) : 0
|
||||
if (!timeoutMs) return await run()
|
||||
/** @type {ReturnType<typeof setTimeout> | null} */
|
||||
let timer = null
|
||||
try {
|
||||
return await Promise.race([
|
||||
run(),
|
||||
new Promise((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
reject(
|
||||
new Error(
|
||||
`shell: pipeline stage timeout (${timeoutMs}ms): ${label}`
|
||||
)
|
||||
)
|
||||
}, timeoutMs)
|
||||
})
|
||||
])
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {SimpleCmd[]} pipeline
|
||||
@@ -1569,34 +1522,6 @@ async function execForConstruct(ctx, tokens) {
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Token[]} toks
|
||||
* @param {Record<string, string>} env
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function casePatternList(toks, env) {
|
||||
/** @type {string[]} */
|
||||
const out = []
|
||||
/** @type {Token[]} */
|
||||
let cur = []
|
||||
for (const t of toks) {
|
||||
if (t.type === 'op' && t.value === '|') {
|
||||
if (cur.length) {
|
||||
const s = cur.map((w) => w.value).join(' ')
|
||||
out.push(expandWord(s.trim(), env))
|
||||
cur = []
|
||||
}
|
||||
} else if (t.type === 'word') {
|
||||
cur.push(t)
|
||||
}
|
||||
}
|
||||
if (cur.length) {
|
||||
const s = cur.map((w) => w.value).join(' ')
|
||||
out.push(expandWord(s.trim(), env))
|
||||
}
|
||||
return out.filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* `case WORD in pattern) list ;; … esac` — bounded branches; patterns support `|` alternation and `*`.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
@@ -1692,128 +1617,6 @@ async function execCaseConstruct(ctx, tokens) {
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Token[]} stmt
|
||||
* @returns {Promise<'exit' | 'ok'>}
|
||||
*/
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {{ type: string, value: string }[]} rest
|
||||
*/
|
||||
async function execShellLocalBuiltin(ctx, rest) {
|
||||
const vfs = ctx.vfs
|
||||
const env = vfs?.env
|
||||
if (!env || typeof env !== 'object') {
|
||||
ctx.exitCode = 0
|
||||
return 'ok'
|
||||
}
|
||||
let n = 0
|
||||
for (const t of rest) {
|
||||
if (++n > 48) break
|
||||
if (t.type !== 'word') continue
|
||||
const eq = t.value.indexOf('=')
|
||||
if (eq <= 0) continue
|
||||
const name = t.value.slice(0, eq).trim()
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue
|
||||
env[name] = expandWord(t.value.slice(eq + 1), env)
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {{ type: string, value: string }[]} rest
|
||||
* @returns {'ok' | null}
|
||||
*/
|
||||
function tryExecShellDeclareBuiltin(ctx, rest) {
|
||||
const vfs = ctx.vfs
|
||||
const env = vfs?.env
|
||||
if (!env || typeof env !== 'object') return 'ok'
|
||||
if (rest[0]?.type !== 'word' || rest[0].value !== '-r') return null
|
||||
let n = 0
|
||||
for (let i = 1; i < rest.length; i++) {
|
||||
if (++n > 32) break
|
||||
const t = rest[i]
|
||||
if (t.type !== 'word') continue
|
||||
const eq = t.value.indexOf('=')
|
||||
if (eq <= 0) continue
|
||||
const name = t.value.slice(0, eq).trim()
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue
|
||||
env[name] = expandWord(t.value.slice(eq + 1), env)
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/** Reserved words that cannot begin a simple or compound statement (POSIX-style). */
|
||||
const BARE_OS_SHELL_MISPLACED_STATEMENT_START = new Set([
|
||||
'then',
|
||||
'else',
|
||||
'elif',
|
||||
'fi',
|
||||
'do',
|
||||
'done',
|
||||
'esac',
|
||||
'in'
|
||||
])
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Token[]} stmt
|
||||
* @returns {boolean} true when an error was reported (caller should return)
|
||||
*/
|
||||
function tryReportMisplacedReservedStatementStart(ctx, stmt) {
|
||||
const h = stmt[0]
|
||||
if (!h || h.type !== 'word') return false
|
||||
const w = h.value
|
||||
if (!BARE_OS_SHELL_MISPLACED_STATEMENT_START.has(w)) return false
|
||||
ctx.console.error(
|
||||
`shell: syntax error: reserved word '${w}' cannot start a statement`
|
||||
)
|
||||
ctx.exitCode = 2
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal gated **`[[ … ]]`** — only **`[[ WORD == WORD ]]`** and **`[[ WORD != WORD ]]`**.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Token[]} stmt
|
||||
* @returns {Promise<'exit' | 'ok'>}
|
||||
*/
|
||||
async function execDoubleBracketLimited(ctx, stmt) {
|
||||
const vfs = ctx.vfs
|
||||
const env = vfs?.env && typeof vfs.env === 'object' ? vfs.env : {}
|
||||
const last = stmt[stmt.length - 1]
|
||||
if (!last || last.type !== 'word' || last.value !== ']]') {
|
||||
ctx.console.error('shell: [[: missing closing ]]')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
const bodyStart = stmt[0]?.value === '[[' ? 1 : 2
|
||||
const inner = stmt.slice(bodyStart, -1)
|
||||
if (
|
||||
inner.length === 3 &&
|
||||
inner[0].type === 'word' &&
|
||||
inner[1].type === 'word' &&
|
||||
inner[2].type === 'word'
|
||||
) {
|
||||
const op = inner[1].value
|
||||
if (op === '==' || op === '!=') {
|
||||
const a = expandWord(inner[0].value, env)
|
||||
const b = expandWord(inner[2].value, env)
|
||||
ctx.exitCode = op === '==' ? (a === b ? 0 : 1) : a !== b ? 0 : 1
|
||||
return 'ok'
|
||||
}
|
||||
}
|
||||
ctx.console.error(
|
||||
'shell: [[: only `[[ WORD == WORD ]]` and `[[ WORD != WORD ]]` are supported'
|
||||
)
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
async function dispatchShellStatement(ctx, stmt) {
|
||||
/** Normalize inline brace-expression tokens like `{1..5}` into a word token so
|
||||
* they use normal shell word expansion instead of statement/group operators. */
|
||||
@@ -2091,17 +1894,6 @@ async function execAndOrList(ctx, tokens) {
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, string>} env
|
||||
* @param {string[]} argv
|
||||
*/
|
||||
function assignShellFunctionPositionalEnv(env, argv) {
|
||||
env['0'] = argv[0] || ''
|
||||
const fnArgs = argv.slice(1)
|
||||
for (let i = 1; i <= 9; i++) env[String(i)] = fnArgs[i - 1] ?? ''
|
||||
env['#'] = String(fnArgs.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} line
|
||||
@@ -2292,18 +2084,3 @@ async function execShellLineInner(ctx, rawTrimmed) {
|
||||
syncBareOsExitStatusEnv(ctx)
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear simulated background jobs on guest ↔ unlocked transitions (POSIX session model).
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
export function bareOsResetShellIdentityState(ctx) {
|
||||
if (
|
||||
ctx.shellBackgroundJobs &&
|
||||
typeof ctx.shellBackgroundJobs === 'object' &&
|
||||
Array.isArray(ctx.shellBackgroundJobs.list)
|
||||
) {
|
||||
ctx.shellBackgroundJobs.list.length = 0
|
||||
ctx.shellBackgroundJobs.nextId = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* VFS mount / mirror / tilde / system-RO alias routers used by createVfs.
|
||||
*/
|
||||
import unixPathResolve from 'unix-path-resolve'
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* getCwd: () => string,
|
||||
* normalizeHome: () => string,
|
||||
* mntRef?: { getMounts?: () => Map<string, { drive: unknown, writable: boolean }> } | null,
|
||||
* systemRoAliasNorm: string,
|
||||
* getAuxiliaryMountLines?: (() => (string | null | undefined)[]) | null,
|
||||
* getAuxiliaryDrives?: (() => unknown[] | null | undefined) | null,
|
||||
* systemDrive: unknown
|
||||
* }} deps
|
||||
*/
|
||||
export function createVfsRouteHelpers(deps) {
|
||||
const {
|
||||
getCwd,
|
||||
normalizeHome,
|
||||
mntRef = null,
|
||||
systemRoAliasNorm,
|
||||
getAuxiliaryMountLines = null,
|
||||
getAuxiliaryDrives = null,
|
||||
systemDrive
|
||||
} = deps
|
||||
|
||||
/**
|
||||
* `cd ~`, `touch ~/x`, etc. must map to $HOME — otherwise `unix-path-resolve(cwd, '~')`
|
||||
* becomes `/~` on the system drive (read-only).
|
||||
*/
|
||||
function expandTilde(userPath) {
|
||||
const h = normalizeHome()
|
||||
if (userPath === '~') return h
|
||||
if (userPath.startsWith('~/')) {
|
||||
const rest = userPath.slice(2)
|
||||
return rest ? unixPathResolve(h, rest) : h
|
||||
}
|
||||
return userPath
|
||||
}
|
||||
|
||||
/** Logical absolute path from cwd + user path */
|
||||
function resolveLogical(userPath) {
|
||||
const expanded = expandTilde(userPath)
|
||||
return unixPathResolve(getCwd(), expanded)
|
||||
}
|
||||
|
||||
function getMntMap() {
|
||||
if (mntRef && typeof mntRef.getMounts === 'function')
|
||||
return mntRef.getMounts()
|
||||
return new Map()
|
||||
}
|
||||
|
||||
/** Linux-shaped mount table: root + HDMS `/mnt/<label>` rows (best-effort). */
|
||||
function pseudoMountsText() {
|
||||
const lines = ['bare-os-root / hyperdrive ro 0 0']
|
||||
if (systemRoAliasNorm)
|
||||
lines.push(
|
||||
`bare-os-system-ro-alias ${systemRoAliasNorm} hyperdrive ro 0 0`
|
||||
)
|
||||
const map = getMntMap()
|
||||
const keys = [...map.keys()].sort((a, b) => a.localeCompare(b))
|
||||
for (const label of keys) {
|
||||
const ent = map.get(label)
|
||||
if (!ent) continue
|
||||
const rw = ent.writable ? 'rw' : 'ro'
|
||||
lines.push(`bare-os-${label} /mnt/${label} hyperdrive ${rw} 0 0`)
|
||||
}
|
||||
try {
|
||||
const extra = getAuxiliaryMountLines ? getAuxiliaryMountLines() : []
|
||||
if (Array.isArray(extra)) {
|
||||
for (const row of extra) {
|
||||
if (typeof row === 'string' && row.trim()) lines.push(row.trim())
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return lines.join('\n') + '\n'
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional read-only auxiliary Hyperdrives under `/mirror/aux0`, `/mirror/aux1`, …
|
||||
* @returns {null | Record<string, unknown>}
|
||||
*/
|
||||
function routeMirror(absPath) {
|
||||
const aux = getAuxiliaryDrives ? getAuxiliaryDrives() : null
|
||||
if (!Array.isArray(aux) || aux.length === 0) return null
|
||||
if (absPath === '/mirror' || absPath === '/mirror/') {
|
||||
return { virtualMirrorRoot: true }
|
||||
}
|
||||
if (!absPath.startsWith('/mirror/')) return null
|
||||
const rest = absPath.slice('/mirror/'.length)
|
||||
const slash = rest.indexOf('/')
|
||||
const seg = slash === -1 ? rest : rest.slice(0, slash)
|
||||
const tail = slash === -1 ? '' : rest.slice(slash + 1)
|
||||
if (!seg) return { virtualMirrorRoot: true }
|
||||
const m = /^aux(\d+)$/.exec(seg)
|
||||
if (!m) {
|
||||
return { drive: systemDrive, path: absPath }
|
||||
}
|
||||
const idx = Number(m[1])
|
||||
const drive = /** @type {unknown} */ (aux[idx])
|
||||
if (
|
||||
!drive ||
|
||||
typeof (/** @type {{ get?: unknown }} */ (drive).get) !== 'function'
|
||||
) {
|
||||
return { drive: systemDrive, path: absPath }
|
||||
}
|
||||
const sub = tail ? '/' + tail.replace(/^\/+/, '') : '/'
|
||||
const p = unixPathResolve('/', sub)
|
||||
return {
|
||||
drive: /** @type {import('hyperdrive').default} */ (drive),
|
||||
path: p,
|
||||
mntReadOnly: true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {null | Record<string, unknown>}
|
||||
*/
|
||||
function routeMnt(absPath) {
|
||||
if (absPath === '/mnt' || absPath === '/mnt/') {
|
||||
return { virtualMntRoot: true }
|
||||
}
|
||||
if (!absPath.startsWith('/mnt/')) return null
|
||||
const rest = absPath.slice(5)
|
||||
const slash = rest.indexOf('/')
|
||||
const label = slash === -1 ? rest : rest.slice(0, slash)
|
||||
const tail = slash === -1 ? '' : rest.slice(slash + 1)
|
||||
if (!label) return { virtualMntRoot: true }
|
||||
const mounts = getMntMap()
|
||||
const ent = mounts.get(label)
|
||||
if (!ent) {
|
||||
return { drive: systemDrive, path: absPath }
|
||||
}
|
||||
const sub = tail ? '/' + tail.replace(/^\/+/, '') : '/'
|
||||
const p = unixPathResolve('/', sub)
|
||||
return { drive: ent.drive, path: p, mntReadOnly: !ent.writable }
|
||||
}
|
||||
|
||||
/**
|
||||
* Map logical absolute path to { drive, path } for Hyperdrive ops.
|
||||
* Virtual /home lists only the active session dir; /home/<active>/… is the personal drive.
|
||||
*/
|
||||
function routeSystemRoAlias(absPath) {
|
||||
if (!systemRoAliasNorm || !systemRoAliasNorm.startsWith('/')) return null
|
||||
const norm = absPath.replace(/\/+$/, '') || '/'
|
||||
if (
|
||||
norm !== systemRoAliasNorm &&
|
||||
!absPath.startsWith(systemRoAliasNorm + '/')
|
||||
)
|
||||
return null
|
||||
let tail = '/'
|
||||
if (norm !== systemRoAliasNorm) {
|
||||
tail = absPath.slice(systemRoAliasNorm.length) || '/'
|
||||
}
|
||||
const p = tail.startsWith('/') ? tail : '/' + tail
|
||||
const resolved = unixPathResolve('/', p.replace(/^\/+/, '') || '/')
|
||||
return {
|
||||
drive: systemDrive,
|
||||
path: resolved,
|
||||
mntReadOnly: true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When HDMS (or other) mounts label `www` at `/mnt/www`, expose it at `$HOME/.www`.
|
||||
* @param {string} relFromHomeRoot path under the session home root (e.g. `.www`, `.www/a`)
|
||||
* @returns {{ drive: unknown, path: string, mntReadOnly: boolean } | null}
|
||||
*/
|
||||
function routeHomeWwwAlias(relFromHomeRoot) {
|
||||
const rel = String(relFromHomeRoot || '').replace(/^\/+/, '')
|
||||
if (rel !== '.www' && !rel.startsWith('.www/')) return null
|
||||
const mounts = getMntMap()
|
||||
const ent = mounts.get('www')
|
||||
if (!ent) return null
|
||||
const wwwPrefix = '.www/'
|
||||
const tail =
|
||||
rel === '.www'
|
||||
? ''
|
||||
: rel.startsWith(wwwPrefix)
|
||||
? rel.slice(wwwPrefix.length)
|
||||
: null
|
||||
if (tail === null && rel !== '.www') return null
|
||||
const sub = tail ? '/' + tail.replace(/^\/+/, '') : '/'
|
||||
const p = unixPathResolve('/', sub)
|
||||
return {
|
||||
drive: ent.drive,
|
||||
path: p,
|
||||
mntReadOnly: !ent.writable
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
expandTilde,
|
||||
resolveLogical,
|
||||
getMntMap,
|
||||
pseudoMountsText,
|
||||
routeMirror,
|
||||
routeMnt,
|
||||
routeSystemRoAlias,
|
||||
routeHomeWwwAlias
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
assertNotBootPolicyDenyVfs as assertNotBootPolicyDenyVfsPolicy
|
||||
} from './vfs-policy.js'
|
||||
import { createVfsWarmReadCache } from './vfs-warm-cache.js'
|
||||
import { createVfsRouteHelpers } from './vfs-route.js'
|
||||
import {
|
||||
BARE_OS_PROC_FILE_TO_ID_REPLICATION_OPERATOR_SURFACE,
|
||||
BARE_OS_PROC_FILE_TO_ID_PEAR_CORESTORE_HRPC,
|
||||
@@ -625,6 +626,24 @@ export function createVfs(
|
||||
})
|
||||
let cwd = env.PWD || HOME()
|
||||
|
||||
const {
|
||||
resolveLogical,
|
||||
getMntMap,
|
||||
pseudoMountsText,
|
||||
routeMirror,
|
||||
routeMnt,
|
||||
routeSystemRoAlias,
|
||||
routeHomeWwwAlias
|
||||
} = createVfsRouteHelpers({
|
||||
getCwd: () => cwd,
|
||||
normalizeHome,
|
||||
mntRef,
|
||||
systemRoAliasNorm,
|
||||
getAuxiliaryMountLines,
|
||||
getAuxiliaryDrives,
|
||||
systemDrive
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {unknown} drive
|
||||
* @param {string} personalPath
|
||||
@@ -1773,173 +1792,6 @@ export function createVfs(
|
||||
return st
|
||||
}
|
||||
|
||||
/**
|
||||
* `cd ~`, `touch ~/x`, etc. must map to $HOME — otherwise `unix-path-resolve(cwd, '~')`
|
||||
* becomes `/~` on the system drive (read-only).
|
||||
*/
|
||||
function expandTilde(userPath) {
|
||||
const h = normalizeHome()
|
||||
if (userPath === '~') return h
|
||||
if (userPath.startsWith('~/')) {
|
||||
const rest = userPath.slice(2)
|
||||
return rest ? unixPathResolve(h, rest) : h
|
||||
}
|
||||
return userPath
|
||||
}
|
||||
|
||||
/** Logical absolute path from cwd + user path */
|
||||
function resolveLogical(userPath) {
|
||||
const expanded = expandTilde(userPath)
|
||||
return unixPathResolve(cwd, expanded)
|
||||
}
|
||||
|
||||
function getMntMap() {
|
||||
if (mntRef && typeof mntRef.getMounts === 'function')
|
||||
return mntRef.getMounts()
|
||||
return new Map()
|
||||
}
|
||||
|
||||
/** Linux-shaped mount table: root + HDMS `/mnt/<label>` rows (best-effort). */
|
||||
function pseudoMountsText() {
|
||||
const lines = ['bare-os-root / hyperdrive ro 0 0']
|
||||
if (systemRoAliasNorm)
|
||||
lines.push(
|
||||
`bare-os-system-ro-alias ${systemRoAliasNorm} hyperdrive ro 0 0`
|
||||
)
|
||||
const map = getMntMap()
|
||||
const keys = [...map.keys()].sort((a, b) => a.localeCompare(b))
|
||||
for (const label of keys) {
|
||||
const ent = map.get(label)
|
||||
if (!ent) continue
|
||||
const rw = ent.writable ? 'rw' : 'ro'
|
||||
lines.push(`bare-os-${label} /mnt/${label} hyperdrive ${rw} 0 0`)
|
||||
}
|
||||
try {
|
||||
const extra = getAuxiliaryMountLines ? getAuxiliaryMountLines() : []
|
||||
if (Array.isArray(extra)) {
|
||||
for (const row of extra) {
|
||||
if (typeof row === 'string' && row.trim()) lines.push(row.trim())
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return lines.join('\n') + '\n'
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional read-only auxiliary Hyperdrives under `/mirror/aux0`, `/mirror/aux1`, …
|
||||
* @returns {null | Record<string, unknown>}
|
||||
*/
|
||||
function routeMirror(absPath) {
|
||||
const aux = getAuxiliaryDrives ? getAuxiliaryDrives() : null
|
||||
if (!Array.isArray(aux) || aux.length === 0) return null
|
||||
if (absPath === '/mirror' || absPath === '/mirror/') {
|
||||
return { virtualMirrorRoot: true }
|
||||
}
|
||||
if (!absPath.startsWith('/mirror/')) return null
|
||||
const rest = absPath.slice('/mirror/'.length)
|
||||
const slash = rest.indexOf('/')
|
||||
const seg = slash === -1 ? rest : rest.slice(0, slash)
|
||||
const tail = slash === -1 ? '' : rest.slice(slash + 1)
|
||||
if (!seg) return { virtualMirrorRoot: true }
|
||||
const m = /^aux(\d+)$/.exec(seg)
|
||||
if (!m) {
|
||||
return { drive: systemDrive, path: absPath }
|
||||
}
|
||||
const idx = Number(m[1])
|
||||
const drive = /** @type {unknown} */ (aux[idx])
|
||||
if (
|
||||
!drive ||
|
||||
typeof (/** @type {{ get?: unknown }} */ (drive).get) !== 'function'
|
||||
) {
|
||||
return { drive: systemDrive, path: absPath }
|
||||
}
|
||||
const sub = tail ? '/' + tail.replace(/^\/+/, '') : '/'
|
||||
const p = unixPathResolve('/', sub)
|
||||
return {
|
||||
drive: /** @type {import('hyperdrive').default} */ (drive),
|
||||
path: p,
|
||||
mntReadOnly: true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {null | Record<string, unknown>}
|
||||
*/
|
||||
function routeMnt(absPath) {
|
||||
if (absPath === '/mnt' || absPath === '/mnt/') {
|
||||
return { virtualMntRoot: true }
|
||||
}
|
||||
if (!absPath.startsWith('/mnt/')) return null
|
||||
const rest = absPath.slice(5)
|
||||
const slash = rest.indexOf('/')
|
||||
const label = slash === -1 ? rest : rest.slice(0, slash)
|
||||
const tail = slash === -1 ? '' : rest.slice(slash + 1)
|
||||
if (!label) return { virtualMntRoot: true }
|
||||
const mounts = getMntMap()
|
||||
const ent = mounts.get(label)
|
||||
if (!ent) {
|
||||
return { drive: systemDrive, path: absPath }
|
||||
}
|
||||
const sub = tail ? '/' + tail.replace(/^\/+/, '') : '/'
|
||||
const p = unixPathResolve('/', sub)
|
||||
return { drive: ent.drive, path: p, mntReadOnly: !ent.writable }
|
||||
}
|
||||
|
||||
/**
|
||||
* Map logical absolute path to { drive, path } for Hyperdrive ops.
|
||||
* Virtual /home lists only the active session dir; /home/<active>/… is the personal drive.
|
||||
*/
|
||||
function routeSystemRoAlias(absPath) {
|
||||
if (!systemRoAliasNorm || !systemRoAliasNorm.startsWith('/')) return null
|
||||
const norm = absPath.replace(/\/+$/, '') || '/'
|
||||
if (
|
||||
norm !== systemRoAliasNorm &&
|
||||
!absPath.startsWith(systemRoAliasNorm + '/')
|
||||
)
|
||||
return null
|
||||
let tail = '/'
|
||||
if (norm !== systemRoAliasNorm) {
|
||||
tail = absPath.slice(systemRoAliasNorm.length) || '/'
|
||||
}
|
||||
const p = tail.startsWith('/') ? tail : '/' + tail
|
||||
const resolved = unixPathResolve('/', p.replace(/^\/+/, '') || '/')
|
||||
return {
|
||||
drive: systemDrive,
|
||||
path: resolved,
|
||||
mntReadOnly: true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When HDMS (or other) mounts label `www` at `/mnt/www`, expose it at `$HOME/.www`.
|
||||
* @param {string} relFromHomeRoot path under the session home root (e.g. `.www`, `.www/a`)
|
||||
* @returns {{ drive: import('hyperdrive').default, path: string, mntReadOnly: boolean } | null}
|
||||
*/
|
||||
function routeHomeWwwAlias(relFromHomeRoot) {
|
||||
const rel = String(relFromHomeRoot || '').replace(/^\/+/, '')
|
||||
if (rel !== '.www' && !rel.startsWith('.www/')) return null
|
||||
const mounts = getMntMap()
|
||||
const ent = mounts.get('www')
|
||||
if (!ent) return null
|
||||
const wwwPrefix = '.www/'
|
||||
const tail =
|
||||
rel === '.www'
|
||||
? ''
|
||||
: rel.startsWith(wwwPrefix)
|
||||
? rel.slice(wwwPrefix.length)
|
||||
: null
|
||||
if (tail === null && rel !== '.www') return null
|
||||
const sub = tail ? '/' + tail.replace(/^\/+/, '') : '/'
|
||||
const p = unixPathResolve('/', sub)
|
||||
return {
|
||||
drive: ent.drive,
|
||||
path: p,
|
||||
mntReadOnly: !ent.writable
|
||||
}
|
||||
}
|
||||
|
||||
function route(absPath) {
|
||||
const aliasR = routeSystemRoAlias(absPath)
|
||||
if (aliasR) return aliasR
|
||||
|
||||
@@ -72,6 +72,20 @@ import {
|
||||
} from './lib/vfs-policy.js'
|
||||
import { createVfsWarmReadCache } from './lib/vfs-warm-cache.js'
|
||||
import { createSessionReadMaskedLine } from './lib/cli-readline.js'
|
||||
import {
|
||||
bareOsPipelineChildCtx,
|
||||
bareOsPipelineRawChunkToText,
|
||||
runWithShellPipelineStageTimeout
|
||||
} from './lib/shell-pipeline.js'
|
||||
import {
|
||||
assignShellFunctionPositionalEnv,
|
||||
tryReportMisplacedReservedStatementStart,
|
||||
casePatternList,
|
||||
execDoubleBracketLimited,
|
||||
bareOsResetShellIdentityState
|
||||
} from './lib/shell-stmt.js'
|
||||
import { createVfsRouteHelpers } from './lib/vfs-route.js'
|
||||
import { createBareOsPosixFdSimMethods } from './lib/bare-os-posix-fd-sim.js'
|
||||
|
||||
test('posix env flags and caps', (t) => {
|
||||
t.ok(wantPosixSocketFdBridge({ BARE_OS_POSIX_SOCKET_FD_BRIDGE: '1' }))
|
||||
@@ -474,3 +488,97 @@ test('session masked line falls back without TTY', async (t) => {
|
||||
})
|
||||
t.is(await read('pw> '), 'fb:pw> ')
|
||||
})
|
||||
|
||||
test('pipeline child ctx chunk and timeout', async (t) => {
|
||||
const child = bareOsPipelineChildCtx({ a: 1 }, { X: '1' }, 'in', true)
|
||||
t.is(child.shellStdin, 'in')
|
||||
t.ok(child.bareOsStdoutCaptured)
|
||||
t.is(child.env.X, '1')
|
||||
t.is(bareOsPipelineRawChunkToText('ab'), 'ab')
|
||||
t.is(bareOsPipelineRawChunkToText(new Uint8Array([65, 66])), 'AB')
|
||||
const v = await runWithShellPipelineStageTimeout({}, async () => 7, 'x')
|
||||
t.is(v, 7)
|
||||
})
|
||||
|
||||
test('shell stmt helpers local-style', async (t) => {
|
||||
const env = {}
|
||||
assignShellFunctionPositionalEnv(env, ['fn', 'a', 'b'])
|
||||
t.is(env['0'], 'fn')
|
||||
t.is(env['1'], 'a')
|
||||
t.is(env['2'], 'b')
|
||||
t.is(env['#'], '2')
|
||||
const errs = []
|
||||
const ctx = { console: { error: (m) => errs.push(m) }, exitCode: 0 }
|
||||
t.ok(tryReportMisplacedReservedStatementStart(ctx, [{ type: 'word', value: 'then' }]))
|
||||
t.is(ctx.exitCode, 2)
|
||||
t.alike(casePatternList([{ type: 'word', value: 'x*' }, { type: 'op', value: '|' }, { type: 'word', value: 'y' }], {}), [
|
||||
'x*',
|
||||
'y'
|
||||
])
|
||||
const br = {
|
||||
vfs: { env: { A: 'hi' } },
|
||||
console: { error: () => {} },
|
||||
exitCode: 99
|
||||
}
|
||||
await execDoubleBracketLimited(br, [
|
||||
{ type: 'word', value: '[[' },
|
||||
{ type: 'word', value: '$A' },
|
||||
{ type: 'word', value: '==' },
|
||||
{ type: 'word', value: 'hi' },
|
||||
{ type: 'word', value: ']]' }
|
||||
])
|
||||
t.is(br.exitCode, 0)
|
||||
const jobs = { shellBackgroundJobs: { nextId: 9, list: [1] } }
|
||||
bareOsResetShellIdentityState(jobs)
|
||||
t.is(jobs.shellBackgroundJobs.nextId, 1)
|
||||
t.is(jobs.shellBackgroundJobs.list.length, 0)
|
||||
})
|
||||
|
||||
test('vfs route tilde mnt and ro alias', (t) => {
|
||||
const helpers = createVfsRouteHelpers({
|
||||
getCwd: () => '/home/g',
|
||||
normalizeHome: () => '/home/g',
|
||||
mntRef: {
|
||||
getMounts: () =>
|
||||
new Map([['data', { drive: { get: () => 1 }, writable: true }]])
|
||||
},
|
||||
systemRoAliasNorm: '/sysro',
|
||||
systemDrive: { id: 'sys' }
|
||||
})
|
||||
t.is(helpers.expandTilde('~/x'), '/home/g/x')
|
||||
t.is(helpers.resolveLogical('y'), '/home/g/y')
|
||||
t.ok(helpers.routeMnt('/mnt').virtualMntRoot)
|
||||
t.is(helpers.routeMnt('/mnt/data/a').path, '/a')
|
||||
t.ok(helpers.routeSystemRoAlias('/sysro/etc').mntReadOnly)
|
||||
t.ok(helpers.pseudoMountsText().includes('/mnt/data'))
|
||||
})
|
||||
|
||||
test('posix fd sim pipe write read', (t) => {
|
||||
const methods = createBareOsPosixFdSimMethods({
|
||||
env: { BARE_OS_POSIX_FD_SIM: '1' },
|
||||
bootHrtimeNowNs: null
|
||||
})
|
||||
const ctx = {
|
||||
bareOsLogicalFds: {},
|
||||
bareOsLogicalFdFlags: {},
|
||||
bareOsPosixFdSimState: {
|
||||
nextFd: 10,
|
||||
nextPairId: 1,
|
||||
queues: Object.create(null),
|
||||
byteTotals: Object.create(null)
|
||||
},
|
||||
bareOsRegisterLogicalFd(fd, target) {
|
||||
this.bareOsLogicalFds[String(fd)] = target
|
||||
},
|
||||
...methods
|
||||
}
|
||||
const pipe = ctx.bareOsPosixFdSimPipe()
|
||||
t.ok(pipe)
|
||||
const w = ctx.bareOsPosixFdSimWrite(pipe.writeFd, 'hi')
|
||||
t.ok(w.ok)
|
||||
const r = ctx.bareOsPosixFdSimRead(pipe.readFd)
|
||||
t.ok(r.ok)
|
||||
t.is(r.data, 'hi')
|
||||
const probe = ctx.bareOsPosixPollProbe({ fds: [{ fd: 1, events: 'w' }] })
|
||||
t.ok(probe.ready.some((x) => x.fd === 1 && x.revents === 'w'))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user