2453 lines
77 KiB
JavaScript
2453 lines
77 KiB
JavaScript
/**
|
||
* POSIX-like syscall facade used as `ctx.bareOsSyscall`.
|
||
* Factory closes over executeKernel runtime objects; method body uses `this` as ctx.
|
||
*/
|
||
import b4a from 'b4a'
|
||
import { persistBareOsMountsSnapshot } from './bare-os-mounts-persist.js'
|
||
import {
|
||
bindFallbackEnabled,
|
||
isAddrInUse,
|
||
readListeningTcpPort
|
||
} from './bare-os-bind-fallback.js'
|
||
import {
|
||
bareOsTryAcquireCooperativeLock,
|
||
bareOsRemoveFcntlWaiter,
|
||
bareOsDrainFcntlLockWaiters
|
||
} from './bare-os-fcntl-cooperative-lock.js'
|
||
import {
|
||
wantPosixSocketFdBridge,
|
||
wantPosixFcntlBlockingWait,
|
||
bareOsPosixDgramRecvQueueMax,
|
||
bareOsPosixDgramRecvBlockMsMax,
|
||
bareOsPosixAcceptQueueMax,
|
||
bareOsPosixAcceptBlockMsMax,
|
||
bareOsFcntlLockWaitMsMax,
|
||
bareOsResolveCooperativeLockPath
|
||
} from './bare-os-posix-syscall-helpers.js'
|
||
import {
|
||
bareOsShutdownSocketBridgeFd,
|
||
prepareBridgeSendmsg
|
||
} from './bare-os-socket-bridge.js'
|
||
|
||
/**
|
||
* @param {{
|
||
* shellEnv: Record<string, string | undefined>,
|
||
* bootHrtimeNowNs: (() => bigint) | null,
|
||
* bareLibrary: Record<string, unknown>,
|
||
* bareOsIpc: unknown,
|
||
* hdmsController: unknown,
|
||
* vfs: unknown
|
||
* }} deps
|
||
*/
|
||
export function createBareOsSyscall(deps) {
|
||
const {
|
||
shellEnv,
|
||
bootHrtimeNowNs,
|
||
bareLibrary,
|
||
bareOsIpc,
|
||
hdmsController,
|
||
vfs
|
||
} = deps
|
||
|
||
/**
|
||
* Minimal syscall-shaped facade for POSIX-like userland adaptation.
|
||
* @param {string} op
|
||
* @param {Record<string, unknown>} [args]
|
||
*/
|
||
return async function bareOsSyscall(op, args = {}) {
|
||
const name = String(op || '').trim()
|
||
if (!name) throw new Error('bareOsSyscall: op is required')
|
||
if (name === 'pathconf') {
|
||
const pconfName = String(args.name || args.key || '').trim()
|
||
const v = this.bareOsPathconf(String(args.path || '/'), pconfName)
|
||
return { name: pconfName, value: v }
|
||
}
|
||
if (name === 'fsync' || name === 'fdatasync') {
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
posixAlignment: 'partial',
|
||
note: 'Guest logical fsync/fdatasync is a no-op; replication and block persistence are handled by the host Hyperdrive stack.'
|
||
}
|
||
}
|
||
if (name === 'posix_fadvise') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/')) {
|
||
throw new Error(
|
||
'bareOsSyscall: posix_fadvise: absolute path required'
|
||
)
|
||
}
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
path: p,
|
||
offset: Number(args.offset ?? 0) || 0,
|
||
len: Number(args.len ?? args.length ?? 0) || 0,
|
||
advice: Number(args.advice ?? args.adv ?? 0) || 0,
|
||
posixAlignment: 'simulated',
|
||
note: 'posix_fadvise is a logical no-op on Hyperdrive-backed paths; use for portable hints only.'
|
||
}
|
||
}
|
||
if (name === 'nanosleep') {
|
||
const sec = Number(args.seconds ?? args.tv_sec ?? 0)
|
||
const nsec = Number(args.nanoseconds ?? args.tv_nsec ?? args.ns ?? 0)
|
||
let ms =
|
||
(Number.isFinite(sec) ? sec : 0) * 1000 +
|
||
(Number.isFinite(nsec) ? nsec : 0) / 1e6
|
||
if (!Number.isFinite(ms) || ms < 0) ms = 0
|
||
const cap = 3_600_000
|
||
if (ms > cap) ms = cap
|
||
await new Promise((r) => setTimeout(r, ms))
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
sleptMs: Math.round(ms),
|
||
posixAlignment: 'partial',
|
||
note: 'POSIX-like delay via host setTimeout; capped at 3600s; not clock_nanosleep(2).'
|
||
}
|
||
}
|
||
if (name === 'clock_gettime') {
|
||
const raw = String(
|
||
args.clockId ?? args.clock_id ?? args.id ?? 'CLOCK_MONOTONIC'
|
||
).toUpperCase()
|
||
const isReal =
|
||
raw === 'REALTIME' ||
|
||
raw === 'CLOCK_REALTIME' ||
|
||
raw === '0' ||
|
||
raw === 'REAL_TIME'
|
||
let tvSec = 0
|
||
let tvNsec = 0
|
||
if (isReal) {
|
||
const ms = Date.now()
|
||
tvSec = Math.floor(ms / 1000)
|
||
tvNsec = (ms % 1000) * 1e6
|
||
} else if (bootHrtimeNowNs) {
|
||
const ns = bootHrtimeNowNs()
|
||
const bn = BigInt.asUintN(64, ns)
|
||
tvSec = Number(bn / 1000000000n)
|
||
tvNsec = Number(bn % 1000000000n)
|
||
} else {
|
||
try {
|
||
const perf = globalThis.performance
|
||
if (perf && typeof perf.now === 'function') {
|
||
const ms = perf.now()
|
||
tvSec = Math.floor(ms / 1000)
|
||
tvNsec = Math.round((ms % 1000) * 1e6)
|
||
} else {
|
||
const ms = Date.now()
|
||
tvSec = Math.floor(ms / 1000)
|
||
tvNsec = (ms % 1000) * 1e6
|
||
}
|
||
} catch {
|
||
const ms = Date.now()
|
||
tvSec = Math.floor(ms / 1000)
|
||
tvNsec = (ms % 1000) * 1e6
|
||
}
|
||
}
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
clockId: raw,
|
||
tv_sec: tvSec,
|
||
tv_nsec: tvNsec,
|
||
posixAlignment: 'partial',
|
||
monotonicSource: bootHrtimeNowNs
|
||
? 'bare-hrtime.bigint'
|
||
: globalThis.performance?.now
|
||
? 'performance.now'
|
||
: 'Date.now',
|
||
note: 'Partial clock_gettime: REALTIME wall; MONOTONIC prefers bare-hrtime else performance.now sketch; not CLOCK_TAI.'
|
||
}
|
||
}
|
||
if (name === 'mq_open') {
|
||
const ipc = bareOsIpc
|
||
if (!ipc || typeof ipc.mqOpen !== 'function') {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'ENOSYS',
|
||
errnoHint: 'ENOSYS',
|
||
note: 'IPC message queues unavailable'
|
||
}
|
||
}
|
||
const qname = String(args.name || args.pathname || '').trim()
|
||
if (!qname) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'mq_open: name required'
|
||
}
|
||
}
|
||
const r = ipc.mqOpen(qname, {
|
||
maxmsg: args.maxmsg,
|
||
maxBytes: args.maxBytes
|
||
})
|
||
return { ok: true, op: name, ...r }
|
||
}
|
||
if (name === 'mq_send') {
|
||
const ipc = bareOsIpc
|
||
if (!ipc || typeof ipc.mqSend !== 'function') {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'ENOSYS',
|
||
errnoHint: 'ENOSYS'
|
||
}
|
||
}
|
||
const qname = String(args.name || '').trim()
|
||
if (!qname) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL'
|
||
}
|
||
}
|
||
if (!ipc.mqAttrs(qname)) {
|
||
ipc.mqOpen(qname, {
|
||
maxmsg: args.maxmsg,
|
||
maxBytes: args.maxBytes
|
||
})
|
||
}
|
||
let body = args.data ?? args.buf ?? args.buffer
|
||
if (body == null) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'mq_send: data required'
|
||
}
|
||
}
|
||
if (typeof body === 'string') body = b4a.from(body, 'utf8')
|
||
if (!(body instanceof Uint8Array)) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL'
|
||
}
|
||
}
|
||
const prio = Number(args.prio) || 0
|
||
try {
|
||
ipc.mqSend(qname, prio, body)
|
||
return { ok: true, op: name, bytes: body.byteLength }
|
||
} catch (e) {
|
||
const em = e && /** @type {Error} */ (e).message
|
||
if (em && (em.includes('full') || em.includes('exceeds'))) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EAGAIN',
|
||
errnoHint: 'EAGAIN',
|
||
posixAlignment: 'partial',
|
||
note: String(em)
|
||
}
|
||
}
|
||
throw e
|
||
}
|
||
}
|
||
if (name === 'mq_receive') {
|
||
const ipc = bareOsIpc
|
||
if (!ipc || typeof ipc.mqReceive !== 'function') {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'ENOSYS',
|
||
errnoHint: 'ENOSYS'
|
||
}
|
||
}
|
||
const qname = String(args.name || '').trim()
|
||
if (!qname) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL'
|
||
}
|
||
}
|
||
const row = ipc.mqReceive(qname)
|
||
if (!row) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EAGAIN',
|
||
errnoHint: 'EAGAIN',
|
||
posixAlignment: 'partial',
|
||
note: 'mq_receive: queue empty or unknown (mq_open first)'
|
||
}
|
||
}
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
prio: row.prio,
|
||
data: row.data,
|
||
bytesReceived: row.data.byteLength
|
||
}
|
||
}
|
||
if (name === 'readFile') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
return vfs.readFile(p)
|
||
}
|
||
if (name === 'writeFile') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
const d = args.data
|
||
const buf =
|
||
typeof d === 'string'
|
||
? b4a.from(d, 'utf8')
|
||
: d instanceof Uint8Array
|
||
? d
|
||
: d instanceof ArrayBuffer
|
||
? new Uint8Array(d)
|
||
: null
|
||
if (!buf)
|
||
throw new Error(
|
||
'bareOsSyscall: writeFile data must be string or bytes'
|
||
)
|
||
await vfs.writeFile(p, buf)
|
||
return { ok: true, path: p, bytes: buf.byteLength }
|
||
}
|
||
if (name === 'readdir') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
return vfs.readdir(p)
|
||
}
|
||
if (name === 'mkdir') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
const recursive = !!args.recursive
|
||
const mode =
|
||
args.mode == null ? undefined : Number.parseInt(String(args.mode), 10)
|
||
await vfs.mkdir(
|
||
p,
|
||
Number.isFinite(mode)
|
||
? { recursive, mode: Math.max(0, Math.min(0o777, mode)) }
|
||
: { recursive }
|
||
)
|
||
return { ok: true, path: p }
|
||
}
|
||
if (name === 'stat') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
return vfs.stat(p)
|
||
}
|
||
if (name === 'unlink') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
await vfs.unlink(p)
|
||
return { ok: true, path: p }
|
||
}
|
||
if (name === 'chmod') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
const mode = Number.parseInt(String(args.mode || ''), 8)
|
||
if (!Number.isFinite(mode))
|
||
throw new Error('bareOsSyscall: chmod mode is required')
|
||
await vfs.chmod(p, mode)
|
||
return { ok: true, path: p, mode: mode & 0o777 }
|
||
}
|
||
if (name === 'chdir') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
await vfs.chdir(p)
|
||
return { ok: true, cwd: vfs.getcwd() }
|
||
}
|
||
if (name === 'getcwd') {
|
||
return { cwd: vfs.getcwd() }
|
||
}
|
||
if (name === 'readlink') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
const out = await vfs.readlink(p)
|
||
return { path: p, target: out }
|
||
}
|
||
if (name === 'symlink') {
|
||
const linkPath = String(args.path || '').trim()
|
||
const target = String(args.target || '').trim()
|
||
if (!linkPath.startsWith('/') || !target) {
|
||
throw new Error(
|
||
'bareOsSyscall: symlink requires absolute path and target'
|
||
)
|
||
}
|
||
await vfs.symlink(target, linkPath)
|
||
return { ok: true, path: linkPath, target }
|
||
}
|
||
if (name === 'exists') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
return { path: p, exists: await vfs.exists(p) }
|
||
}
|
||
if (name === 'lstat') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
return vfs.lstat(p)
|
||
}
|
||
if (name === 'rmdir') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
await vfs.rmdir(p)
|
||
return { ok: true, path: p }
|
||
}
|
||
if (name === 'rename') {
|
||
const from = String(args.from || args.path || '').trim()
|
||
const to = String(args.to || '').trim()
|
||
if (!from.startsWith('/') || !to.startsWith('/')) {
|
||
throw new Error(
|
||
'bareOsSyscall: rename requires absolute from and to paths'
|
||
)
|
||
}
|
||
if (!(await vfs.exists(from))) {
|
||
throw new Error('bareOsSyscall: rename: source not found')
|
||
}
|
||
const buf = await vfs.readFile(from)
|
||
const u8 =
|
||
buf instanceof Uint8Array
|
||
? buf
|
||
: buf != null
|
||
? b4a.from(buf)
|
||
: b4a.alloc(0)
|
||
await vfs.writeFile(to, u8)
|
||
await vfs.unlink(from)
|
||
return { ok: true, from, to }
|
||
}
|
||
if (name === 'link') {
|
||
const strict =
|
||
shellEnv.BARE_OS_VFS_STRICT_HARDLINK === '1' ||
|
||
shellEnv.BARE_OS_VFS_STRICT_HARDLINK === 'true'
|
||
if (strict) {
|
||
const err = /** @type {Error & { code?: string }} */ (
|
||
new Error(
|
||
'EOPNOTSUPP: hard links are not supported on Hyperdrive (unset BARE_OS_VFS_STRICT_HARDLINK for legacy copy semantics)'
|
||
)
|
||
)
|
||
err.code = 'EOPNOTSUPP'
|
||
throw err
|
||
}
|
||
const existing = String(args.existing || args.path || '').trim()
|
||
const newPath = String(args.newPath || args.to || '').trim()
|
||
if (!existing.startsWith('/') || !newPath.startsWith('/')) {
|
||
throw new Error('bareOsSyscall: link requires absolute paths')
|
||
}
|
||
if (!(await vfs.exists(existing))) {
|
||
throw new Error('bareOsSyscall: link: source not found')
|
||
}
|
||
const buf = await vfs.readFile(existing)
|
||
const u8 =
|
||
buf instanceof Uint8Array
|
||
? buf
|
||
: buf != null
|
||
? b4a.from(buf)
|
||
: b4a.alloc(0)
|
||
await vfs.writeFile(newPath, u8)
|
||
return {
|
||
ok: true,
|
||
existing,
|
||
newPath,
|
||
note: 'Data copied; Hyperdrive has no shared inode hard links.'
|
||
}
|
||
}
|
||
if (name === 'access') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
const ok = await vfs.exists(p)
|
||
if (!ok) throw new Error('ENOENT: access ' + p)
|
||
return { ok: true, path: p }
|
||
}
|
||
if (name === 'utimes') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
const mtimeMs = Number(args.mtimeMs ?? args.mtime)
|
||
if (!Number.isFinite(mtimeMs)) {
|
||
throw new Error('bareOsSyscall: utimes requires numeric mtimeMs')
|
||
}
|
||
let cur = await vfs.readFile(p)
|
||
const u8 =
|
||
cur instanceof Uint8Array
|
||
? cur
|
||
: cur != null
|
||
? b4a.from(cur)
|
||
: b4a.alloc(0)
|
||
await vfs.writeFile(p, u8, { mtimeMs, bumpMtime: true })
|
||
return { ok: true, path: p, mtimeMs }
|
||
}
|
||
if (name === 'truncate' || name === 'ftruncate') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/'))
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
const len = Number(args.length ?? args.len ?? 0)
|
||
if (!Number.isFinite(len) || len < 0) {
|
||
throw new Error(
|
||
'bareOsSyscall: truncate length must be a non-negative number'
|
||
)
|
||
}
|
||
let cur = await vfs.readFile(p)
|
||
let u8 =
|
||
cur instanceof Uint8Array
|
||
? cur
|
||
: cur != null
|
||
? b4a.from(cur)
|
||
: b4a.alloc(0)
|
||
if (u8.byteLength > len) {
|
||
u8 = u8.subarray(0, len)
|
||
} else if (u8.byteLength < len) {
|
||
u8 = b4a.concat([u8, b4a.alloc(len - u8.byteLength)])
|
||
}
|
||
await vfs.writeFile(p, u8)
|
||
return { ok: true, path: p, length: len }
|
||
}
|
||
if (name === 'mount') {
|
||
const label = String(args.label || '').trim()
|
||
const source = String(args.source || '').trim()
|
||
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$/.test(label)) {
|
||
throw new Error('bareOsSyscall: mount label is invalid')
|
||
}
|
||
if (hdmsController.byLabel.has(label)) {
|
||
throw new Error('bareOsSyscall: mount label already mounted')
|
||
}
|
||
if (source === 'local') {
|
||
await hdmsController.create(this, label)
|
||
} else if (source) {
|
||
await hdmsController.addReadonly(this, label, source)
|
||
} else {
|
||
throw new Error('bareOsSyscall: mount source is required')
|
||
}
|
||
await persistBareOsMountsSnapshot(vfs, hdmsController, {
|
||
console: this.console
|
||
})
|
||
return { ok: true, label, source }
|
||
}
|
||
if (name === 'umount') {
|
||
const label = String(args.label || '').trim()
|
||
const force = !!args.force
|
||
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$/.test(label)) {
|
||
throw new Error('bareOsSyscall: umount label is invalid')
|
||
}
|
||
if (!force && typeof vfs.readdir === 'function') {
|
||
try {
|
||
const mntPath = `/mnt/${label}`
|
||
const list = await vfs.readdir(mntPath)
|
||
if (
|
||
Array.isArray(list) &&
|
||
list.some((n) => n && n !== '.' && n !== '..')
|
||
) {
|
||
throw new Error(
|
||
'umount: target is busy (use umount -f or remove files under ' +
|
||
mntPath +
|
||
')'
|
||
)
|
||
}
|
||
} catch (e) {
|
||
const msg = e?.message || String(e)
|
||
if (msg.includes('busy')) throw e
|
||
/* ENOENT → not mounted at VFS layer; still try HDMS remove */
|
||
}
|
||
}
|
||
await hdmsController.remove(this, label)
|
||
await persistBareOsMountsSnapshot(vfs, hdmsController, {
|
||
console: this.console
|
||
})
|
||
return { ok: true, label }
|
||
}
|
||
if (name === 'fsync' || name === 'fdatasync') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/')) {
|
||
throw new Error('bareOsSyscall: absolute path required')
|
||
}
|
||
if (typeof vfs.fsync === 'function') await vfs.fsync(p)
|
||
return {
|
||
ok: true,
|
||
path: p,
|
||
op: name,
|
||
note: 'Best-effort no-op unless the VFS exposes a flush primitive.'
|
||
}
|
||
}
|
||
if (name === 'posix_fadvise') {
|
||
const p = String(args.path || '').trim()
|
||
if (!p.startsWith('/')) {
|
||
throw new Error(
|
||
'bareOsSyscall: posix_fadvise: absolute path required'
|
||
)
|
||
}
|
||
return {
|
||
ok: true,
|
||
path: p,
|
||
op: name,
|
||
offset: Number(args.offset ?? 0) || 0,
|
||
len: Number(args.len ?? args.length ?? 0) || 0,
|
||
advice: Number(args.advice ?? args.adv ?? 0) || 0,
|
||
posixAlignment: 'simulated',
|
||
note: 'posix_fadvise is a logical no-op; documents Issue 7-style advice without host page-cache control.'
|
||
}
|
||
}
|
||
if (name === 'fcntl') {
|
||
const O_ACCMODE = 3
|
||
const O_APPEND = 0x400
|
||
const O_NONBLOCK = 0x800
|
||
const SETFL_MASK = O_APPEND | O_NONBLOCK
|
||
const fd = Number(args.fd)
|
||
if (!Number.isFinite(fd) || fd < 0 || fd > 65535) {
|
||
throw new Error('bareOsSyscall: fcntl: invalid fd')
|
||
}
|
||
const k = String(fd >>> 0)
|
||
const target = this.bareOsLogicalFds[k]
|
||
const isStd = k === '0' || k === '1' || k === '2'
|
||
if (!isStd && (target == null || target === '')) {
|
||
const err = /** @type {Error & { code?: string }} */ (
|
||
new Error('EBADF: fcntl')
|
||
)
|
||
err.code = 'EBADF'
|
||
throw err
|
||
}
|
||
const baseAcc = () => {
|
||
if (k === '0') return 0
|
||
if (k === '1' || k === '2') return 2
|
||
const s = String(target || '')
|
||
if (/posix-pipe:\d+:r$/.test(s)) return 0
|
||
if (/posix-pipe:\d+:w$/.test(s)) return 1
|
||
return 2
|
||
}
|
||
let cmd = args.cmd
|
||
if (typeof cmd === 'string') {
|
||
const u = String(cmd).toUpperCase()
|
||
if (u === 'F_GETLK' || u === 'F_SETLK' || u === 'F_SETLKW') {
|
||
const lockPath = bareOsResolveCooperativeLockPath(args, this)
|
||
if (!lockPath) {
|
||
return {
|
||
ok: false,
|
||
op: 'fcntl',
|
||
cmd: u,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
cooperative: true,
|
||
note: 'Cooperative advisory locks require args.path (absolute) or args.fd whose logical target is an absolute path.'
|
||
}
|
||
}
|
||
if (
|
||
!this.bareOsCooperativeLocks ||
|
||
typeof this.bareOsCooperativeLocks !== 'object'
|
||
) {
|
||
this.bareOsCooperativeLocks = Object.create(null)
|
||
}
|
||
const locks =
|
||
/** @type {Record<string, { readers: Set<string>; writer: string | null }>} */ (
|
||
this.bareOsCooperativeLocks
|
||
)
|
||
const owner = String(
|
||
(this.env && this.env.BARE_OS_SESSION_ID) || 'session'
|
||
)
|
||
const flock =
|
||
args.flock && typeof args.flock === 'object'
|
||
? /** @type {Record<string, unknown>} */ (args.flock)
|
||
: {}
|
||
let lType = flock.l_type ?? flock.lType ?? args.l_type ?? args.lType
|
||
if (typeof lType === 'string') {
|
||
const up = String(lType).toUpperCase()
|
||
if (up === 'F_RDLCK' || up === 'RDLCK') lType = 0
|
||
else if (up === 'F_WRLCK' || up === 'WRLCK') lType = 1
|
||
else if (up === 'F_UNLCK' || up === 'UNLCK') lType = 2
|
||
}
|
||
lType = Number(lType)
|
||
if (!Number.isFinite(lType)) lType = 0
|
||
|
||
function ensureSlot(path) {
|
||
let s = locks[path]
|
||
if (!s) {
|
||
s = { readers: new Set(), writer: null }
|
||
locks[path] = s
|
||
}
|
||
return s
|
||
}
|
||
|
||
if (u === 'F_GETLK') {
|
||
const slot = locks[lockPath]
|
||
if (!slot || (!slot.writer && slot.readers.size === 0)) {
|
||
return {
|
||
ok: true,
|
||
op: 'fcntl',
|
||
cmd: 'F_GETLK',
|
||
path: lockPath,
|
||
cooperative: true,
|
||
flock: { l_type: 'F_UNLCK' }
|
||
}
|
||
}
|
||
if (slot.writer && slot.writer !== owner) {
|
||
return {
|
||
ok: true,
|
||
op: 'fcntl',
|
||
cmd: 'F_GETLK',
|
||
path: lockPath,
|
||
cooperative: true,
|
||
flock: { l_type: 'F_WRLCK', l_pid: 1 }
|
||
}
|
||
}
|
||
for (const sid of slot.readers) {
|
||
if (sid !== owner && lType === 1) {
|
||
return {
|
||
ok: true,
|
||
op: 'fcntl',
|
||
cmd: 'F_GETLK',
|
||
path: lockPath,
|
||
cooperative: true,
|
||
flock: { l_type: 'F_RDLCK', l_pid: 1 }
|
||
}
|
||
}
|
||
}
|
||
return {
|
||
ok: true,
|
||
op: 'fcntl',
|
||
cmd: 'F_GETLK',
|
||
path: lockPath,
|
||
cooperative: true,
|
||
flock: { l_type: 'F_UNLCK' }
|
||
}
|
||
}
|
||
|
||
const slot = ensureSlot(lockPath)
|
||
if (lType === 2) {
|
||
if (slot.writer === owner) slot.writer = null
|
||
slot.readers.delete(owner)
|
||
bareOsDrainFcntlLockWaiters(this, lockPath)
|
||
return {
|
||
ok: true,
|
||
op: 'fcntl',
|
||
cmd: u,
|
||
path: lockPath,
|
||
cooperative: true,
|
||
unlocked: true
|
||
}
|
||
}
|
||
const wantWrite = lType === 1
|
||
if (bareOsTryAcquireCooperativeLock(slot, owner, wantWrite)) {
|
||
return {
|
||
ok: true,
|
||
op: 'fcntl',
|
||
cmd: u,
|
||
path: lockPath,
|
||
cooperative: true,
|
||
locked: wantWrite ? 'write' : 'read'
|
||
}
|
||
}
|
||
const blocking =
|
||
u === 'F_SETLKW' && wantPosixFcntlBlockingWait(this.env)
|
||
if (!blocking) {
|
||
return {
|
||
ok: false,
|
||
op: 'fcntl',
|
||
cmd: u,
|
||
code: 'EAGAIN',
|
||
errnoHint: 'EAGAIN',
|
||
cooperative: true,
|
||
note: wantWrite
|
||
? 'Write lock held by another session.'
|
||
: 'Read or write lock held by another session.'
|
||
}
|
||
}
|
||
const maxMs = bareOsFcntlLockWaitMsMax(this.env)
|
||
return await new Promise((resolve) => {
|
||
const entry = {
|
||
resolve,
|
||
timer: /** @type {ReturnType<typeof setTimeout> | null} */ (
|
||
null
|
||
),
|
||
owner,
|
||
wantWrite
|
||
}
|
||
entry.timer = setTimeout(() => {
|
||
bareOsRemoveFcntlWaiter(this, lockPath, entry)
|
||
resolve({
|
||
ok: false,
|
||
op: 'fcntl',
|
||
cmd: 'F_SETLKW',
|
||
code: 'ETIMEDOUT',
|
||
errnoHint: 'ETIMEDOUT',
|
||
cooperative: true,
|
||
note: 'F_SETLKW wait exceeded BARE_OS_FCNTL_LOCK_WAIT_MS_MAX (or default 30000).'
|
||
})
|
||
}, maxMs)
|
||
if (
|
||
!this.bareOsFcntlLockWaitQueues ||
|
||
typeof this.bareOsFcntlLockWaitQueues !== 'object'
|
||
) {
|
||
this.bareOsFcntlLockWaitQueues = Object.create(null)
|
||
}
|
||
const wq = this.bareOsFcntlLockWaitQueues
|
||
if (!Array.isArray(wq[lockPath])) wq[lockPath] = []
|
||
wq[lockPath].push(entry)
|
||
})
|
||
}
|
||
if (u === 'F_GETFL' || u === 'GETFL') cmd = 3
|
||
else if (u === 'F_SETFL' || u === 'SETFL') cmd = 4
|
||
else {
|
||
const err = /** @type {Error & { code?: string }} */ (
|
||
new Error('ENOTSUP: fcntl cmd')
|
||
)
|
||
err.code = 'ENOTSUP'
|
||
throw err
|
||
}
|
||
}
|
||
cmd = Number(cmd)
|
||
if (cmd === 3) {
|
||
const acc = baseAcc() & O_ACCMODE
|
||
const extra = (this.bareOsLogicalFdFlags[k] || 0) & SETFL_MASK
|
||
return {
|
||
ok: true,
|
||
op: 'fcntl',
|
||
cmd: 'F_GETFL',
|
||
fd,
|
||
flags: acc | extra
|
||
}
|
||
}
|
||
if (cmd === 4) {
|
||
if (
|
||
!this.bareOsLogicalFdFlags ||
|
||
typeof this.bareOsLogicalFdFlags !== 'object'
|
||
) {
|
||
this.bareOsLogicalFdFlags = {}
|
||
}
|
||
const arg = Number(args.arg ?? args.value ?? 0) >>> 0
|
||
this.bareOsLogicalFdFlags[k] = arg & SETFL_MASK
|
||
const acc = baseAcc() & O_ACCMODE
|
||
const extra = this.bareOsLogicalFdFlags[k] & SETFL_MASK
|
||
return {
|
||
ok: true,
|
||
op: 'fcntl',
|
||
cmd: 'F_SETFL',
|
||
fd,
|
||
flags: acc | extra
|
||
}
|
||
}
|
||
const err2 = /** @type {Error & { code?: string }} */ (
|
||
new Error('ENOTSUP: fcntl cmd')
|
||
)
|
||
err2.code = 'ENOTSUP'
|
||
throw err2
|
||
}
|
||
if (name === 'select') {
|
||
const readfds = Array.isArray(args.readfds) ? args.readfds : []
|
||
const writefds = Array.isArray(args.writefds) ? args.writefds : []
|
||
/** @type {Map<number, string>} */
|
||
const ev = new Map()
|
||
for (const x of readfds) {
|
||
const fd = Number(x)
|
||
if (Number.isFinite(fd)) ev.set(fd, (ev.get(fd) || '') + 'r')
|
||
}
|
||
for (const x of writefds) {
|
||
const fd = Number(x)
|
||
if (Number.isFinite(fd)) ev.set(fd, (ev.get(fd) || '') + 'w')
|
||
}
|
||
const fds = [...ev.entries()].map(([fd, events]) => ({ fd, events }))
|
||
const timeoutMs = Math.max(
|
||
0,
|
||
Math.min(60000, Math.floor(Number(args.timeoutMs) || 0))
|
||
)
|
||
const pr =
|
||
timeoutMs > 0
|
||
? await this.bareOsPosixPoll({ fds, timeoutMs })
|
||
: this.bareOsPosixPollProbe({ fds })
|
||
const readSet = new Set(
|
||
readfds.map((x) => Number(x)).filter(Number.isFinite)
|
||
)
|
||
const writeSet = new Set(
|
||
writefds.map((x) => Number(x)).filter(Number.isFinite)
|
||
)
|
||
const readReady = []
|
||
const writeReady = []
|
||
for (const row of pr.ready || []) {
|
||
const fd = row.fd
|
||
const rv = String(row.revents || '')
|
||
if (readSet.has(fd) && rv.includes('r')) readReady.push(fd)
|
||
if (writeSet.has(fd) && rv.includes('w')) writeReady.push(fd)
|
||
}
|
||
return {
|
||
ok: true,
|
||
op: 'select',
|
||
readReady,
|
||
writeReady,
|
||
timedOut: pr.timedOut === true,
|
||
waitedMs: pr.waitedMs ?? 0,
|
||
pollClock: pr.pollClock
|
||
}
|
||
}
|
||
if (name === 'umask') {
|
||
const env = this.vfs?.env
|
||
if (!env || typeof env !== 'object') {
|
||
throw new Error('bareOsSyscall: umask: vfs.env unavailable')
|
||
}
|
||
const curRaw = String(env.UMASK || '022').trim()
|
||
const curMask = Number.parseInt(curRaw, 8)
|
||
const validCur =
|
||
Number.isFinite(curMask) && curMask >= 0 && curMask <= 0o777
|
||
if (
|
||
args.set !== undefined &&
|
||
args.set !== null &&
|
||
String(args.set).trim() !== ''
|
||
) {
|
||
const next = Number.parseInt(String(args.set), 8)
|
||
if (!Number.isFinite(next) || next < 0 || next > 0o777) {
|
||
throw new Error('bareOsSyscall: umask: invalid mask')
|
||
}
|
||
env.UMASK = next.toString(8)
|
||
}
|
||
const nowMask = Number.parseInt(String(env.UMASK || '022'), 8)
|
||
return {
|
||
ok: true,
|
||
op: 'umask',
|
||
previous: validCur ? curMask : 0o022,
|
||
current: Number.isFinite(nowMask) ? nowMask : 0o022
|
||
}
|
||
}
|
||
if (name === 'readv') {
|
||
const fd = Number(args.fd)
|
||
if (!Number.isFinite(fd)) {
|
||
throw new Error('bareOsSyscall: readv: fd is required')
|
||
}
|
||
const iovs = Array.isArray(args.iovs) ? args.iovs : []
|
||
const maxIovs = 16
|
||
const maxPer = 65536
|
||
const maxTotal = 1024 * 1024
|
||
let totalBytes = 0
|
||
/** @type {string[]} */
|
||
const buffers = []
|
||
for (const iov of iovs.slice(0, maxIovs)) {
|
||
const want = Math.min(
|
||
maxPer,
|
||
Math.max(
|
||
0,
|
||
Math.floor(
|
||
Number(
|
||
iov && typeof iov === 'object' && 'length' in iov
|
||
? iov.length
|
||
: iov && typeof iov === 'object' && 'maxBytes' in iov
|
||
? iov.maxBytes
|
||
: 4096
|
||
)
|
||
)
|
||
)
|
||
)
|
||
if (want <= 0) {
|
||
buffers.push('')
|
||
continue
|
||
}
|
||
if (totalBytes + want > maxTotal) {
|
||
throw new Error('bareOsSyscall: readv: total iov budget exceeded')
|
||
}
|
||
const r = this.bareOsPosixFdSimRead(fd, want)
|
||
if (!r || !r.ok) {
|
||
if (r && r.reason === 'EAGAIN') {
|
||
const err = /** @type {Error & { code?: string }} */ (
|
||
new Error('EAGAIN: readv')
|
||
)
|
||
err.code = 'EAGAIN'
|
||
throw err
|
||
}
|
||
throw new Error(
|
||
'bareOsSyscall: readv: ' + (r && r.reason ? r.reason : 'failed')
|
||
)
|
||
}
|
||
const chunk = String(r.data || '')
|
||
buffers.push(chunk)
|
||
totalBytes += b4a.from(chunk, 'utf8').byteLength
|
||
if (chunk.length < want) break
|
||
}
|
||
return { ok: true, op: 'readv', fd, buffers, bytesRead: totalBytes }
|
||
}
|
||
if (name === 'writev') {
|
||
const fd = Number(args.fd)
|
||
if (!Number.isFinite(fd)) {
|
||
throw new Error('bareOsSyscall: writev: fd is required')
|
||
}
|
||
const bufs = Array.isArray(args.buffers) ? args.buffers : []
|
||
const maxChunks = 64
|
||
const maxTotal = 1024 * 1024
|
||
const pieces = []
|
||
let n = 0
|
||
for (const b of bufs.slice(0, maxChunks)) {
|
||
const s =
|
||
typeof b === 'string'
|
||
? b
|
||
: b instanceof Uint8Array
|
||
? b4a.toString(b, 'utf8')
|
||
: String(b)
|
||
const add = b4a.from(s, 'utf8').byteLength
|
||
n += add
|
||
if (n > maxTotal) {
|
||
throw new Error('bareOsSyscall: writev: total length exceeded')
|
||
}
|
||
pieces.push(s)
|
||
}
|
||
const payload = pieces.join('')
|
||
const w = this.bareOsPosixFdSimWrite(fd, payload)
|
||
if (!w || !w.ok) {
|
||
if (w && w.reason === 'EAGAIN') {
|
||
const err = /** @type {Error & { code?: string }} */ (
|
||
new Error('EAGAIN: writev')
|
||
)
|
||
err.code = 'EAGAIN'
|
||
throw err
|
||
}
|
||
throw new Error(
|
||
'bareOsSyscall: writev: ' + (w && w.reason ? w.reason : 'failed')
|
||
)
|
||
}
|
||
return {
|
||
ok: true,
|
||
op: 'writev',
|
||
fd,
|
||
bytesWritten: w.bytes ?? b4a.from(payload, 'utf8').byteLength
|
||
}
|
||
}
|
||
if (name === 'getsockopt' || name === 'setsockopt') {
|
||
const SOL_SOCKET = 1
|
||
const SO_KEEPALIVE = 9
|
||
const SO_SNDBUF = 7
|
||
const SO_RCVBUF = 8
|
||
const IPPROTO_TCP = 6
|
||
const TCP_NODELAY = 1
|
||
const DEF_SNDBUF = 262144
|
||
const DEF_RCVBUF = 262144
|
||
const fd = Number(args.fd)
|
||
if (
|
||
wantPosixSocketFdBridge(shellEnv) &&
|
||
Number.isFinite(fd) &&
|
||
this.bareOsSocketBridgeByFd &&
|
||
typeof this.bareOsSocketBridgeByFd === 'object'
|
||
) {
|
||
const k = String(fd >>> 0)
|
||
const slot = this.bareOsSocketBridgeByFd[k]
|
||
if (slot && typeof slot === 'object') {
|
||
if (!slot.bridgeSockOpts) {
|
||
slot.bridgeSockOpts = {
|
||
soKeepalive: 1,
|
||
tcpNodelay: 0,
|
||
sndbuf: DEF_SNDBUF,
|
||
rcvbuf: DEF_RCVBUF
|
||
}
|
||
} else {
|
||
if (slot.bridgeSockOpts.sndbuf == null)
|
||
slot.bridgeSockOpts.sndbuf = DEF_SNDBUF
|
||
if (slot.bridgeSockOpts.rcvbuf == null)
|
||
slot.bridgeSockOpts.rcvbuf = DEF_RCVBUF
|
||
}
|
||
const level = Number(args.level) >>> 0
|
||
const optname = Number(args.optname) >>> 0
|
||
if (name === 'getsockopt') {
|
||
let intVal = 0
|
||
if (level === SOL_SOCKET && optname === SO_KEEPALIVE) {
|
||
intVal = slot.bridgeSockOpts.soKeepalive ? 1 : 0
|
||
} else if (level === SOL_SOCKET && optname === SO_SNDBUF) {
|
||
intVal = Math.max(
|
||
4096,
|
||
Number(slot.bridgeSockOpts.sndbuf) || DEF_SNDBUF
|
||
)
|
||
} else if (level === SOL_SOCKET && optname === SO_RCVBUF) {
|
||
intVal = Math.max(
|
||
4096,
|
||
Number(slot.bridgeSockOpts.rcvbuf) || DEF_RCVBUF
|
||
)
|
||
} else if (level === IPPROTO_TCP && optname === TCP_NODELAY) {
|
||
intVal = slot.bridgeSockOpts.tcpNodelay ? 1 : 0
|
||
} else {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
fd,
|
||
code: 'ENOPROTOOPT',
|
||
errnoHint: 'ENOPROTOOPT',
|
||
posixAlignment: 'partial',
|
||
note: 'Bridge supports SOL_SOCKET/SO_KEEPALIVE, SO_SNDBUF, SO_RCVBUF, and IPPROTO_TCP/TCP_NODELAY.'
|
||
}
|
||
}
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
fd,
|
||
level,
|
||
optname,
|
||
intValue: intVal,
|
||
posixAlignment: 'partial',
|
||
note: 'Logical socket-bridge option read; does not change host UDX/TCP stack.'
|
||
}
|
||
}
|
||
const v = Number(args.value ?? args.optval ?? args.intValue ?? 0)
|
||
const on = Number.isFinite(v) && v !== 0 ? 1 : 0
|
||
if (level === SOL_SOCKET && optname === SO_KEEPALIVE) {
|
||
slot.bridgeSockOpts.soKeepalive = on
|
||
} else if (level === IPPROTO_TCP && optname === TCP_NODELAY) {
|
||
slot.bridgeSockOpts.tcpNodelay = on
|
||
} else if (level === SOL_SOCKET && optname === SO_SNDBUF) {
|
||
const n = Math.floor(Number(v))
|
||
if (!Number.isFinite(n) || n < 4096) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
fd,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
posixAlignment: 'partial',
|
||
note: 'SO_SNDBUF must be >= 4096'
|
||
}
|
||
}
|
||
slot.bridgeSockOpts.sndbuf = Math.min(n, 4 * 1024 * 1024)
|
||
} else if (level === SOL_SOCKET && optname === SO_RCVBUF) {
|
||
const n = Math.floor(Number(v))
|
||
if (!Number.isFinite(n) || n < 4096) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
fd,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
posixAlignment: 'partial',
|
||
note: 'SO_RCVBUF must be >= 4096'
|
||
}
|
||
}
|
||
slot.bridgeSockOpts.rcvbuf = Math.min(n, 4 * 1024 * 1024)
|
||
} else {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
fd,
|
||
code: 'ENOPROTOOPT',
|
||
errnoHint: 'ENOPROTOOPT',
|
||
posixAlignment: 'partial',
|
||
note: 'Bridge setsockopt: SOL_SOCKET/SO_KEEPALIVE, SO_SNDBUF, SO_RCVBUF, and IPPROTO_TCP/TCP_NODELAY.'
|
||
}
|
||
}
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
fd,
|
||
level,
|
||
optname,
|
||
posixAlignment: 'partial'
|
||
}
|
||
}
|
||
}
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'ENOSYS',
|
||
errnoHint: 'ENOSYS',
|
||
posixAlignment: 'ENOTSUP',
|
||
note: 'getsockopt/setsockopt need BARE_OS_POSIX_SOCKET_FD_BRIDGE and a bridge socket fd; else ENOSYS.'
|
||
}
|
||
}
|
||
if (
|
||
name === 'socket' ||
|
||
name === 'bind' ||
|
||
name === 'listen' ||
|
||
name === 'accept' ||
|
||
name === 'connect' ||
|
||
name === 'send' ||
|
||
name === 'sendmsg' ||
|
||
name === 'recv' ||
|
||
name === 'recvfrom' ||
|
||
name === 'recvmsg' ||
|
||
name === 'shutdown'
|
||
) {
|
||
if (wantPosixSocketFdBridge(shellEnv)) {
|
||
const st = this.bareOsPosixFdSimState
|
||
const bridgeNotify = (slot, kind) => {
|
||
const key = kind === 'accept' ? 'acceptWaiters' : 'recvWaiters'
|
||
const waiters = slot && Array.isArray(slot[key]) ? slot[key] : null
|
||
if (!waiters || waiters.length === 0) return
|
||
const pending = waiters.splice(0, waiters.length)
|
||
for (const resolve of pending) {
|
||
try {
|
||
resolve()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
const bridgeWait = (slot, timeoutMs, kind) =>
|
||
new Promise((resolve) => {
|
||
const key = kind === 'accept' ? 'acceptWaiters' : 'recvWaiters'
|
||
if (!slot[key] || !Array.isArray(slot[key])) slot[key] = []
|
||
let settled = false
|
||
const done = () => {
|
||
if (settled) return
|
||
settled = true
|
||
const waiters = slot[key]
|
||
if (Array.isArray(waiters)) {
|
||
const idx = waiters.indexOf(done)
|
||
if (idx >= 0) waiters.splice(idx, 1)
|
||
}
|
||
resolve()
|
||
}
|
||
slot[key].push(done)
|
||
const ms = Math.max(1, Math.min(100, Number(timeoutMs) || 10))
|
||
setTimeout(done, ms)
|
||
})
|
||
if (name === 'socket') {
|
||
const fd = st.nextFd++
|
||
const k = String(fd)
|
||
if (
|
||
!this.bareOsSocketBridgeByFd ||
|
||
typeof this.bareOsSocketBridgeByFd !== 'object'
|
||
) {
|
||
this.bareOsSocketBridgeByFd = Object.create(null)
|
||
}
|
||
const sockType = Number(args.type ?? args.socktype ?? 1) >>> 0
|
||
this.bareOsSocketBridgeByFd[k] = {
|
||
domain: Number(args.domain ?? args.af ?? 2),
|
||
type: sockType,
|
||
sockType,
|
||
state: 'created',
|
||
recvWaiters: [],
|
||
acceptWaiters: [],
|
||
aliasKeys: new Set([k])
|
||
}
|
||
this.bareOsRegisterLogicalFd(fd, 'bare-os-socket-bridge:idle')
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
fd,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
note:
|
||
sockType === 2
|
||
? 'Optional POSIX socket FD bridge (SOCK_DGRAM): bind(port,host) for passive UDP, or connect(port,host) for connected mode, when ctx.bare exposes bareDgram.Socket (UDX).'
|
||
: 'Optional POSIX socket FD bridge (SOCK_STREAM): follow with connect(port, host) when ctx.bare exposes bareTcp.Socket.'
|
||
}
|
||
}
|
||
if (name === 'connect') {
|
||
const fd = Number(args.fd)
|
||
if (!Number.isFinite(fd)) {
|
||
throw new Error('bareOsSyscall: connect: fd required')
|
||
}
|
||
const k = String(fd >>> 0)
|
||
const slot =
|
||
this.bareOsSocketBridgeByFd &&
|
||
typeof this.bareOsSocketBridgeByFd === 'object'
|
||
? this.bareOsSocketBridgeByFd[k]
|
||
: null
|
||
const port = Number(args.port)
|
||
const host = String(args.host || args.address || '127.0.0.1').trim()
|
||
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL'
|
||
}
|
||
}
|
||
if (!slot) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'connect: unknown bridge fd.'
|
||
}
|
||
}
|
||
const isDgram = Number(slot.sockType ?? slot.type ?? 1) === 2
|
||
const dgramBound =
|
||
isDgram && slot.state === 'udp_bound' && slot.sock
|
||
if (!isDgram && slot.state !== 'created') {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'ENOTSUP',
|
||
errnoHint: 'ENOTSUP',
|
||
posixAlignment: 'ENOTSUP',
|
||
note: 'connect: requires prior bridge socket(2) fd in created state.'
|
||
}
|
||
}
|
||
if (isDgram && slot.state !== 'created' && !dgramBound) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
posixAlignment: 'ENOTSUP',
|
||
note: 'connect: SOCK_DGRAM bridge requires created socket or bind(2) before connect on the same fd.'
|
||
}
|
||
}
|
||
const Tcp =
|
||
bareLibrary.bareTcp &&
|
||
bareLibrary.bareTcp.Socket &&
|
||
typeof bareLibrary.bareTcp.Socket === 'function'
|
||
? bareLibrary.bareTcp.Socket
|
||
: null
|
||
const dg = bareLibrary.bareDgram
|
||
const UdpSocket =
|
||
dg &&
|
||
((typeof dg.Socket === 'function' && dg.Socket) ||
|
||
(dg.default &&
|
||
typeof dg.default.Socket === 'function' &&
|
||
dg.default.Socket))
|
||
? dg.Socket || dg.default.Socket
|
||
: null
|
||
if (isDgram) {
|
||
if (!UdpSocket) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'ENOTSUP',
|
||
errnoHint: 'ENOTSUP',
|
||
posixAlignment: 'ENOTSUP',
|
||
note: 'SOCK_DGRAM bridge requires bareDgram.Socket merged in ctx.bare (bare-dgram / UDX).'
|
||
}
|
||
}
|
||
if (dgramBound) {
|
||
const usock = /** @type {{ connect: Function }} */ (slot.sock)
|
||
await new Promise((resolve, reject) => {
|
||
try {
|
||
usock.connect(port, host || '127.0.0.1', (err) =>
|
||
err ? reject(err) : resolve(undefined)
|
||
)
|
||
} catch (e) {
|
||
reject(e)
|
||
}
|
||
})
|
||
slot.state = 'connected'
|
||
this.bareOsLogicalFds[k] =
|
||
'bare-os-socket-bridge:udp:' +
|
||
(host || '127.0.0.1') +
|
||
':' +
|
||
port
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
fd,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
transport: 'udp'
|
||
}
|
||
}
|
||
const usock = new UdpSocket({})
|
||
await new Promise((resolve, reject) => {
|
||
try {
|
||
usock.connect(port, host || '127.0.0.1', (err) =>
|
||
err ? reject(err) : resolve(undefined)
|
||
)
|
||
} catch (e) {
|
||
reject(e)
|
||
}
|
||
})
|
||
slot.state = 'connected'
|
||
slot.sock = usock
|
||
slot.transport = 'udp'
|
||
slot.dgramRecvQueue = []
|
||
slot.dgramRecvDropped = 0
|
||
const qmax = bareOsPosixDgramRecvQueueMax(shellEnv)
|
||
usock.on('message', (message, rinfo) => {
|
||
let u8 =
|
||
message instanceof Uint8Array ? message : b4a.from(message)
|
||
if (u8.byteLength > 1024 * 1024) u8 = u8.slice(0, 1024 * 1024)
|
||
if (slot.dgramRecvQueue.length >= qmax) {
|
||
slot.dgramRecvDropped = (slot.dgramRecvDropped || 0) + 1
|
||
return
|
||
}
|
||
slot.dgramRecvQueue.push({
|
||
buf: u8.slice(),
|
||
rinfo:
|
||
rinfo && typeof rinfo === 'object'
|
||
? {
|
||
address: String(rinfo.address || ''),
|
||
port: Number(rinfo.port) >>> 0
|
||
}
|
||
: null
|
||
})
|
||
bridgeNotify(slot, 'recv')
|
||
})
|
||
this.bareOsLogicalFds[k] =
|
||
'bare-os-socket-bridge:udp:' +
|
||
(host || '127.0.0.1') +
|
||
':' +
|
||
port
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
fd,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
transport: 'udp'
|
||
}
|
||
}
|
||
if (!Tcp) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'ENOTSUP',
|
||
errnoHint: 'ENOTSUP',
|
||
posixAlignment: 'ENOTSUP',
|
||
note: 'connect: SOCK_STREAM bridge requires bareTcp.Socket merged in ctx.bare (bare-tcp bundle).'
|
||
}
|
||
}
|
||
const connectHosts =
|
||
host === 'localhost'
|
||
? ['127.0.0.1', '::1']
|
||
: [host || '127.0.0.1']
|
||
/** @type {unknown[]} */
|
||
const connectErrors = []
|
||
/** @type {any} */
|
||
let sock = null
|
||
const connectTimeoutMs = Math.min(
|
||
300000,
|
||
Math.max(
|
||
0,
|
||
Number.parseInt(
|
||
String(
|
||
shellEnv.BARE_OS_POSIX_SOCKET_CONNECT_TIMEOUT_MS || '0'
|
||
).trim(),
|
||
10
|
||
) || 0
|
||
)
|
||
)
|
||
try {
|
||
for (const candHost of connectHosts) {
|
||
const candSock = new Tcp()
|
||
const connectPromise = new Promise((resolve, reject) => {
|
||
try {
|
||
candSock.connect(
|
||
port,
|
||
candHost || '127.0.0.1',
|
||
{},
|
||
(err) => (err ? reject(err) : resolve(undefined))
|
||
)
|
||
} catch (e) {
|
||
reject(e)
|
||
}
|
||
})
|
||
try {
|
||
if (connectTimeoutMs > 0) {
|
||
/** @type {ReturnType<typeof setTimeout> | null} */
|
||
let to = null
|
||
const timeoutP = new Promise((_, reject) => {
|
||
to = setTimeout(
|
||
() =>
|
||
reject(
|
||
Object.assign(new Error('ETIMEDOUT'), {
|
||
code: 'ETIMEDOUT'
|
||
})
|
||
),
|
||
connectTimeoutMs
|
||
)
|
||
})
|
||
await Promise.race([connectPromise, timeoutP]).finally(
|
||
() => {
|
||
if (to) clearTimeout(to)
|
||
}
|
||
)
|
||
} else {
|
||
await connectPromise
|
||
}
|
||
sock = candSock
|
||
break
|
||
} catch (e) {
|
||
connectErrors.push(e)
|
||
try {
|
||
candSock.destroy?.()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
try {
|
||
sock.destroy?.()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
if (
|
||
e &&
|
||
/** @type {{ code?: string }} */ (e).code === 'ETIMEDOUT'
|
||
) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'ETIMEDOUT',
|
||
errnoHint: 'ETIMEDOUT',
|
||
posixAlignment: 'partial',
|
||
note: `connect: timed out after ${connectTimeoutMs}ms (BARE_OS_POSIX_SOCKET_CONNECT_TIMEOUT_MS).`
|
||
}
|
||
}
|
||
throw e
|
||
}
|
||
if (!sock) {
|
||
const first = connectErrors[0]
|
||
if (
|
||
first &&
|
||
/** @type {{ code?: string }} */ (first).code === 'ETIMEDOUT'
|
||
) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'ETIMEDOUT',
|
||
errnoHint: 'ETIMEDOUT',
|
||
posixAlignment: 'partial',
|
||
note: `connect: timed out after ${connectTimeoutMs}ms (BARE_OS_POSIX_SOCKET_CONNECT_TIMEOUT_MS).`
|
||
}
|
||
}
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'ECONNREFUSED',
|
||
errnoHint: 'ECONNREFUSED',
|
||
posixAlignment: 'partial',
|
||
note: `connect: all address candidates failed for host=${host || '127.0.0.1'}`
|
||
}
|
||
}
|
||
slot.state = 'connected'
|
||
slot.sock = sock
|
||
slot.transport = 'tcp'
|
||
slot.tcpRecvQueue = []
|
||
slot.tcpRecvDropped = 0
|
||
const tcpQmax = bareOsPosixDgramRecvQueueMax(shellEnv)
|
||
sock.on('data', (message) => {
|
||
let u8 =
|
||
message instanceof Uint8Array ? message : b4a.from(message)
|
||
if (u8.byteLength > 1024 * 1024) u8 = u8.slice(0, 1024 * 1024)
|
||
if (!Array.isArray(slot.tcpRecvQueue)) slot.tcpRecvQueue = []
|
||
if (slot.tcpRecvQueue.length >= tcpQmax) {
|
||
slot.tcpRecvDropped = (slot.tcpRecvDropped || 0) + 1
|
||
return
|
||
}
|
||
slot.tcpRecvQueue.push({ buf: u8.slice() })
|
||
bridgeNotify(slot, 'recv')
|
||
})
|
||
sock.on('end', () => {
|
||
slot.tcpPeerEnded = true
|
||
bridgeNotify(slot, 'recv')
|
||
})
|
||
sock.on('error', () => {
|
||
slot.tcpSockError = true
|
||
bridgeNotify(slot, 'recv')
|
||
})
|
||
this.bareOsLogicalFds[k] =
|
||
'bare-os-socket-bridge:tcp:' + (host || '127.0.0.1') + ':' + port
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
fd,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
transport: 'tcp'
|
||
}
|
||
}
|
||
if (name === 'bind') {
|
||
const fd = Number(args.fd)
|
||
if (!Number.isFinite(fd)) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'bind: fd required'
|
||
}
|
||
}
|
||
const k = String(fd >>> 0)
|
||
const slot =
|
||
this.bareOsSocketBridgeByFd &&
|
||
typeof this.bareOsSocketBridgeByFd === 'object'
|
||
? this.bareOsSocketBridgeByFd[k]
|
||
: null
|
||
const isDgram = Number(slot?.sockType ?? slot?.type ?? 1) === 2
|
||
if (isDgram) {
|
||
if (!slot || slot.state !== 'created') {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'bind: requires SOCK_DGRAM bridge socket in created state.'
|
||
}
|
||
}
|
||
const dg = bareLibrary.bareDgram
|
||
const UdpSocket =
|
||
dg &&
|
||
((typeof dg.Socket === 'function' && dg.Socket) ||
|
||
(dg.default &&
|
||
typeof dg.default.Socket === 'function' &&
|
||
dg.default.Socket))
|
||
? dg.Socket || dg.default.Socket
|
||
: null
|
||
if (!UdpSocket) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'ENOTSUP',
|
||
errnoHint: 'ENOTSUP',
|
||
posixAlignment: 'ENOTSUP',
|
||
note: 'bind: SOCK_DGRAM bridge requires bareDgram.Socket merged in ctx.bare (bare-dgram / UDX).'
|
||
}
|
||
}
|
||
const port = Number(args.port)
|
||
const host =
|
||
String(args.host || args.address || '0.0.0.0').trim() ||
|
||
'0.0.0.0'
|
||
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'bind: port required (0–65535)'
|
||
}
|
||
}
|
||
const bindUdpOnce = (sock, bindPort) =>
|
||
new Promise((resolve, reject) => {
|
||
const tmo = setTimeout(() => {
|
||
sock.removeAllListeners('error')
|
||
reject(new Error('bind: timeout'))
|
||
}, 30000)
|
||
sock.once('error', (e) => {
|
||
clearTimeout(tmo)
|
||
sock.removeAllListeners('error')
|
||
reject(e)
|
||
})
|
||
try {
|
||
sock.bind(bindPort, host, () => {
|
||
clearTimeout(tmo)
|
||
sock.removeAllListeners('error')
|
||
resolve(undefined)
|
||
})
|
||
} catch (e) {
|
||
clearTimeout(tmo)
|
||
reject(e)
|
||
}
|
||
})
|
||
|
||
let usock = new UdpSocket({})
|
||
try {
|
||
await bindUdpOnce(usock, port)
|
||
} catch (e) {
|
||
try {
|
||
await usock.close?.()
|
||
} catch (_) {}
|
||
if (
|
||
!bindFallbackEnabled(shellEnv) ||
|
||
!isAddrInUse(e) ||
|
||
port === 0
|
||
) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EADDRINUSE',
|
||
errnoHint: 'EADDRINUSE',
|
||
posixAlignment: 'partial',
|
||
note: String(
|
||
e && /** @type {Error} */ (e).message ? e.message : e
|
||
)
|
||
}
|
||
}
|
||
usock = new UdpSocket({})
|
||
try {
|
||
await bindUdpOnce(usock, 0)
|
||
} catch (e2) {
|
||
try {
|
||
await usock.close?.()
|
||
} catch (_) {}
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EADDRINUSE',
|
||
errnoHint: 'EADDRINUSE',
|
||
posixAlignment: 'partial',
|
||
note: String(
|
||
e2 && /** @type {Error} */ (e2).message ? e2.message : e2
|
||
)
|
||
}
|
||
}
|
||
}
|
||
let boundUdpPort = port
|
||
try {
|
||
const a =
|
||
usock.address && typeof usock.address === 'function'
|
||
? usock.address()
|
||
: null
|
||
if (a && typeof a === 'object' && Number.isFinite(a.port))
|
||
boundUdpPort = Number(a.port)
|
||
} catch (_) {}
|
||
slot.state = 'udp_bound'
|
||
slot.sock = usock
|
||
slot.transport = 'udp'
|
||
slot.bindPort = boundUdpPort
|
||
slot.bindHost = host
|
||
slot.dgramRecvQueue = []
|
||
slot.dgramRecvDropped = 0
|
||
const qmax = bareOsPosixDgramRecvQueueMax(shellEnv)
|
||
usock.on('message', (message, rinfo) => {
|
||
let u8 =
|
||
message instanceof Uint8Array ? message : b4a.from(message)
|
||
if (u8.byteLength > 1024 * 1024) u8 = u8.slice(0, 1024 * 1024)
|
||
if (slot.dgramRecvQueue.length >= qmax) {
|
||
slot.dgramRecvDropped = (slot.dgramRecvDropped || 0) + 1
|
||
return
|
||
}
|
||
slot.dgramRecvQueue.push({
|
||
buf: u8.slice(),
|
||
rinfo:
|
||
rinfo && typeof rinfo === 'object'
|
||
? {
|
||
address: String(rinfo.address || ''),
|
||
port: Number(rinfo.port) >>> 0
|
||
}
|
||
: null
|
||
})
|
||
bridgeNotify(slot, 'recv')
|
||
})
|
||
this.bareOsLogicalFds[k] =
|
||
'bare-os-socket-bridge:udp-bound:' + host + ':' + boundUdpPort
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
fd,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
transport: 'udp',
|
||
port: boundUdpPort,
|
||
host
|
||
}
|
||
}
|
||
if (!slot || slot.state !== 'created') {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'bind: requires SOCK_STREAM bridge socket in created state.'
|
||
}
|
||
}
|
||
const port = Number(args.port)
|
||
const host =
|
||
String(args.host || args.address || '0.0.0.0').trim() || '0.0.0.0'
|
||
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'bind: port required (0–65535)'
|
||
}
|
||
}
|
||
slot.bindPort = port
|
||
slot.bindHost = host
|
||
slot.state = 'bound'
|
||
this.bareOsLogicalFds[k] =
|
||
'bare-os-socket-bridge:tcp-bound:' + host + ':' + port
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
fd,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
port,
|
||
host
|
||
}
|
||
}
|
||
if (name === 'listen') {
|
||
const fd = Number(args.fd)
|
||
if (!Number.isFinite(fd)) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'listen: fd required'
|
||
}
|
||
}
|
||
const k = String(fd >>> 0)
|
||
const slot =
|
||
this.bareOsSocketBridgeByFd &&
|
||
typeof this.bareOsSocketBridgeByFd === 'object'
|
||
? this.bareOsSocketBridgeByFd[k]
|
||
: null
|
||
if (!slot || slot.state !== 'bound') {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'listen: call bind(2) on the bridge fd first.'
|
||
}
|
||
}
|
||
const TcpServer =
|
||
bareLibrary.bareTcp &&
|
||
bareLibrary.bareTcp.Server &&
|
||
typeof bareLibrary.bareTcp.Server === 'function'
|
||
? bareLibrary.bareTcp.Server
|
||
: null
|
||
if (!TcpServer) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'ENOTSUP',
|
||
errnoHint: 'ENOTSUP',
|
||
posixAlignment: 'ENOTSUP',
|
||
note: 'listen: bareTcp.Server not merged in ctx.bare (bare-tcp bundle).'
|
||
}
|
||
}
|
||
const backlog = Math.min(
|
||
511,
|
||
Math.max(1, Math.floor(Number(args.backlog) || 511))
|
||
)
|
||
const acceptQmax = bareOsPosixAcceptQueueMax(shellEnv)
|
||
const dgramQmax = bareOsPosixDgramRecvQueueMax(shellEnv)
|
||
const server = new TcpServer()
|
||
slot.acceptQueue = []
|
||
server.on('connection', (sock) => {
|
||
if (!Array.isArray(slot.acceptQueue)) return
|
||
if (slot.acceptQueue.length >= acceptQmax) {
|
||
try {
|
||
sock.destroy?.()
|
||
} catch (_) {}
|
||
return
|
||
}
|
||
const wrap = {
|
||
sock,
|
||
tcpRecvQueue: [],
|
||
tcpRecvDropped: 0
|
||
}
|
||
sock.on('data', (message) => {
|
||
let u8 =
|
||
message instanceof Uint8Array ? message : b4a.from(message)
|
||
if (u8.byteLength > 1024 * 1024) u8 = u8.slice(0, 1024 * 1024)
|
||
if (wrap.tcpRecvQueue.length >= dgramQmax) {
|
||
wrap.tcpRecvDropped = (wrap.tcpRecvDropped || 0) + 1
|
||
return
|
||
}
|
||
wrap.tcpRecvQueue.push({ buf: u8.slice() })
|
||
bridgeNotify(wrap, 'recv')
|
||
})
|
||
slot.acceptQueue.push(wrap)
|
||
bridgeNotify(slot, 'accept')
|
||
})
|
||
try {
|
||
await new Promise((resolve, reject) => {
|
||
server.once('error', reject)
|
||
server.once('listening', resolve)
|
||
server.listen(slot.bindPort, slot.bindHost, backlog)
|
||
})
|
||
} catch (e) {
|
||
if (
|
||
!bindFallbackEnabled(shellEnv) ||
|
||
!isAddrInUse(e) ||
|
||
slot.bindPort === 0
|
||
) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EADDRINUSE',
|
||
errnoHint: 'EADDRINUSE',
|
||
posixAlignment: 'partial',
|
||
note: String(e && e.message ? e.message : e)
|
||
}
|
||
}
|
||
try {
|
||
await new Promise((resolve, reject) => {
|
||
server.once('error', reject)
|
||
server.once('listening', resolve)
|
||
server.listen(0, slot.bindHost, backlog)
|
||
})
|
||
} catch (e2) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EADDRINUSE',
|
||
errnoHint: 'EADDRINUSE',
|
||
posixAlignment: 'partial',
|
||
note: String(e2 && e2.message ? e2.message : e2)
|
||
}
|
||
}
|
||
const ephem = readListeningTcpPort(server, 0)
|
||
if (Number.isFinite(ephem) && ephem > 0) slot.bindPort = ephem
|
||
}
|
||
slot.server = server
|
||
slot.state = 'listening'
|
||
this.bareOsLogicalFds[k] =
|
||
'bare-os-socket-bridge:tcp-listen:' +
|
||
slot.bindHost +
|
||
':' +
|
||
slot.bindPort
|
||
const addr =
|
||
server.address && typeof server.address === 'function'
|
||
? server.address()
|
||
: null
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
fd,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
address: addr
|
||
}
|
||
}
|
||
if (name === 'accept') {
|
||
const fd = Number(args.fd)
|
||
if (!Number.isFinite(fd)) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'accept: listening fd required'
|
||
}
|
||
}
|
||
const k = String(fd >>> 0)
|
||
const slot =
|
||
this.bareOsSocketBridgeByFd &&
|
||
typeof this.bareOsSocketBridgeByFd === 'object'
|
||
? this.bareOsSocketBridgeByFd[k]
|
||
: null
|
||
if (!slot || slot.state !== 'listening') {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'accept: fd must be a listening SOCK_STREAM bridge socket.'
|
||
}
|
||
}
|
||
const nonblock = (this.bareOsLogicalFdFlags[k] & 0x800) === 0x800
|
||
const blockMax = bareOsPosixAcceptBlockMsMax(shellEnv)
|
||
const t0 = Date.now()
|
||
while (true) {
|
||
const q = slot.acceptQueue
|
||
if (Array.isArray(q) && q.length > 0) {
|
||
const wrap = q.shift()
|
||
const sock = wrap && wrap.sock
|
||
if (!sock) continue
|
||
const st = this.bareOsPosixFdSimState
|
||
const newFd = st.nextFd++
|
||
const nk = String(newFd)
|
||
this.bareOsSocketBridgeByFd[nk] = {
|
||
state: 'connected',
|
||
sock,
|
||
transport: 'tcp',
|
||
sockType: 1,
|
||
type: 1,
|
||
tcpRecvQueue: wrap.tcpRecvQueue,
|
||
tcpRecvDropped: wrap.tcpRecvDropped || 0,
|
||
recvWaiters: wrap.recvWaiters || [],
|
||
aliasKeys: new Set([nk])
|
||
}
|
||
this.bareOsRegisterLogicalFd(
|
||
newFd,
|
||
'bare-os-socket-bridge:tcp-accepted'
|
||
)
|
||
return {
|
||
ok: true,
|
||
op: name,
|
||
fd: newFd,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
listenFd: fd
|
||
}
|
||
}
|
||
if (nonblock) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EAGAIN',
|
||
errnoHint: 'EAGAIN',
|
||
posixAlignment: 'EAGAIN',
|
||
bridge: true,
|
||
note: 'accept: no connection ready (O_NONBLOCK)'
|
||
}
|
||
}
|
||
if (Date.now() - t0 >= blockMax) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EAGAIN',
|
||
errnoHint: 'EAGAIN',
|
||
posixAlignment: 'partial',
|
||
bridge: true,
|
||
note: 'accept: timed out waiting (same budget as BARE_OS_POSIX_DGRAM_RECV_BLOCK_MS_MAX)'
|
||
}
|
||
}
|
||
await bridgeWait(slot, blockMax - (Date.now() - t0), 'accept')
|
||
}
|
||
}
|
||
if (name === 'shutdown') {
|
||
const fd = Number(args.fd)
|
||
if (!Number.isFinite(fd)) {
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'shutdown: fd required'
|
||
}
|
||
}
|
||
return await bareOsShutdownSocketBridgeFd(
|
||
/** @type {Record<string, unknown>} */ (this),
|
||
fd,
|
||
args && typeof args === 'object'
|
||
? /** @type {Record<string, unknown>} */ (
|
||
args.how ??
|
||
/** @type {Record<string, unknown>} */ (args).shutdownHow
|
||
)
|
||
: undefined
|
||
)
|
||
}
|
||
if (
|
||
name === 'send' ||
|
||
name === 'sendmsg' ||
|
||
name === 'recv' ||
|
||
name === 'recvfrom' ||
|
||
name === 'recvmsg'
|
||
) {
|
||
const logicalOp = name
|
||
const fd = Number(args.fd)
|
||
if (!Number.isFinite(fd)) {
|
||
return {
|
||
ok: false,
|
||
op: logicalOp,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
posixAlignment: 'ENOTSUP',
|
||
note: 'send/sendmsg/recv/recvfrom/recvmsg: fd required'
|
||
}
|
||
}
|
||
const k = String(fd >>> 0)
|
||
const slot =
|
||
this.bareOsSocketBridgeByFd &&
|
||
typeof this.bareOsSocketBridgeByFd === 'object'
|
||
? this.bareOsSocketBridgeByFd[k]
|
||
: null
|
||
const udpReady =
|
||
slot &&
|
||
slot.transport === 'udp' &&
|
||
slot.sock &&
|
||
(slot.state === 'connected' || slot.state === 'udp_bound')
|
||
const tcpReady =
|
||
slot &&
|
||
slot.transport === 'tcp' &&
|
||
slot.sock &&
|
||
slot.state === 'connected'
|
||
if (!udpReady && !tcpReady) {
|
||
return {
|
||
ok: false,
|
||
op: logicalOp,
|
||
code: 'ENOTSUP',
|
||
errnoHint: 'ENOTSUP',
|
||
posixAlignment: 'ENOTSUP',
|
||
note: 'Bridge send/sendmsg/recv/recvfrom/recvmsg supported for SOCK_STREAM (connected) or SOCK_DGRAM (connected or bound) fds.'
|
||
}
|
||
}
|
||
if (slot.transport === 'tcp') {
|
||
if (logicalOp === 'send' || logicalOp === 'sendmsg') {
|
||
if (slot.tcpShutWr) {
|
||
return {
|
||
ok: false,
|
||
op: logicalOp,
|
||
code: 'EPIPE',
|
||
errnoHint: 'EPIPE',
|
||
posixAlignment: 'partial',
|
||
bridge: true,
|
||
transport: 'tcp',
|
||
note: 'send after SHUT_WR / half-close write side on TCP bridge (POSIX EPIPE-shaped).'
|
||
}
|
||
}
|
||
let payload
|
||
/** @type {{ from: number; to: number }[]} */
|
||
let scmRightsLocalDup = []
|
||
if (logicalOp === 'sendmsg') {
|
||
const prep = prepareBridgeSendmsg(
|
||
/** @type {Record<string, unknown>} */ (this),
|
||
args && typeof args === 'object'
|
||
? /** @type {Record<string, unknown>} */ (args)
|
||
: {}
|
||
)
|
||
if (!prep.ok) return prep.err
|
||
payload = prep.payload
|
||
scmRightsLocalDup = prep.scmRightsLocalDup
|
||
} else {
|
||
payload = args.buf ?? args.buffer ?? args.data
|
||
if (payload == null) {
|
||
return {
|
||
ok: false,
|
||
op: logicalOp,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL'
|
||
}
|
||
}
|
||
if (typeof payload === 'string')
|
||
payload = b4a.from(String(payload), 'utf8')
|
||
if (!(payload instanceof Uint8Array)) {
|
||
return {
|
||
ok: false,
|
||
op: logicalOp,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'send: buf must be Uint8Array or string'
|
||
}
|
||
}
|
||
}
|
||
await new Promise((resolve, reject) => {
|
||
try {
|
||
const w = slot.sock.write(payload, (err) =>
|
||
err ? reject(err) : resolve(undefined)
|
||
)
|
||
if (w === false) {
|
||
slot.sock.once('drain', () => resolve(undefined))
|
||
}
|
||
} catch (e) {
|
||
reject(e)
|
||
}
|
||
})
|
||
return {
|
||
ok: true,
|
||
op: logicalOp,
|
||
fd,
|
||
bytesSent: payload.byteLength,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
transport: 'tcp',
|
||
...(scmRightsLocalDup.length ? { scmRightsLocalDup } : {})
|
||
}
|
||
}
|
||
const maxBuf = Math.max(
|
||
1,
|
||
Math.min(
|
||
Number(args.len ?? args.length ?? args.max ?? 65536) || 65536,
|
||
1024 * 1024
|
||
)
|
||
)
|
||
const nonblock = (this.bareOsLogicalFdFlags[k] & 0x800) === 0x800
|
||
const t0 = Date.now()
|
||
const blockMax = bareOsPosixAcceptBlockMsMax(shellEnv)
|
||
while (true) {
|
||
const q = slot.tcpRecvQueue
|
||
if (Array.isArray(q) && q.length > 0) {
|
||
const row = q.shift()
|
||
if (row && row.buf instanceof Uint8Array) {
|
||
let out = row.buf
|
||
if (out.byteLength > maxBuf) {
|
||
const rest = out.slice(maxBuf)
|
||
out = out.slice(0, maxBuf)
|
||
q.unshift({ buf: rest })
|
||
}
|
||
const base = {
|
||
ok: true,
|
||
op: logicalOp,
|
||
fd,
|
||
bytesReceived: out.byteLength,
|
||
buf: out,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
transport: 'tcp',
|
||
tcpRecvDropped: slot.tcpRecvDropped || 0
|
||
}
|
||
if (logicalOp === 'recvfrom' || logicalOp === 'recvmsg') {
|
||
base.address = slot.sock.remoteAddress
|
||
base.port = slot.sock.remotePort
|
||
}
|
||
if (logicalOp === 'recvmsg') {
|
||
base.msgHdr = {
|
||
schema: 1,
|
||
name:
|
||
slot.sock.remoteAddress != null
|
||
? {
|
||
address: String(slot.sock.remoteAddress || ''),
|
||
port: Number(slot.sock.remotePort) >>> 0
|
||
}
|
||
: null,
|
||
flags: 0,
|
||
controllen: 0,
|
||
note: 'TCP bridge: single flat buffer; recv ancillary controllen 0 (socketMsgSurface schema 6).'
|
||
}
|
||
}
|
||
return base
|
||
}
|
||
}
|
||
if (slot.tcpSockError) {
|
||
return {
|
||
ok: false,
|
||
op: logicalOp,
|
||
code: 'ECONNRESET',
|
||
errnoHint: 'ECONNRESET',
|
||
posixAlignment: 'partial',
|
||
bridge: true,
|
||
transport: 'tcp',
|
||
note: 'recv: TCP socket error (bridge)',
|
||
tcpRecvDropped: slot.tcpRecvDropped || 0
|
||
}
|
||
}
|
||
if (
|
||
slot.tcpPeerEnded &&
|
||
(!Array.isArray(slot.tcpRecvQueue) ||
|
||
slot.tcpRecvQueue.length === 0)
|
||
) {
|
||
return {
|
||
ok: true,
|
||
op: logicalOp,
|
||
fd,
|
||
bytesReceived: 0,
|
||
buf: new Uint8Array(0),
|
||
eof: true,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
transport: 'tcp',
|
||
errnoHint: 'EOF',
|
||
note: 'recv: EOF after peer half-close / FIN (queue drained); aligns with poll POLLIN readiness.',
|
||
tcpRecvDropped: slot.tcpRecvDropped || 0
|
||
}
|
||
}
|
||
if (slot.tcpShutRd && (!q || q.length === 0)) {
|
||
return {
|
||
ok: true,
|
||
op: logicalOp,
|
||
fd,
|
||
bytesReceived: 0,
|
||
buf: new Uint8Array(0),
|
||
eof: true,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
transport: 'tcp',
|
||
note: 'recv: local SHUT_RD with empty queue (EOF-shaped).',
|
||
tcpRecvDropped: slot.tcpRecvDropped || 0
|
||
}
|
||
}
|
||
if (nonblock) {
|
||
return {
|
||
ok: false,
|
||
op: logicalOp,
|
||
code: 'EAGAIN',
|
||
errnoHint: 'EAGAIN',
|
||
posixAlignment: 'EAGAIN',
|
||
bridge: true,
|
||
note: 'recv: no data ready (O_NONBLOCK)',
|
||
tcpRecvDropped: slot.tcpRecvDropped || 0
|
||
}
|
||
}
|
||
if (Date.now() - t0 >= blockMax) {
|
||
return {
|
||
ok: false,
|
||
op: logicalOp,
|
||
code: 'EAGAIN',
|
||
errnoHint: 'EAGAIN',
|
||
posixAlignment: 'partial',
|
||
bridge: true,
|
||
note: 'recv: timed out waiting for TCP segment (BARE_OS_POSIX_DGRAM_RECV_BLOCK_MS_MAX budget)',
|
||
tcpRecvDropped: slot.tcpRecvDropped || 0
|
||
}
|
||
}
|
||
await bridgeWait(slot, blockMax - (Date.now() - t0), 'recv')
|
||
}
|
||
}
|
||
if (logicalOp === 'send' || logicalOp === 'sendmsg') {
|
||
let payload
|
||
/** @type {{ from: number; to: number }[]} */
|
||
let scmRightsLocalDupUdp = []
|
||
if (logicalOp === 'sendmsg') {
|
||
const prep = prepareBridgeSendmsg(
|
||
/** @type {Record<string, unknown>} */ (this),
|
||
args && typeof args === 'object'
|
||
? /** @type {Record<string, unknown>} */ (args)
|
||
: {}
|
||
)
|
||
if (!prep.ok) return prep.err
|
||
payload = prep.payload
|
||
scmRightsLocalDupUdp = prep.scmRightsLocalDup
|
||
} else {
|
||
payload = args.buf ?? args.buffer ?? args.data
|
||
if (payload == null) {
|
||
return {
|
||
ok: false,
|
||
op: logicalOp,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL'
|
||
}
|
||
}
|
||
if (typeof payload === 'string')
|
||
payload = b4a.from(String(payload), 'utf8')
|
||
if (!(payload instanceof Uint8Array)) {
|
||
return {
|
||
ok: false,
|
||
op: logicalOp,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
note: 'send: buf must be Uint8Array or string'
|
||
}
|
||
}
|
||
}
|
||
if (slot.state === 'udp_bound') {
|
||
const dp = Number(
|
||
args.port ?? args.peerPort ?? args.sin_port ?? args.sinPort
|
||
)
|
||
const dh = String(
|
||
args.host ||
|
||
args.address ||
|
||
args.peerHost ||
|
||
args.sin_addr ||
|
||
args.sinAddr ||
|
||
''
|
||
).trim()
|
||
if (!Number.isFinite(dp) || dp <= 0 || dp > 65535 || !dh) {
|
||
return {
|
||
ok: false,
|
||
op: logicalOp,
|
||
code: 'EINVAL',
|
||
errnoHint: 'EINVAL',
|
||
posixAlignment: 'partial',
|
||
note: 'send: bound SOCK_DGRAM bridge requires port and host (or connect(2) first for default peer).'
|
||
}
|
||
}
|
||
await new Promise((resolve, reject) => {
|
||
try {
|
||
slot.sock.send(payload, dp, dh, (err) =>
|
||
err ? reject(err) : resolve(undefined)
|
||
)
|
||
} catch (e) {
|
||
reject(e)
|
||
}
|
||
})
|
||
} else {
|
||
await new Promise((resolve, reject) => {
|
||
try {
|
||
slot.sock.send(payload, (err) =>
|
||
err ? reject(err) : resolve(undefined)
|
||
)
|
||
} catch (e) {
|
||
reject(e)
|
||
}
|
||
})
|
||
}
|
||
return {
|
||
ok: true,
|
||
op: logicalOp,
|
||
fd,
|
||
bytesSent: payload.byteLength,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
...(scmRightsLocalDupUdp.length
|
||
? { scmRightsLocalDup: scmRightsLocalDupUdp }
|
||
: {})
|
||
}
|
||
}
|
||
const maxBuf = Math.max(
|
||
1,
|
||
Math.min(
|
||
Number(args.len ?? args.length ?? args.max ?? 65536) || 65536,
|
||
1024 * 1024
|
||
)
|
||
)
|
||
const nonblock = (this.bareOsLogicalFdFlags[k] & 0x800) === 0x800
|
||
const wantFrom = logicalOp === 'recvfrom' || logicalOp === 'recvmsg'
|
||
const t0 = Date.now()
|
||
const blockMax = bareOsPosixDgramRecvBlockMsMax(shellEnv)
|
||
while (true) {
|
||
const q = slot.dgramRecvQueue
|
||
if (Array.isArray(q) && q.length > 0) {
|
||
const row = q.shift()
|
||
if (row && row.buf instanceof Uint8Array) {
|
||
let out = row.buf
|
||
if (out.byteLength > maxBuf) {
|
||
const rest = out.slice(maxBuf)
|
||
out = out.slice(0, maxBuf)
|
||
q.unshift({ buf: rest, rinfo: row.rinfo })
|
||
}
|
||
const base = {
|
||
ok: true,
|
||
op: logicalOp,
|
||
fd,
|
||
bytesReceived: out.byteLength,
|
||
buf: out,
|
||
bridge: true,
|
||
posixAlignment: 'partial',
|
||
dgramQueueDepth: q.length,
|
||
dgramRecvDropped: slot.dgramRecvDropped || 0
|
||
}
|
||
if (wantFrom && row.rinfo) {
|
||
base.address = row.rinfo.address
|
||
base.port = row.rinfo.port
|
||
}
|
||
if (logicalOp === 'recvmsg') {
|
||
base.msgHdr = {
|
||
schema: 1,
|
||
name: row.rinfo
|
||
? {
|
||
address: String(row.rinfo.address || ''),
|
||
port: Number(row.rinfo.port) >>> 0
|
||
}
|
||
: null,
|
||
flags: 0,
|
||
controllen: 0,
|
||
note: 'UDP bridge: single flat buf; recv controllen 0; logical SCM_RIGHTS apply only to sendmsg result (schema 4).'
|
||
}
|
||
}
|
||
return base
|
||
}
|
||
}
|
||
if (nonblock) {
|
||
return {
|
||
ok: false,
|
||
op: logicalOp,
|
||
code: 'EAGAIN',
|
||
errnoHint: 'EAGAIN',
|
||
posixAlignment: 'EAGAIN',
|
||
bridge: true,
|
||
note: 'recv: no datagram ready (O_NONBLOCK)',
|
||
dgramRecvDropped: slot.dgramRecvDropped || 0
|
||
}
|
||
}
|
||
if (Date.now() - t0 >= blockMax) {
|
||
return {
|
||
ok: false,
|
||
op: logicalOp,
|
||
code: 'EAGAIN',
|
||
errnoHint: 'EAGAIN',
|
||
posixAlignment: 'partial',
|
||
bridge: true,
|
||
note: 'recv: timed out waiting for datagram (BARE_OS_POSIX_DGRAM_RECV_BLOCK_MS_MAX)',
|
||
dgramRecvDropped: slot.dgramRecvDropped || 0
|
||
}
|
||
}
|
||
await bridgeWait(slot, blockMax - (Date.now() - t0), 'recv')
|
||
}
|
||
}
|
||
}
|
||
return {
|
||
ok: false,
|
||
op: name,
|
||
code: 'ENOSYS',
|
||
errnoHint: 'ENOSYS',
|
||
posixAlignment: 'ENOTSUP',
|
||
note: 'POSIX socket-style syscalls are not kernel FDs in Bare OS; use ctx.bare TCP/UDP modules, Hyperswarm, or Protomux-framed streams. Set BARE_OS_POSIX_SOCKET_FD_BRIDGE=1 for partial socket+connect when bareTcp is merged.'
|
||
}
|
||
}
|
||
if (name === 'posixPoll') {
|
||
const timeoutMs = Math.max(
|
||
0,
|
||
Math.min(60000, Math.floor(Number(args.timeoutMs) || 0))
|
||
)
|
||
if (timeoutMs > 0) {
|
||
return this.bareOsPosixPoll({
|
||
fds: Array.isArray(args.fds) ? args.fds : [],
|
||
timeoutMs
|
||
})
|
||
}
|
||
return this.bareOsPosixPollProbe({
|
||
fds: Array.isArray(args.fds) ? args.fds : []
|
||
})
|
||
}
|
||
if (name === 'kill') {
|
||
return this.bareOsSendSignal(args.target, String(args.signal || 'TERM'))
|
||
}
|
||
throw new Error(`bareOsSyscall: unsupported op "${name}"`)
|
||
}
|
||
}
|