From baa70dfb77730117a7d8bcdb75bb314b58163616 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 30 Jun 2026 16:58:02 -0700 Subject: [PATCH 1/2] Implement epoll for the JS filesystem Add epoll_create1/epoll_ctl/epoll_wait/epoll_pwait on the legacy (non-WASMFS) JS syscall layer, built on the per-inode readiness wait-queue: level- and edge-triggered modes, EPOLLONESHOT, EPOLLEXCLUSIVE, EPOLLRDHUP, nesting, and blocking waits under PROXY_TO_PTHREAD, ASYNCIFY, and JSPI. Also add emscripten_epoll_set_callback (new experimental ), a non-blocking variant that delivers an epoll set's readiness to a JS callback without ASYNCIFY/JSPI. --- ChangeLog.md | 9 + src/lib/libepoll.js | 403 ++++++++++++++++++ src/lib/libsigs.js | 2 + src/lib/libsyscall.js | 47 +- src/modules.mjs | 1 + src/struct_info.json | 24 ++ src/struct_info_generated.json | 18 + src/struct_info_generated_wasm64.json | 18 + system/include/emscripten/epoll.h | 57 +++ system/include/emscripten/syscalls.h | 1 + system/lib/libc/musl/src/linux/epoll.c | 10 + system/lib/wasmfs/syscalls.cpp | 6 + .../test_codesize_hello_dylink_all.json | 9 +- test/core/test_epoll.c | 98 +++++ test/core/test_epoll_advanced.c | 181 ++++++++ test/core/test_epoll_blocking_asyncify.c | 39 ++ test/core/test_epoll_callback.c | 74 ++++ test/core/test_epoll_callback_close.c | 46 ++ test/core/test_epoll_callback_edge.c | 62 +++ test/core/test_epoll_callback_level.c | 42 ++ test/core/test_epoll_callback_nested.c | 54 +++ test/core/test_epoll_callback_nested_close.c | 47 ++ test/core/test_epoll_callback_overflow.c | 61 +++ test/core/test_epoll_callback_replace.c | 56 +++ test/core/test_epoll_fairness.c | 41 ++ test/core/test_epoll_noderawfs.c | 59 +++ test/core/test_epoll_wait_and_callback.c | 100 +++++ test/sockets/test_epoll_callback.c | 75 ++++ test/sockets/test_epoll_rdhup.c | 79 ++++ test/sockets/test_epoll_socket_blocking.c | 84 ++++ test/test_core.py | 98 +++++ test/test_sockets.py | 50 +++ tools/maint/gen_sig_info.py | 2 + tools/native_sigs.py | 1 + 34 files changed, 1941 insertions(+), 13 deletions(-) create mode 100644 src/lib/libepoll.js create mode 100644 system/include/emscripten/epoll.h create mode 100644 test/core/test_epoll.c create mode 100644 test/core/test_epoll_advanced.c create mode 100644 test/core/test_epoll_blocking_asyncify.c create mode 100644 test/core/test_epoll_callback.c create mode 100644 test/core/test_epoll_callback_close.c create mode 100644 test/core/test_epoll_callback_edge.c create mode 100644 test/core/test_epoll_callback_level.c create mode 100644 test/core/test_epoll_callback_nested.c create mode 100644 test/core/test_epoll_callback_nested_close.c create mode 100644 test/core/test_epoll_callback_overflow.c create mode 100644 test/core/test_epoll_callback_replace.c create mode 100644 test/core/test_epoll_fairness.c create mode 100644 test/core/test_epoll_noderawfs.c create mode 100644 test/core/test_epoll_wait_and_callback.c create mode 100644 test/sockets/test_epoll_callback.c create mode 100644 test/sockets/test_epoll_rdhup.c create mode 100644 test/sockets/test_epoll_socket_blocking.c diff --git a/ChangeLog.md b/ChangeLog.md index e97b4adad87f5..6654a14aa0b07 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -40,6 +40,15 @@ See docs/process.md for more on how version tagging works. - The `GROWABLE_ARRAYBUFFERS` setting now defaults to 1, which means it will be used when available. Note that this only affects programs that are built with `ALLOW_MEMORY_GROWTH`, which is not enabled by default. (#27212) + backends with a `poll` handler must update. (#27207) +- Added support for `epoll` (`epoll_create1`/`epoll_ctl`/`epoll_wait`/ + `epoll_pwait`) on the legacy (non-WASMFS) JS filesystem, including + level- and edge-triggered modes, `EPOLLONESHOT`, `EPOLLEXCLUSIVE`, + `EPOLLRDHUP`, nesting, and blocking waits under `PROXY_TO_PTHREAD`, + `ASYNCIFY`, and `JSPI`. Also added `emscripten_epoll_set_callback` + (in the new ``, experimental), a non-blocking variant + that delivers an epoll set's readiness to a JS callback with no + `ASYNCIFY`/`JSPI`. (#27207) - New `-sNODERAWSOCKETS` setting that backs the POSIX sockets API with real TCP (`node:net`) and UDP (`node:dgram`) sockets on Node.js, with no `ws`, proxy process, or pthreads required. Supports incoming and outgoing TCP, UDP, IPv6, diff --git a/src/lib/libepoll.js b/src/lib/libepoll.js new file mode 100644 index 0000000000000..461600928a07a --- /dev/null +++ b/src/lib/libepoll.js @@ -0,0 +1,403 @@ +/** + * @license + * Copyright 2026 The Emscripten Authors + * SPDX-License-Identifier: MIT + */ + +// epoll(7) for the JS filesystem. The epoll syscalls and the +// emscripten_epoll_set_callback extension build on the per-inode readiness +// wait-queue (FSNode.addListener/notifyListeners) and the synchronous readiness +// derivation ($pollOne) defined in libsyscall.js. + +var EpollLibrary = { + // An epoll instance is a real FS fd whose stream carries an interest map + // `epoll` (fd -> reg) and a ready list (rdlHead/rdlTail). Each registration + // arms a persistent listener on the watched node's wait-queue at EPOLL_CTL_ADD + // (not per-wait), feeding the ready list on each edge so readiness can be + // tracked across waits and up a nesting chain. Being an fd, close(2) reclaims + // it (tearing every registration down) and it can itself be added to another + // epoll. + $newEpollInstance__internal: true, + $newEpollInstance__deps: ['$FS', '$pollOne', '$clearEpollInterest', '$reconcileEpollKeepalive', '$epollEvict'], + $newEpollInstance: () => FS.createStream({ + // Its own (detached) node, so the epoll fd can be watched by a parent epoll + // (nesting) and carry the readiness wait-queue methods. + node: new FS.FSNode(0, 'epoll', 0, 0), + epoll: new Map(), + stream_ops: { + // Readable when any listed registration is currently ready: this is what + // lets an epoll fd be polled/nested. Walks only the ready list (O(ready)); + // edge/oneshot/exclusive are reporting-time concerns, masked out here. A + // closed/reused fd is evicted here too, so a nested epoll that is only ever + // polled (never directly waited) does not accumulate dead registrations. + poll(stream) { + for (var reg = stream.rdlHead, next; reg; reg = next) { + next = reg.rdlNext; + if (FS.getStream(reg.fd)?.shared !== reg.shared) { epollEvict(stream, reg); continue; } + if (pollOne(reg.fd, reg.events & ~{{{ cDefs.EPOLLET | cDefs.EPOLLONESHOT | cDefs.EPOLLEXCLUSIVE }}})) { + return {{{ cDefs.POLLIN }}}; + } + } + return 0; + }, + // close(2): drop the readiness callback interest (if any), then every + // registration's listener (a fired EPOLLONESHOT has already dropped its + // own) from its watched node. + close(stream) { + // FS.close already fires POLLNVAL on this node, waking any parent epoll + // watching this epoll fd so it re-derives and drops the now-stale + // registration (via doEpollWait's shared check). + clearEpollInterest(stream); + reconcileEpollKeepalive(stream); // drop the keepalive if it was held + for (var reg of stream.epoll.values()) { + reg.listener?.listeners.delete(reg.listener.entry); + } + stream.epoll.clear(); + }, + }, + }), + + // Drop an epoll's persistent readiness callback interest: remove its listener + // on the epoll node and free the output buffer. Keepalive is managed by the + // caller (popped on clear/close, kept on replace). + $clearEpollInterest__internal: true, + $clearEpollInterest__deps: ['free'], + $clearEpollInterest: (ep) => { + var it = ep.interest; + if (!it) return; + ep.interest = null; + it.listener.listeners.delete(it.listener.entry); + _free(it.buf); + }, + + // A registered callback keeps the runtime alive only while it can still fire - + // i.e. while the epoll has at least one live registration. Once every watched + // fd is closed the set is terminal (it can never become ready again), so the + // keepalive is dropped and the runtime may exit. Reconciled after any change to + // the callback or the registration count. + $reconcileEpollKeepalive__internal: true, + $reconcileEpollKeepalive: (ep) => { + var want = !!ep.interest && ep.epoll.size > 0; + if (want == !!ep.keepalive) return; + ep.keepalive = want; +#if !MINIMAL_RUNTIME && (EXIT_RUNTIME || PTHREADS) + if (want) { {{{ runtimeKeepalivePush() }}} } else { {{{ runtimeKeepalivePop() }}} } +#endif + }, + + // The ready list (Linux's rdllist): registrations whose readiness edge has + // fired but not yet been consumed by a wait, linked intrusively through + // reg.rdlPrev/reg.rdlNext with head/tail on the epoll stream. Membership + // (reg.onList) is the edge state - a reg is listed on an edge (or when seeded + // ready at ctl), removed when a wait consumes it, and re-listed at the tail if + // a level trigger is still ready. O(1) add/remove, O(delivered) to drain. + $rdllistAdd__internal: true, + $rdllistAdd: (ep, reg) => { + if (reg.onList) return; + reg.onList = true; + reg.rdlPrev = ep.rdlTail; + reg.rdlNext = null; + if (ep.rdlTail) ep.rdlTail.rdlNext = reg; + else ep.rdlHead = reg; + ep.rdlTail = reg; + }, + $rdllistRemove__internal: true, + $rdllistRemove: (ep, reg) => { + if (!reg.onList) return; + reg.onList = false; + if (reg.rdlPrev) reg.rdlPrev.rdlNext = reg.rdlNext; + else ep.rdlHead = reg.rdlNext; + if (reg.rdlNext) reg.rdlNext.rdlPrev = reg.rdlPrev; + else ep.rdlTail = reg.rdlPrev; + reg.rdlPrev = reg.rdlNext = null; + }, + + // Remove a registration from its epoll: off the ready list, unlink its + // watched-node listener (a fired EPOLLONESHOT has none), drop from the interest + // map, and reconcile the callback keepalive. The single eviction primitive, + // used by EPOLL_CTL_DEL, a stale entry at ctl time, and a closed/reused fd seen + // at derive time (doEpollWait or the nesting poll). + $epollEvict__internal: true, + $epollEvict__deps: ['$rdllistRemove', '$reconcileEpollKeepalive'], + $epollEvict: (ep, reg) => { + rdllistRemove(ep, reg); + reg.listener?.listeners.delete(reg.listener.entry); + ep.epoll.delete(reg.fd); + reconcileEpollKeepalive(ep); + }, + + // The heavy lifting behind the epoll syscalls. The `__syscall_epoll_*` entry + // points stay in libsyscall.js (like every other syscall) and resolve the + // epoll stream before calling in here, so `ep` is a known-valid epoll stream. + $epollCtl__internal: true, + $epollCtl__deps: ['$FS', '$pollOne', '$rdllistAdd', '$epollEvict', '$reconcileEpollKeepalive'], + $epollCtl: (ep, op, fd, ev) => { + var target = FS.getStream(fd); + if (!target) return -{{{ cDefs.EBADF }}}; + if (op != {{{ cDefs.EPOLL_CTL_ADD }}} && op != {{{ cDefs.EPOLL_CTL_MOD }}} && op != {{{ cDefs.EPOLL_CTL_DEL }}}) { + return -{{{ cDefs.EINVAL }}}; + } + // An epoll cannot watch itself. + if (fd == ep.fd) return -{{{ cDefs.EINVAL }}}; + + // A registration keys on the open file description (stream.shared) - the + // struct-file analog that dup'd fds share. If this fd's number now resolves + // to a different open (closed and the slot reused), the old registration is + // stale: evict it so ctl sees the fd as fresh, matching Linux's eviction of + // the epitem when the watched file is released. + var cur = ep.epoll.get(fd); + if (cur && target.shared !== cur.shared) { + epollEvict(ep, cur); // stale: this fd number is now a different open + cur = undefined; + } + var has = !!cur; + if (op == {{{ cDefs.EPOLL_CTL_DEL }}}) { + if (!has) return -{{{ cDefs.ENOENT }}}; + epollEvict(ep, cur); + return 0; + } + + var events = {{{ makeGetValue('ev', C_STRUCTS.epoll_event.events, 'u32') }}}; + if (op == {{{ cDefs.EPOLL_CTL_ADD }}}) { + if (has) return -{{{ cDefs.EEXIST }}}; + // Only descriptors with a readiness derivation can be epoll-watched + // (sockets/pipes/epoll itself). Regular files have no poll handler and so + // are not epoll-capable, matching Linux (-EPERM). + if (!target.stream_ops?.poll) return -{{{ cDefs.EPERM }}}; + // Nesting another epoll: reject cycles, and chains deeper than 5 levels of + // epoll (ELOOP) - the Linux cap is EP_MAX_NESTS (4) plus the leaf level. + if (target.epoll) { + var reaches = (from, goal, seen) => { + if (from === goal) return true; + if (!from?.epoll || seen.has(from)) return false; + seen.add(from); + for (var f of from.epoll.keys()) { + if (reaches(FS.getStream(f), goal, seen)) return true; + } + return false; + }; + var depth = (s, seen) => { + if (!s?.epoll || seen.has(s)) return 0; + seen.add(s); + var max = 0; + for (var f of s.epoll.keys()) max = Math.max(max, depth(FS.getStream(f), seen)); + seen.delete(s); + return 1 + max; + }; + if (reaches(target, ep, new Set()) || 1 + depth(target, new Set()) > 5) { + return -{{{ cDefs.ELOOP }}}; + } + } + } else { // EPOLL_CTL_MOD + if (!has) return -{{{ cDefs.ENOENT }}}; + // EPOLLEXCLUSIVE may only be set at ADD time. + if (events & {{{ cDefs.EPOLLEXCLUSIVE }}}) return -{{{ cDefs.EINVAL }}}; + } + + // `data` is opaque user data echoed back by epoll_wait; keep its 8 bytes. + var reg = cur ?? {}; + reg.fd = fd; + reg.shared = target.shared; // open file description: the dup-shared identity + reg.events = events; + reg.dataLo = {{{ makeGetValue('ev', C_STRUCTS.epoll_event.data, 'i32') }}}; + reg.dataHi = {{{ makeGetValue('ev', C_STRUCTS.epoll_event.data + 4, 'i32') }}}; + if (op == {{{ cDefs.EPOLL_CTL_ADD }}}) { ep.epoll.set(fd, reg); reconcileEpollKeepalive(ep); } + // The registration's listener is its edge in the interest graph - present + // only while armed, so a watched node fires nothing for a dead edge. ADD + // installs it; a fired EPOLLONESHOT dropped it, so a MOD re-arm reinstalls it. + // (ep_poll_callback: on an edge, list the reg and wake any waiter on this + // epoll - and through ep.node any parent epoll nesting it.) + if (!reg.listener) { + reg.listener = target.node.addListener(() => { + rdllistAdd(ep, reg); + ep.node.notifyListeners({{{ cDefs.POLLIN }}}); + // EPOLLEXCLUSIVE: when one fd is watched by several epolls, the watched + // node wakes only one of them per edge (round-robin), not all. + }, !!(events & {{{ cDefs.EPOLLEXCLUSIVE }}})); + } + // Arming is itself an event source (ep_insert/ep_modify): a source-based + // model only learns readiness from edges, so sample the level now - the + // (re-)armed fd may already be ready with no producer notify to follow. + if (pollOne(fd, reg.events & ~{{{ cDefs.EPOLLET | cDefs.EPOLLONESHOT | cDefs.EPOLLEXCLUSIVE }}})) { + rdllistAdd(ep, reg); + ep.node.notifyListeners({{{ cDefs.POLLIN }}}); + } + return 0; + }, + + // Consume the ready list (Linux's ep_send_events), writing up to `maxevents` + // epoll_events into `ev` and returning the count. Each listed registration is + // re-derived against its current mask: level-triggered ones still ready are + // re-listed at the tail; edge-triggered ones leave the list until the next + // edge; EPOLLONESHOT ones drop their watched-node listener until re-armed by + // EPOLL_CTL_MOD; a no-longer-ready (spurious) edge is dropped; a closed/reused + // fd is evicted. + $doEpollWait__internal: true, + $doEpollWait__deps: ['$FS', '$pollOne', '$rdllistAdd', '$epollEvict'], + $doEpollWait: (ep, ev, maxevents) => { + // Detach the list and drain from the head: re-armed level triggers and the + // unprocessed remainder go back onto ep's now-empty list, so a single pass + // never revisits an entry. O(delivered), not O(registered). + var node = ep.rdlHead, tail = ep.rdlTail; + ep.rdlHead = ep.rdlTail = null; + var n = 0; + while (node && n < maxevents) { + var next = node.rdlNext; + node.onList = false; + node.rdlPrev = node.rdlNext = null; + var fd = node.fd; + if (FS.getStream(fd)?.shared !== node.shared) { + // The fd closed, or its number was reused for a different open: evict the + // now-stale registration (a surviving dup keeps the open file alive). + // Already detached from the list above, so epollEvict just unlinks the + // listener, drops it from the map, and reconciles the keepalive. + epollEvict(ep, node); + } else { + var revents = pollOne(fd, node.events & ~{{{ cDefs.EPOLLET | cDefs.EPOLLONESHOT | cDefs.EPOLLEXCLUSIVE }}}); + if (revents) { + var out = ev + {{{ C_STRUCTS.epoll_event.__size__ }}} * n; + {{{ makeSetValue('out', C_STRUCTS.epoll_event.events, 'revents', 'u32') }}}; + {{{ makeSetValue('out', C_STRUCTS.epoll_event.data, 'node.dataLo', 'i32') }}}; + {{{ makeSetValue('out', C_STRUCTS.epoll_event.data + 4, 'node.dataHi', 'i32') }}}; + n++; + if (node.events & {{{ cDefs.EPOLLONESHOT }}}) { + // Fired: a dead edge until EPOLL_CTL_MOD re-arms it, so drop its + // listener - the watched node stops poking it (no re-arm needed). + node.listener.listeners.delete(node.listener.entry); + node.listener = null; + } else if (!(node.events & {{{ cDefs.EPOLLET }}})) { + rdllistAdd(ep, node); // level: re-list at tail + } + } + // else: a spurious edge (no longer ready) - drop it from the list. + } + node = next; + } + // Stopped at maxevents with entries left: splice the unprocessed remainder + // (node..tail) back to the FRONT, ahead of any re-armed items, so the next + // wait services them first (round-robin fairness). + if (node) { + node.rdlPrev = null; + tail.rdlNext = ep.rdlHead; + if (ep.rdlHead) ep.rdlHead.rdlPrev = tail; + else ep.rdlTail = tail; + ep.rdlHead = node; + } + return n; + }, + + // The blocking wait behind __syscall_epoll_pwait; `ep` is a known-valid epoll + // stream and `maxevents` already validated by the entry point. + $epollPwait__internal: true, + $epollPwait__deps: ['$doEpollWait'], + $epollPwait: (ep, ev, maxevents, timeout) => { +#if PTHREADS || ASYNCIFY +#if PTHREADS + const isAsyncContext = PThread.currentProxiedOperationCallerThread; +#else + const isAsyncContext = true; +#endif + // Always resolve through a Promise here: when proxied from a worker the + // result is delivered by promise resolution, so a bare value would break + // the proxy (it has no `.then`). Block on the epoll's own readiness - each + // registration's persistent listener wakes ep.node on a leaf edge - and + // re-derive on wake, resolving the count or 0 after `timeout`. + if (isAsyncContext) { + return new Promise((resolve) => { + var count = doEpollWait(ep, ev, maxevents); + if (count || !timeout) { + resolve(count); + return; + } + var done = false; + var reg = ep.node.addListener(() => { + if (done) return; + var c = doEpollWait(ep, ev, maxevents); + if (c) finish(c); + }); + var timer = timeout > 0 ? setTimeout(() => finish(0), timeout) : undefined; + function finish(c) { + if (done) return; + done = true; + reg.listeners.delete(reg.entry); + if (timer) clearTimeout(timer); + resolve(c); + } + }); + } +#endif + var count = doEpollWait(ep, ev, maxevents); +#if ASSERTIONS + if (!count && timeout != 0) warnOnce('non-zero epoll_wait() timeout not supported: ' + timeout) +#endif + return count; + }, + + // Register a persistent readiness callback on an existing epoll fd: instead of + // blocking in epoll_wait, the runtime delivers the ready set to `callback` + // every time the epoll set makes progress. An epoll is a long-lived readiness + // aggregator, so the interest (a single listener on the epoll's own node plus a + // runtime-owned output buffer) is armed once and reused across every delivery - + // no per-spin register/deregister. Per-fd EPOLLET/EPOLLONESHOT apply exactly as + // in epoll_wait (one-shot is a property of the registration, not of this call), + // so a long-lived callback can mix level/edge/oneshot fds in one set. + // + // The interest persists until replaced (call again), cleared (callback == NULL), + // or the epoll fd is closed. It never suspends the stack, so it works without + // ASYNCIFY/JSPI, and it keeps the runtime alive while armed. Returns 0 or -errno. + emscripten_epoll_set_callback__deps: ['$FS', '$doEpollWait', '$clearEpollInterest', '$reconcileEpollKeepalive', '$callUserCallback', 'malloc', 'free'], + emscripten_epoll_set_callback__proxy: 'sync', + emscripten_epoll_set_callback: (epfd, maxevents, callback, userdata) => { + var ep = FS.getStream(epfd); + if (!ep?.epoll) return -{{{ cDefs.EBADF }}}; + // maxevents only matters when (re-)arming; validate before any mutation so a + // bad register call has no side effects (an unregister ignores it). + if (callback && maxevents <= 0) return -{{{ cDefs.EINVAL }}}; + + // Tear down any existing interest first - a second call replaces the + // callback, it does not stack. + clearEpollInterest(ep); + if (!callback) { + reconcileEpollKeepalive(ep); + return 0; + } + + // Runtime-owned output buffer reused across every delivery; freed at clear. + var buf = _malloc(maxevents * {{{ C_STRUCTS.epoll_event.__size__ }}}); + var it = ep.interest = {buf}; + // Producer notifies arrive synchronously (SOCKFS.emit, pipe writes); coalesce + // them into one delivery on a microtask (the callback must not run in the + // producer's/caller's stack), re-deriving the ready set at that tick. A + // microtask keeps delivery latency minimal (no setTimeout clamp). Edges are + // still disjoint from any concurrent blocking epoll_wait on the same epoll - + // that waiter drains synchronously in the producer's stack, ahead of this + // tick - but the tick may now run before vs after the waiter's async + // resumption; that relative ordering is not guaranteed. + function wake() { + if (it.scheduled) return; + it.scheduled = true; + queueMicrotask(() => { + it.scheduled = false; + if (ep.interest !== it) return; // cleared before the tick fired + var c = doEpollWait(ep, buf, maxevents); + if (c) { + callUserCallback(() => {{{ makeDynCall('vipip', 'callback') }}}(epfd, buf, c, userdata)); + // Still ready (overflow past maxevents, or a still-ready level fd + // re-listed): keep draining on the next tick. Note this is NOT a + // blocking epoll_wait loop - there the app owns the loop and may block + // elsewhere. A level-triggered fd that is structurally always ready and + // never drained (e.g. EPOLLOUT on a writable socket) will re-schedule a + // microtask each tick and so starve the event loop; use EPOLLET or + // unregister for such fds. + if (ep.interest === it && ep.rdlHead) wake(); + } + }); + } + it.listener = ep.node.addListener(wake); + reconcileEpollKeepalive(ep); // hold the runtime only while there are live fds + wake(); // deliver initial readiness if the set is already ready + return 0; + }, +}; + +addToLibrary(EpollLibrary); diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index d3a5b64b62605..b4bd6bfae8e02 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -240,6 +240,7 @@ sigs = { __syscall_epoll_create1__sig: 'ii', __syscall_epoll_ctl__sig: 'iiiip', __syscall_epoll_pwait__sig: 'iipiipp', + __syscall_epoll_pwait_nonblocking__sig: 'iipi', __syscall_faccessat__sig: 'iipii', __syscall_fallocate__sig: 'iiijj', __syscall_fchdir__sig: 'ii', @@ -637,6 +638,7 @@ sigs = { emscripten_destroy_web_audio_node__sig: 'vi', emscripten_destroy_worker__sig: 'vi', emscripten_enter_soft_fullscreen__sig: 'ipp', + emscripten_epoll_set_callback__sig: 'iiipp', emscripten_err__sig: 'vp', emscripten_errn__sig: 'vpp', emscripten_exit_fullscreen__sig: 'i', diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index e2add292a2a63..5aca9628a2eca 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -594,9 +594,7 @@ var SyscallsLibrary = { if (!stream) return {{{ cDefs.POLLNVAL }}}; // Streams without a poll handler (regular files, incl. NODERAWFS/NODEFS // which leave stream_ops unset) are treated as always readable+writable. - var flags = stream.stream_ops?.poll - ? stream.stream_ops.poll(stream) - : {{{ cDefs.POLLIN | cDefs.POLLOUT }}}; + var flags = stream.stream_ops?.poll?.(stream) ?? {{{ cDefs.POLLIN | cDefs.POLLOUT }}}; return flags & (events | {{{ cDefs.POLLERR }}} | {{{ cDefs.POLLHUP }}} | {{{ cDefs.POLLNVAL }}}); }, __syscall_poll__proxy: 'sync', @@ -702,13 +700,42 @@ var SyscallsLibrary = { __syscall_poll_nonblocking: (fds, nfds) => { return doPollSync(fds, nfds); }, - // epoll is not yet implemented in the legacy (non-WASMFS) JS syscall layer. - __syscall_epoll_create1__nothrow: true, - __syscall_epoll_create1: (flags) => -{{{ cDefs.ENOSYS }}}, - __syscall_epoll_ctl__nothrow: true, - __syscall_epoll_ctl: (epfd, op, fd, ev) => -{{{ cDefs.ENOSYS }}}, - __syscall_epoll_pwait__nothrow: true, - __syscall_epoll_pwait: (epfd, ev, maxevents, timeout, sigmask, sigsetsize) => -{{{ cDefs.ENOSYS }}}, + // epoll: the entry points live here (like every other syscall); the heavy + // lifting is in libepoll.js, which they call after resolving the epoll stream. + __syscall_epoll_create1__deps: ['$newEpollInstance'], + __syscall_epoll_create1__proxy: 'sync', + __syscall_epoll_create1: (flags) => { + return newEpollInstance().fd; + }, + __syscall_epoll_ctl__deps: ['$FS', '$epollCtl'], + __syscall_epoll_ctl__proxy: 'sync', + __syscall_epoll_ctl: (epfd, op, fd, ev) => { + var ep = FS.getStream(epfd); + if (!ep?.epoll) return -{{{ cDefs.EBADF }}}; + return epollCtl(ep, op, fd, ev); + }, + __syscall_epoll_pwait__proxy: 'sync', + __syscall_epoll_pwait__async: 'auto', + __syscall_epoll_pwait__deps: ['$FS', '$epollPwait'], + __syscall_epoll_pwait: (epfd, ev, maxevents, timeout, sigmask, sigsetsize) => { + var ep = FS.getStream(epfd); + if (!ep?.epoll) return -{{{ cDefs.EBADF }}}; + if (maxevents <= 0) return -{{{ cDefs.EINVAL }}}; + return epollPwait(ep, ev, maxevents, timeout); + }, + // libc routes zero-timeout epoll_wait()/epoll_pwait() calls here: a plain + // import that never suspends, so probes stay callable from any context (under + // JSPI, __syscall_epoll_pwait is a suspending import and traps when called + // from a stack that wasn't entered through a promising export). Mirrors + // __syscall_poll_nonblocking. + __syscall_epoll_pwait_nonblocking__proxy: 'sync', + __syscall_epoll_pwait_nonblocking__deps: ['$FS', '$doEpollWait'], + __syscall_epoll_pwait_nonblocking: (epfd, ev, maxevents) => { + var ep = FS.getStream(epfd); + if (!ep?.epoll) return -{{{ cDefs.EBADF }}}; + if (maxevents <= 0) return -{{{ cDefs.EINVAL }}}; + return doEpollWait(ep, ev, maxevents); + }, __syscall_getcwd__deps: ['$lengthBytesUTF8', '$stringToUTF8'], __syscall_getcwd: (buf, size) => { if (size === 0) return -{{{ cDefs.EINVAL }}}; diff --git a/src/modules.mjs b/src/modules.mjs index bc6aa5295f5fa..83a7bde92f3a2 100644 --- a/src/modules.mjs +++ b/src/modules.mjs @@ -88,6 +88,7 @@ function calculateLibraries() { if (!WASMFS) { libraries.push('libsyscall.js'); + libraries.push('libepoll.js'); } if (MAIN_MODULE) { diff --git a/src/struct_info.json b/src/struct_info.json index e4852a0b44b92..30d6c271c2d83 100644 --- a/src/struct_info.json +++ b/src/struct_info.json @@ -141,6 +141,30 @@ ] } }, + { + "file": "sys/epoll.h", + "defines": [ + "EPOLLIN", + "EPOLLOUT", + "EPOLLERR", + "EPOLLHUP", + "EPOLLRDNORM", + "EPOLLWRNORM", + "EPOLLET", + "EPOLLONESHOT", + "EPOLLEXCLUSIVE", + "EPOLL_CTL_ADD", + "EPOLL_CTL_DEL", + "EPOLL_CTL_MOD", + "EPOLL_CLOEXEC" + ], + "structs": { + "epoll_event": [ + "events", + "data" + ] + } + }, { "file": "time.h", "defines": [ diff --git a/src/struct_info_generated.json b/src/struct_info_generated.json index 54c755a8dd05c..791ec049ac6f3 100644 --- a/src/struct_info_generated.json +++ b/src/struct_info_generated.json @@ -260,6 +260,19 @@ "EPERM": 63, "EPFNOSUPPORT": 139, "EPIPE": 64, + "EPOLLERR": 8, + "EPOLLET": -2147483648, + "EPOLLEXCLUSIVE": 268435456, + "EPOLLHUP": 16, + "EPOLLIN": 1, + "EPOLLONESHOT": 1073741824, + "EPOLLOUT": 4, + "EPOLLRDNORM": 64, + "EPOLLWRNORM": 256, + "EPOLL_CLOEXEC": 524288, + "EPOLL_CTL_ADD": 1, + "EPOLL_CTL_DEL": 2, + "EPOLL_CTL_MOD": 3, "EPROTO": 65, "EPROTONOSUPPORT": 66, "EPROTOTYPE": 67, @@ -1018,6 +1031,11 @@ "stack_ptr": 8, "user_data": 16 }, + "epoll_event": { + "__size__": 16, + "data": 8, + "events": 0 + }, "flock": { "__size__": 32, "l_type": 0 diff --git a/src/struct_info_generated_wasm64.json b/src/struct_info_generated_wasm64.json index 22786bca5b884..6f93190cc19df 100644 --- a/src/struct_info_generated_wasm64.json +++ b/src/struct_info_generated_wasm64.json @@ -260,6 +260,19 @@ "EPERM": 63, "EPFNOSUPPORT": 139, "EPIPE": 64, + "EPOLLERR": 8, + "EPOLLET": -2147483648, + "EPOLLEXCLUSIVE": 268435456, + "EPOLLHUP": 16, + "EPOLLIN": 1, + "EPOLLONESHOT": 1073741824, + "EPOLLOUT": 4, + "EPOLLRDNORM": 64, + "EPOLLWRNORM": 256, + "EPOLL_CLOEXEC": 524288, + "EPOLL_CTL_ADD": 1, + "EPOLL_CTL_DEL": 2, + "EPOLL_CTL_MOD": 3, "EPROTO": 65, "EPROTONOSUPPORT": 66, "EPROTOTYPE": 67, @@ -1018,6 +1031,11 @@ "stack_ptr": 16, "user_data": 32 }, + "epoll_event": { + "__size__": 16, + "data": 8, + "events": 0 + }, "flock": { "__size__": 32, "l_type": 0 diff --git a/system/include/emscripten/epoll.h b/system/include/emscripten/epoll.h new file mode 100644 index 0000000000000..b87f5ce5cbd62 --- /dev/null +++ b/system/include/emscripten/epoll.h @@ -0,0 +1,57 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// EXPERIMENTAL. This API is new and may change (signature or semantics) over the +// next few releases; it is not yet covered by Emscripten's stability guarantees. +// +// Register a persistent readiness callback on an existing epoll fd (built with +// epoll_create1/epoll_ctl): instead of blocking in epoll_wait, the runtime calls +// `callback` every time the set makes progress, delivering up to `maxevents` +// ready events. An epoll is a long-lived readiness aggregator, so the interest is +// armed once and reused across every delivery - no per-spin re-arming. Unlike +// epoll_wait it never blocks the calling stack, so it works without ASYNCIFY/JSPI. +// The callback is delivered on the main thread's event loop; under +// PROXY_TO_PTHREAD use a blocking epoll_wait from the pthread instead. +// +// While armed it keeps the runtime alive only as long as it can still fire - i.e. +// while the epoll has at least one open watched fd. Once every watched fd is +// closed the set is terminal (it can never become ready again) and the callback +// stops holding the runtime, so no explicit disposal is required in that case. +// To dispose while open fds remain, either pass a NULL `callback` (any +// `maxevents`) to unregister, or close the epoll fd. There is at most one +// callback per epoll: calling again replaces it (it does not stack). `events` is +// a runtime-owned buffer valid only for the duration of each callback. Returns 0, +// or -errno (-EBADF if `epfd` is not an epoll fd, -EINVAL). +// +// Each registration's trigger mode (set per-fd via epoll_ctl) controls how often +// the callback fires for it - identically to epoll_wait, so one callback can mix +// modes: +// - Level-triggered (the default): the callback fires on the next tick whenever +// the fd is ready, and keeps firing while it stays ready. The runtime - not +// the application - drives the loop, so an fd that is structurally always +// ready and never drained (notably EPOLLOUT on a writable socket) will spin +// the event loop. Use one of the modes below for such fds. +// - EPOLLET (edge-triggered): the callback fires once per readiness edge and +// not again until a fresh edge. Drain the fd fully on each delivery; this is +// the way to watch an always-writable fd without spinning. +// - EPOLLONESHOT: the callback fires once for that fd, then the registration is +// disabled until you re-arm it with epoll_ctl(EPOLL_CTL_MOD). Use it to +// handle an fd exactly once (e.g. before handing it elsewhere). +typedef void (*em_epoll_callback)(int epfd, struct epoll_event *events, int nready, void *userdata); +int emscripten_epoll_set_callback(int epfd, int maxevents, em_epoll_callback callback, void *userdata); + +#ifdef __cplusplus +} +#endif diff --git a/system/include/emscripten/syscalls.h b/system/include/emscripten/syscalls.h index 7864de1efc33f..889ad14d4edb0 100644 --- a/system/include/emscripten/syscalls.h +++ b/system/include/emscripten/syscalls.h @@ -124,6 +124,7 @@ int __syscall_shutdown(int sockfd, int how, int unused1, int unused2, int unused int __syscall_epoll_create1(int flags); int __syscall_epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev); int __syscall_epoll_pwait(int epfd, struct epoll_event *ev, int maxevents, int timeout, const sigset_t *sigmask, size_t sigsetsize); +int __syscall_epoll_pwait_nonblocking(int epfd, struct epoll_event *ev, int maxevents); #ifdef __cplusplus } diff --git a/system/lib/libc/musl/src/linux/epoll.c b/system/lib/libc/musl/src/linux/epoll.c index e56e8f4c8293b..5998d9b28ee93 100644 --- a/system/lib/libc/musl/src/linux/epoll.c +++ b/system/lib/libc/musl/src/linux/epoll.c @@ -25,6 +25,16 @@ int epoll_ctl(int fd, int op, int fd2, struct epoll_event *ev) int epoll_pwait(int fd, struct epoll_event *ev, int cnt, int to, const sigset_t *sigs) { +#ifdef __EMSCRIPTEN__ + // A zero timeout is an instantaneous probe: route it through a plain + // import that never suspends. Under JSPI, __syscall_epoll_pwait is a + // suspending import and so may only be called from a stack entered through + // a promising export — a requirement a readiness probe must not carry + // (e.g. probes from event-loop callbacks). Mirrors poll() above. + if (to == 0) { + return __syscall_ret(__syscall_epoll_pwait_nonblocking(fd, ev, cnt)); + } +#endif int r = __syscall_cp(SYS_epoll_pwait, fd, ev, cnt, to, sigs, _NSIG/8); #ifdef SYS_epoll_wait if (r==-ENOSYS && !sigs) r = __syscall_cp(SYS_epoll_wait, fd, ev, cnt, to); diff --git a/system/lib/wasmfs/syscalls.cpp b/system/lib/wasmfs/syscalls.cpp index 66bfb0e2fd9ba..619930635143d 100644 --- a/system/lib/wasmfs/syscalls.cpp +++ b/system/lib/wasmfs/syscalls.cpp @@ -1862,4 +1862,10 @@ int __syscall_epoll_pwait(int epfd, return -ENOSYS; } +int __syscall_epoll_pwait_nonblocking(int epfd, + struct epoll_event* ev, + int maxevents) { + return -ENOSYS; +} + } // extern "C" diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index ed5c3055fd6f3..c2ae6d3ac6ac2 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 268311, - "a.out.nodebug.wasm": 587978, - "total": 856289, + "a.out.js": 271378, + "a.out.nodebug.wasm": 588038, + "total": 859416, "sent": [ "IMG_Init", "IMG_Load", @@ -225,6 +225,7 @@ "__syscall_epoll_create1", "__syscall_epoll_ctl", "__syscall_epoll_pwait", + "__syscall_epoll_pwait_nonblocking", "__syscall_faccessat", "__syscall_fallocate", "__syscall_fchdir", @@ -455,6 +456,7 @@ "emscripten_debugger", "emscripten_destroy_worker", "emscripten_enter_soft_fullscreen", + "emscripten_epoll_set_callback", "emscripten_err", "emscripten_errn", "emscripten_exit_fullscreen", @@ -1750,6 +1752,7 @@ "env.__syscall_epoll_create1", "env.__syscall_epoll_ctl", "env.__syscall_epoll_pwait", + "env.__syscall_epoll_pwait_nonblocking", "env.__syscall_faccessat", "env.__syscall_fallocate", "env.__syscall_fchdir", diff --git a/test/core/test_epoll.c b/test/core/test_epoll.c new file mode 100644 index 0000000000000..e9884c529b1fc --- /dev/null +++ b/test/core/test_epoll.c @@ -0,0 +1,98 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * Exercises the epoll syscall surface (epoll_create1/epoll_ctl/epoll_wait): + * the interest set, the ADD/MOD/DEL ops with their error returns, readiness + * derivation over a pipe, and that the opaque `data` is echoed back. + */ + +#include +#include +#include +#include +#include +#include + +int main(void) { + int ep = epoll_create1(0); + assert(ep >= 0); + + int p[2]; + assert(pipe(p) == 0); + + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.fd = p[0]; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &ev) == 0); + assert(epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &ev) == -1 && errno == EEXIST); + + struct epoll_event out[4]; + // Nothing written yet: the read end is not readable. + assert(epoll_wait(ep, out, 4, 0) == 0); + + // Make the read end readable. + assert(write(p[1], "x", 1) == 1); + assert(epoll_wait(ep, out, 4, 0) == 1); + assert(out[0].events & EPOLLIN); + assert(out[0].data.fd == p[0]); + + // MOD to a condition that is not satisfied (writable on the read end). + ev.events = EPOLLOUT; + assert(epoll_ctl(ep, EPOLL_CTL_MOD, p[0], &ev) == 0); + assert(epoll_wait(ep, out, 4, 0) == 0); + + // DEL, and DEL again -> ENOENT. + assert(epoll_ctl(ep, EPOLL_CTL_DEL, p[0], &ev) == 0); + assert(epoll_ctl(ep, EPOLL_CTL_DEL, p[0], &ev) == -1 && errno == ENOENT); + assert(epoll_wait(ep, out, 4, 0) == 0); + + // Bad epoll fd and bad target fd. + assert(epoll_ctl(ep, EPOLL_CTL_ADD, 9999, &ev) == -1 && errno == EBADF); + assert(epoll_ctl(9999, EPOLL_CTL_ADD, p[0], &ev) == -1 && errno == EBADF); + + // Bad op. + ev.events = EPOLLIN; + assert(epoll_ctl(ep, 999, p[0], &ev) == -1 && errno == EINVAL); + + // An epoll cannot watch itself. + assert(epoll_ctl(ep, EPOLL_CTL_ADD, ep, &ev) == -1 && errno == EINVAL); + + // Regular files are not epoll-capable (no readiness derivation -> EPERM). + int rf = open("/tmp/epoll_regular", O_CREAT | O_RDWR, 0600); + assert(rf >= 0); + assert(epoll_ctl(ep, EPOLL_CTL_ADD, rf, &ev) == -1 && errno == EPERM); + close(rf); + + // maxevents must be positive. + assert(epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &ev) == 0); + assert(epoll_wait(ep, out, 0, 0) == -1 && errno == EINVAL); + assert(epoll_wait(ep, out, -1, 0) == -1 && errno == EINVAL); + + // fd reuse: a registration keys on the open file description, so closing a + // watched fd and reusing its number for a different open must not resurrect + // the registration onto the new fd (which would report wrong readiness). + assert(epoll_ctl(ep, EPOLL_CTL_DEL, p[0], &ev) == 0); // empty the set + int a[2]; + assert(pipe(a) == 0); + ev.data.fd = a[0]; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, a[0], &ev) == 0); + close(a[0]); // the registration is now stale + int b[2]; + assert(pipe(b) == 0); + assert(b[0] == a[0]); // b[0] reused a[0]'s freed number + assert(write(b[1], "y", 1) == 1); // the reused fd is readable + assert(epoll_wait(ep, out, 4, 0) == 0); // stale registration must not fire + // The stale entry is gone, so the reused fd adds fresh (evicted, not EEXIST) + // and then reports normally. + ev.data.fd = b[0]; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, b[0], &ev) == 0); + assert(epoll_wait(ep, out, 4, 0) == 1 && out[0].data.fd == b[0]); + + close(ep); + close(p[0]); + close(p[1]); + printf("EPOLL PASS\n"); + return 0; +} diff --git a/test/core/test_epoll_advanced.c b/test/core/test_epoll_advanced.c new file mode 100644 index 0000000000000..13021a13a995c --- /dev/null +++ b/test/core/test_epoll_advanced.c @@ -0,0 +1,181 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * The richer epoll semantics, all exercised non-blocking (timeout 0) over pipes: + * EPOLLONESHOT (fire once, re-arm with MOD), EPOLLET (edge-triggered), the + * EPOLLEXCLUSIVE ctl restriction and its round-robin single-wakeup across epolls + * watching one fd, nesting one epoll inside another, ELOOP rejection of cycles + * and over-deep chains, and auto-removal of a registration whose fd is closed. + */ + +#include +#include +#include +#include +#include + +static int ready(int ep) { + struct epoll_event out[4]; + return epoll_wait(ep, out, 4, 0); +} + +static void test_oneshot(void) { + int ep = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + + struct epoll_event ev = { .events = EPOLLIN | EPOLLONESHOT }; + ev.data.fd = p[0]; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &ev) == 0); + + assert(write(p[1], "x", 1) == 1); + assert(ready(ep) == 1); // fires once + assert(ready(ep) == 0); // silent until re-armed, despite still readable + + ev.events = EPOLLIN | EPOLLONESHOT; + assert(epoll_ctl(ep, EPOLL_CTL_MOD, p[0], &ev) == 0); + assert(ready(ep) == 1); // re-armed -> fires again + + close(ep); close(p[0]); close(p[1]); +} + +static void test_edge(void) { + int ep = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + + struct epoll_event ev = { .events = EPOLLIN | EPOLLET }; + ev.data.fd = p[0]; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &ev) == 0); + + assert(write(p[1], "x", 1) == 1); + assert(ready(ep) == 1); // reports on the edge + assert(ready(ep) == 0); // not re-reported while continuously ready + + assert(write(p[1], "y", 1) == 1); + assert(ready(ep) == 1); // a fresh write is a fresh edge + + close(ep); close(p[0]); close(p[1]); +} + +static void test_exclusive(void) { + int ep = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + + struct epoll_event ev = { .events = EPOLLIN | EPOLLEXCLUSIVE }; + ev.data.fd = p[0]; + // EPOLLEXCLUSIVE is accepted at ADD and otherwise functions. + assert(epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &ev) == 0); + assert(write(p[1], "x", 1) == 1); + assert(ready(ep) == 1); + // EPOLLEXCLUSIVE may not be combined with MOD. + assert(epoll_ctl(ep, EPOLL_CTL_MOD, p[0], &ev) == -1 && errno == EINVAL); + + close(ep); close(p[0]); close(p[1]); +} + +static void test_exclusive_wakeup(void) { + // One fd watched by two epolls with EPOLLEXCLUSIVE: each readiness edge wakes + // only one of them, rotating - not both (no thundering herd). Edge-triggered so + // a delivered item is not re-listed, making "who was woken" unambiguous. + int epA = epoll_create1(0), epB = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + struct epoll_event ev = { .events = EPOLLIN | EPOLLET | EPOLLEXCLUSIVE }; + ev.data.fd = p[0]; + assert(epoll_ctl(epA, EPOLL_CTL_ADD, p[0], &ev) == 0); + assert(epoll_ctl(epB, EPOLL_CTL_ADD, p[0], &ev) == 0); + + assert(write(p[1], "x", 1) == 1); // first edge -> exactly one epoll (epA) + assert(ready(epA) == 1); + assert(ready(epB) == 0); + + assert(write(p[1], "y", 1) == 1); // next edge -> the other (epB), round-robin + assert(ready(epA) == 0); + assert(ready(epB) == 1); + + close(epA); close(epB); close(p[0]); close(p[1]); +} + +static void test_nesting(void) { + int epA = epoll_create1(0); + int epB = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.fd = p[0]; + assert(epoll_ctl(epB, EPOLL_CTL_ADD, p[0], &ev) == 0); + + ev.events = EPOLLIN; + ev.data.fd = epB; + assert(epoll_ctl(epA, EPOLL_CTL_ADD, epB, &ev) == 0); + + assert(ready(epA) == 0); // leaf not yet ready -> epB not ready -> epA quiet + assert(write(p[1], "x", 1) == 1); + + struct epoll_event out[4]; + assert(epoll_wait(epA, out, 4, 0) == 1); // leaf readiness propagates to epA + assert(out[0].data.fd == epB); + assert(out[0].events & EPOLLIN); + + close(epA); close(epB); close(p[0]); close(p[1]); +} + +static void test_eloop(void) { + struct epoll_event ev = { .events = EPOLLIN }; + + // A direct cycle is rejected: a watches b, then b watching a closes the loop. + int a = epoll_create1(0), b = epoll_create1(0); + ev.data.fd = b; + assert(epoll_ctl(a, EPOLL_CTL_ADD, b, &ev) == 0); + ev.data.fd = a; + assert(epoll_ctl(b, EPOLL_CTL_ADD, a, &ev) == -1 && errno == ELOOP); + close(a); close(b); + + // A chain six epolls deep is one level too far. Build e[4]->e[5] ... e[1]->e[2] + // (all accepted), then adding e[1] into e[0] would make a 6-level chain. + int e[6]; + for (int i = 0; i < 6; i++) e[i] = epoll_create1(0); + for (int i = 5; i >= 2; i--) { + ev.data.fd = e[i]; + assert(epoll_ctl(e[i - 1], EPOLL_CTL_ADD, e[i], &ev) == 0); + } + ev.data.fd = e[1]; + assert(epoll_ctl(e[0], EPOLL_CTL_ADD, e[1], &ev) == -1 && errno == ELOOP); + for (int i = 0; i < 6; i++) close(e[i]); +} + +static void test_autoremove(void) { + int ep = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.fd = p[0]; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &ev) == 0); + + // Closing the watched fd drops the registration; the wait neither crashes nor + // reports the dead fd. + close(p[0]); + close(p[1]); + assert(ready(ep) == 0); + + close(ep); +} + +int main(void) { + test_oneshot(); + test_edge(); + test_exclusive(); + test_exclusive_wakeup(); + test_nesting(); + test_eloop(); + test_autoremove(); + printf("EPOLL ADVANCED PASS\n"); + return 0; +} diff --git a/test/core/test_epoll_blocking_asyncify.c b/test/core/test_epoll_blocking_asyncify.c new file mode 100644 index 0000000000000..fccd375b28bcd --- /dev/null +++ b/test/core/test_epoll_blocking_asyncify.c @@ -0,0 +1,39 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * A blocking epoll_wait() that suspends the wasm stack (ASYNCIFY/JSPI) and is + * woken by a pipe write scheduled to run only after it has blocked. + */ + +#include +#include +#include +#include +#include + +static int wfd; +static void writer(void* arg) { assert(write(wfd, "x", 1) == 1); } + +int main(void) { + int ep = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + wfd = p[1]; + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.u32 = 0xabcd; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &ev) == 0); + + // The write happens only after epoll_wait suspends. + emscripten_async_call(writer, NULL, 0); + + struct epoll_event out[4]; + int n = epoll_wait(ep, out, 4, -1); + assert(n == 1); + assert(out[0].events & EPOLLIN); + assert(out[0].data.u32 == 0xabcd); + printf("done\n"); + return 0; +} diff --git a/test/core/test_epoll_callback.c b/test/core/test_epoll_callback.c new file mode 100644 index 0000000000000..c5d04e9d1e5cd --- /dev/null +++ b/test/core/test_epoll_callback.c @@ -0,0 +1,74 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * emscripten_epoll_set_callback: a persistent, non-blocking, non-suspending epoll + * readiness callback (no ASYNCIFY/JSPI). A single arm delivers repeatedly. The + * arming itself is an event source - matching Linux, where the set becomes ready + * with no producer wakeup to follow: + * - EPOLL_CTL_ADD of an already-readable fd reports it. + * - EPOLL_CTL_MOD re-arming a still-readable EPOLLONESHOT fd reports it again. + * Clearing the interest (NULL callback) stops delivery and lets the runtime exit. + */ + +#include +#include +#include +#include +#include +#include + +static int ep, rfd, wfd; +static int fires; + +static void arm_rfd(int op) { + struct epoll_event ev = { .events = EPOLLIN | EPOLLONESHOT }; + ev.data.u32 = 0x1234; + assert(epoll_ctl(ep, op, rfd, &ev) == 0); +} + +static void on_ready(int epfd, struct epoll_event* events, int nready, void* ud) { + assert(epfd == ep); + assert(nready == 1); + assert(events[0].events & EPOLLIN); + assert(events[0].data.u32 == 0x1234); + assert((long)ud == 42); + fires++; + + if (fires == 1) { + // EPOLLONESHOT disabled the registration on this delivery, but the byte is + // still in the pipe (level-readable). Re-arm with MOD WITHOUT draining: with + // no producer event to follow, only the MOD poke can re-evaluate readiness. + arm_rfd(EPOLL_CTL_MOD); + return; + } + + assert(fires == 2); + // Drain, clear the interest, then make the set ready again: with the callback + // cleared there is nothing left to fire, and the runtime exits cleanly. + char b[1]; + assert(read(rfd, b, 1) == 1); + assert(emscripten_epoll_set_callback(ep, 4, NULL, NULL) == 0); + assert(write(wfd, "x", 1) == 1); + arm_rfd(EPOLL_CTL_MOD); + printf("done\n"); +} + +int main(void) { + ep = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + rfd = p[0]; + wfd = p[1]; + + // Arm the persistent callback on an empty set: nothing ready, no fire. + assert(emscripten_epoll_set_callback(ep, 4, on_ready, (void*)42) == 0); + + // Make rfd readable, then ADD it. The fd is already ready with no producer + // wakeup to come, so the ADD itself must trigger the first delivery. + assert(write(wfd, "x", 1) == 1); + arm_rfd(EPOLL_CTL_ADD); + return 0; +} diff --git a/test/core/test_epoll_callback_close.c b/test/core/test_epoll_callback_close.c new file mode 100644 index 0000000000000..e0c80b92d5087 --- /dev/null +++ b/test/core/test_epoll_callback_close.c @@ -0,0 +1,46 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * A registered callback keeps the runtime alive only while its epoll can still + * fire. Closing the watched fd makes the set terminal (nothing it watches can + * become ready again), so the keepalive is dropped and the process exits with no + * explicit unregister - here over a pipe, exercising the PIPEFS close -> wake -> + * evict path (the same property the sockets test relies on for SOCKFS). + */ + +#include +#include +#include +#include +#include +#include + +static int ep, rfd, wfd; + +static void on_ready(int epfd, struct epoll_event* ev, int n, void* ud) { + assert(n == 1 && (ev[0].events & EPOLLIN)); + char b[1]; + assert(read(rfd, b, 1) == 1); + printf("done\n"); + // No unregister: closing the watched fd alone must let the runtime exit. + close(rfd); + close(wfd); +} + +int main(void) { + ep = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + rfd = p[0]; + wfd = p[1]; + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.fd = rfd; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, rfd, &ev) == 0); + + assert(emscripten_epoll_set_callback(ep, 4, on_ready, 0) == 0); + assert(write(wfd, "x", 1) == 1); + return 0; +} diff --git a/test/core/test_epoll_callback_edge.c b/test/core/test_epoll_callback_edge.c new file mode 100644 index 0000000000000..ce37269e140c7 --- /dev/null +++ b/test/core/test_epoll_callback_edge.c @@ -0,0 +1,62 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * EPOLLET on the callback path: an edge-triggered fd delivers once per edge. It + * must NOT re-fire while it stays continuously readable (the byte is never + * drained), and it fires again only on a fresh edge (a new write). + */ + +#include +#include +#include +#include +#include +#include + +static int ep, rfd, wfd, fires; + +static void second_edge(void* arg) { + // The fd stayed readable the whole time (fire 1 did not drain it), yet the + // edge-triggered callback did not re-fire. A LEVEL fd would have re-delivered + // (and spun) by now, so fires==1 here is the EPOLLET once-per-edge guarantee. + assert(fires == 1); + assert(write(wfd, "y", 1) == 1); // a fresh edge -> exactly one more delivery +} + +static void on_ready(int e, struct epoll_event* ev, int n, void* ud) { + assert(n == 1); + assert(ev[0].data.fd == rfd); + assert(ev[0].events & EPOLLIN); + fires++; + + if (fires == 1) { + // Do NOT drain: leave the fd readable, then check it stays silent and poke a + // fresh edge. + emscripten_async_call(second_edge, NULL, 0); + return; + } + + assert(fires == 2); + char b[2]; + assert(read(rfd, b, 2) == 2); // drain both bytes + assert(emscripten_epoll_set_callback(ep, 4, NULL, NULL) == 0); + printf("done\n"); +} + +int main(void) { + ep = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + rfd = p[0]; + wfd = p[1]; + struct epoll_event ev = { .events = EPOLLIN | EPOLLET }; + ev.data.fd = rfd; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, rfd, &ev) == 0); + + assert(emscripten_epoll_set_callback(ep, 4, on_ready, 0) == 0); + assert(write(wfd, "x", 1) == 1); // first edge + return 0; +} diff --git a/test/core/test_epoll_callback_level.c b/test/core/test_epoll_callback_level.c new file mode 100644 index 0000000000000..20e5c257ecd49 --- /dev/null +++ b/test/core/test_epoll_callback_level.c @@ -0,0 +1,42 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * Pins the documented level-triggered callback behaviour: an fd that is + * structurally always ready (here a pipe write end, always EPOLLOUT) re-fires + * the callback on every event-loop tick. The runtime drives that loop, so such + * an fd would spin indefinitely - the contract is that the app uses EPOLLET or + * unregisters. This test unregisters after a few deliveries so it terminates. + */ + +#include +#include +#include +#include +#include +#include + +static int ep, fires; + +static void on_ready(int e, struct epoll_event* ev, int n, void* ud) { + assert(n == 1); + assert(ev[0].events & EPOLLOUT); + if (++fires == 3) { // re-fired every tick despite no new event and no drain + assert(emscripten_epoll_set_callback(ep, 4, NULL, NULL) == 0); + printf("done\n"); + } +} + +int main(void) { + ep = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + struct epoll_event ev = { .events = EPOLLOUT }; // level; a write end is always writable + ev.data.fd = p[1]; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, p[1], &ev) == 0); + + assert(emscripten_epoll_set_callback(ep, 4, on_ready, 0) == 0); + return 0; +} diff --git a/test/core/test_epoll_callback_nested.c b/test/core/test_epoll_callback_nested.c new file mode 100644 index 0000000000000..f56d3b2ee4018 --- /dev/null +++ b/test/core/test_epoll_callback_nested.c @@ -0,0 +1,54 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * A readiness callback on an outer epoll that nests an inner one. A single leaf + * edge must propagate two levels - leaf -> inner epoll's wait-queue -> outer + * epoll's registration -> outer epoll's wait-queue -> the callback - and surface + * as readiness on the inner epoll's fd, with no blocking and no ASYNCIFY/JSPI. + */ + +#include +#include +#include +#include +#include +#include + +static int epA, epB, rfd, wfd; + +static void writer(void* arg) { assert(write(wfd, "x", 1) == 1); } + +static void on_ready(int epfd, struct epoll_event* ev, int n, void* ud) { + assert(epfd == epA); + assert(n == 1); + assert(ev[0].data.fd == epB); // the inner epoll, surfaced through nesting + assert(ev[0].events & EPOLLIN); + char b[1]; + assert(read(rfd, b, 1) == 1); // drain the leaf + assert(emscripten_epoll_set_callback(epA, 4, NULL, NULL) == 0); + printf("done\n"); +} + +int main(void) { + epA = epoll_create1(0); + epB = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + rfd = p[0]; + wfd = p[1]; + + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.fd = rfd; + assert(epoll_ctl(epB, EPOLL_CTL_ADD, rfd, &ev) == 0); // leaf in the inner epoll + ev.data.fd = epB; + assert(epoll_ctl(epA, EPOLL_CTL_ADD, epB, &ev) == 0); // inner epoll in the outer + + // Arm the callback on the outer epoll, then write after we return: the leaf + // edge wakes the callback through both levels with no stack switch. + assert(emscripten_epoll_set_callback(epA, 4, on_ready, 0) == 0); + emscripten_async_call(writer, NULL, 0); + return 0; +} diff --git a/test/core/test_epoll_callback_nested_close.c b/test/core/test_epoll_callback_nested_close.c new file mode 100644 index 0000000000000..c552713970e07 --- /dev/null +++ b/test/core/test_epoll_callback_nested_close.c @@ -0,0 +1,47 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * Closing a nested (inner) epoll wakes the outer epoll watching it, which + * re-derives and drops the now-stale registration. An outer callback that + * watched only the inner then has nothing that can fire, so it stops keeping the + * runtime alive and the process exits - with no explicit unregister, the same + * terminal-set property as closing a leaf fd, one level up. + */ + +#include +#include +#include +#include +#include +#include + +static int epA, epB, rfd, wfd; + +static void on_ready(int epfd, struct epoll_event* ev, int n, void* ud) { + assert(epfd == epA); + assert(n == 1 && ev[0].data.fd == epB); + printf("done\n"); + close(epB); // inner epoll gone -> outer's only registration becomes terminal +} + +int main(void) { + epA = epoll_create1(0); + epB = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + rfd = p[0]; + wfd = p[1]; + + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.fd = rfd; + assert(epoll_ctl(epB, EPOLL_CTL_ADD, rfd, &ev) == 0); // leaf in the inner + ev.data.fd = epB; + assert(epoll_ctl(epA, EPOLL_CTL_ADD, epB, &ev) == 0); // inner in the outer + + assert(emscripten_epoll_set_callback(epA, 4, on_ready, 0) == 0); + assert(write(wfd, "x", 1) == 1); // leaf ready -> propagates up to epA's callback + return 0; +} diff --git a/test/core/test_epoll_callback_overflow.c b/test/core/test_epoll_callback_overflow.c new file mode 100644 index 0000000000000..9906db6d88a01 --- /dev/null +++ b/test/core/test_epoll_callback_overflow.c @@ -0,0 +1,61 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * emscripten_epoll_set_callback overflow drain: with more ready fds than + * maxevents, the callback re-triggers itself on the next tick to deliver the + * remainder - there is no app loop to re-call it. Three always-readable fds with + * maxevents=1 are all delivered (each exactly once, round-robin) from a single + * arm and a single set of writes, with no further producer events. + */ + +#include +#include +#include +#include +#include +#include + +static int ep; +static int rfd[3]; +static int fires; +static int seen[3]; + +static int index_of(int fd) { + for (int i = 0; i < 3; i++) if (rfd[i] == fd) return i; + return -1; +} + +static void on_ready(int epfd, struct epoll_event* ev, int n, void* ud) { + assert(n == 1); // maxevents == 1 + int i = index_of(ev[0].data.fd); + assert(i >= 0 && !seen[i]); // each fd delivered exactly once (no starvation) + seen[i] = 1; + char b[1]; + assert(read(rfd[i], b, 1) == 1); // drain so it is no longer ready + + if (++fires == 3) { + assert(emscripten_epoll_set_callback(ep, 1, NULL, NULL) == 0); + printf("done\n"); + } +} + +int main(void) { + ep = epoll_create1(0); + for (int i = 0; i < 3; i++) { + int p[2]; + assert(pipe(p) == 0); + rfd[i] = p[0]; + assert(write(p[1], "x", 1) == 1); // read end readable (level) + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.fd = rfd[i]; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, rfd[i], &ev) == 0); + } + + // One arm, maxevents=1, three ready fds: the callback must deliver all three + // (one per tick via re-trigger), not just the first. + assert(emscripten_epoll_set_callback(ep, 1, on_ready, 0) == 0); + return 0; +} diff --git a/test/core/test_epoll_callback_replace.c b/test/core/test_epoll_callback_replace.c new file mode 100644 index 0000000000000..c60a7cdd0ad91 --- /dev/null +++ b/test/core/test_epoll_callback_replace.c @@ -0,0 +1,56 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * emscripten_epoll_set_callback registration semantics: there is at most one + * callback per epoll, so a second register replaces the first (callbacks do not + * stack), and a NULL callback unregisters regardless of maxevents (including 0). + */ + +#include +#include +#include +#include +#include +#include +#include + +static int ep, rfd, wfd; +static int c1, c2; + +static void cb1(int e, struct epoll_event* ev, int n, void* ud) { c1++; } + +static void cb2(int e, struct epoll_event* ev, int n, void* ud) { + c2++; + assert(c1 == 0); // the replaced callback must never have fired + char b[1]; + assert(read(rfd, b, 1) == 1); // drain + + // Unregister with maxevents 0: it is ignored when clearing. Make the set ready + // again to prove no further delivery happens. + assert(emscripten_epoll_set_callback(ep, 0, 0, 0) == 0); + assert(write(wfd, "x", 1) == 1); + printf("done\n"); +} + +int main(void) { + ep = epoll_create1(0); + int p[2]; + assert(pipe(p) == 0); + rfd = p[0]; + wfd = p[1]; + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.fd = rfd; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, rfd, &ev) == 0); + + // A non-epoll fd is rejected with -EBADF. + assert(emscripten_epoll_set_callback(rfd, 4, cb1, 0) == -EBADF); + + // Register then immediately replace, before any tick runs: only cb2 is armed. + assert(emscripten_epoll_set_callback(ep, 4, cb1, 0) == 0); + assert(emscripten_epoll_set_callback(ep, 4, cb2, 0) == 0); + assert(write(wfd, "x", 1) == 1); // delivered on the next tick, to cb2 only + return 0; +} diff --git a/test/core/test_epoll_fairness.c b/test/core/test_epoll_fairness.c new file mode 100644 index 0000000000000..d212c586f4616 --- /dev/null +++ b/test/core/test_epoll_fairness.c @@ -0,0 +1,41 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * Round-robin fairness: with more ready fds than maxevents, successive waits + * must rotate. A delivered level-triggered fd goes to the back of the ready + * list, and the unprocessed remainder is serviced first on the next call, so no + * fd starves. With three always-readable fds and maxevents=1, the reported fd + * cycles a, b, c, a, b, c. + */ + +#include +#include +#include +#include + +int main(void) { + int ep = epoll_create1(0); + int rfd[3]; + for (int i = 0; i < 3; i++) { + int p[2]; + assert(pipe(p) == 0); + rfd[i] = p[0]; + assert(write(p[1], "x", 1) == 1); // read end is now readable (level), never drained + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.fd = rfd[i]; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, rfd[i], &ev) == 0); + } + + int expect[6] = { rfd[0], rfd[1], rfd[2], rfd[0], rfd[1], rfd[2] }; + struct epoll_event out; + for (int i = 0; i < 6; i++) { + assert(epoll_wait(ep, &out, 1, 0) == 1); + assert(out.data.fd == expect[i]); + } + + printf("done\n"); + return 0; +} diff --git a/test/core/test_epoll_noderawfs.c b/test/core/test_epoll_noderawfs.c new file mode 100644 index 0000000000000..5c87385eccce5 --- /dev/null +++ b/test/core/test_epoll_noderawfs.c @@ -0,0 +1,59 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * poll()/epoll under -sNODERAWFS, where regular-file streams are backed by + * node's fs and carry no stream_ops. The readiness layer must treat such a + * stream as a plain always-ready file (not dereference a missing poll handler): + * poll reports POLLIN|POLLOUT, epoll_ctl rejects it with EPERM, and a PIPEFS + * pipe (still a real stream_ops-bearing stream under NODERAWFS) works normally. + */ + +#include +#include +#include +#include +#include +#include +#include + +int main(void) { + int rf = open("epoll_noderawfs.tmp", O_CREAT | O_RDWR, 0600); + assert(rf >= 0); + + // A regular file with no poll handler is always readable+writable, and does + // not crash the derivation. + struct pollfd pf = { .fd = rf, .events = POLLIN | POLLOUT }; + assert(poll(&pf, 1, 0) == 1); + assert(pf.revents == (POLLIN | POLLOUT)); + + // ...and is not epoll-capable. + int ep = epoll_create1(0); + assert(ep >= 0); + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.fd = rf; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, rf, &ev) == -1 && errno == EPERM); + + // A pipe still comes from PIPEFS under NODERAWFS, so epoll works on it. + int p[2]; + assert(pipe(p) == 0); + ev.events = EPOLLIN; + ev.data.fd = p[0]; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &ev) == 0); + struct epoll_event out[4]; + assert(epoll_wait(ep, out, 4, 0) == 0); + assert(write(p[1], "x", 1) == 1); + assert(epoll_wait(ep, out, 4, 0) == 1); + assert(out[0].events & EPOLLIN); + assert(out[0].data.fd == p[0]); + + close(ep); + close(p[0]); + close(p[1]); + close(rf); + unlink("epoll_noderawfs.tmp"); + printf("EPOLL NODERAWFS PASS\n"); + return 0; +} diff --git a/test/core/test_epoll_wait_and_callback.c b/test/core/test_epoll_wait_and_callback.c new file mode 100644 index 0000000000000..9b7a6c6ab2a89 --- /dev/null +++ b/test/core/test_epoll_wait_and_callback.c @@ -0,0 +1,100 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * A blocking epoll_wait() (suspended under ASYNCIFY/JSPI) and a persistent + * emscripten_epoll_set_callback on the SAME epoll. Both are consumers on the + * epoll's wait-queue, so a readiness edge wakes both - but they share ONE ready + * list, which is consumed rather than copied. So they take DISJOINT slices: no + * edge is ever delivered twice, and together they cover the whole ready set. + * This mirrors Linux, where multiple waiters on one epoll pull different items + * off the shared rdllist (the basis of the multi-waiter work-distribution + * pattern), and an edge-triggered event is reported to exactly one of them. + * + * The split is deterministic: the blocking wait's waiter runs synchronously in + * the producer's stack and drains the ready list immediately, so it wins the one + * edge ready at the instant it is woken; whatever became ready afterwards is left + * on the shared list for the callback's deferred (microtask) tick. What is NOT + * guaranteed is the relative order of the two completions - the callback's tick + * may run before or after the blocking wait's async resumption - so "done" is + * reported once both slices have arrived, whichever lands last. + */ + +#include +#include +#include +#include +#include +#include + +static int ep, rfd[3], wfd[3]; +static int seen[3]; // which fds have been delivered, across BOTH consumers +static int done_printed; // guard: report "done" exactly once + +static int idx(int fd) { + for (int i = 0; i < 3; i++) if (rfd[i] == fd) return i; + return -1; +} + +// Both consumers feed into this; whichever completes the set last prints "done". +// Their completions can interleave in either order, so neither alone can decide. +static void maybe_done(void) { + if (seen[0] && seen[1] && seen[2] && !done_printed) { + done_printed = 1; + assert(emscripten_epoll_set_callback(ep, 8, NULL, NULL) == 0); + printf("done\n"); + } +} + +static void make_ready(void* arg) { + // Runs after epoll_wait has suspended. The first write wakes the blocking + // wait, which drains synchronously and resolves with just the one fd ready at + // that instant; the next two edges land on the shared ready list, with no + // blocking waiter left to take them, for the callback's tick. + for (int i = 0; i < 3; i++) assert(write(wfd[i], "x", 1) == 1); +} + +static void on_ready(int epfd, struct epoll_event* ev, int n, void* ud) { + for (int k = 0; k < n; k++) { + int i = idx(ev[k].data.fd); + assert(i >= 0 && !seen[i]); // disjoint: never an fd the blocking wait took + seen[i] = 1; + } + maybe_done(); +} + +int main(void) { + ep = epoll_create1(0); + for (int i = 0; i < 3; i++) { + int p[2]; + assert(pipe(p) == 0); + rfd[i] = p[0]; + wfd[i] = p[1]; + // Edge-triggered: each readiness is reported once, so "delivered to exactly + // one consumer" is unambiguous (no level re-cycling between the two). + struct epoll_event ev = { .events = EPOLLIN | EPOLLET }; + ev.data.fd = rfd[i]; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, rfd[i], &ev) == 0); + } + + // Arm the callback and schedule the writes, then block. Both consumers are now + // on the epoll's wait-queue with an empty ready list. + assert(emscripten_epoll_set_callback(ep, 8, on_ready, 0) == 0); + emscripten_async_call(make_ready, NULL, 0); + + struct epoll_event out[8]; + int n = epoll_wait(ep, out, 8, -1); // ASYNCIFY/JSPI: suspends until readiness + // Woken on the first edge, the blocking wait sees only what was ready then - + // exactly one fd, not the whole burst that arrived after it drained. + assert(n == 1); + int wi = idx(out[0].data.fd); + assert(wi >= 0 && !seen[wi]); + seen[wi] = 1; + + // The callback (kept alive by its own keepalive) delivers the remaining two + // off the shared list; "done" prints once both slices are in, in either order. + maybe_done(); + return 0; +} diff --git a/test/sockets/test_epoll_callback.c b/test/sockets/test_epoll_callback.c new file mode 100644 index 0000000000000..0b5e30b3cc9b8 --- /dev/null +++ b/test/sockets/test_epoll_callback.c @@ -0,0 +1,75 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * emscripten_epoll_set_callback woken by real socket readiness (arriving UDP + * datagrams) through the SOCKFS -> wait-queue bridge, with no blocking call and + * no ASYNCIFY/JSPI. A single arm delivers repeatedly: each datagram is a + * separate producer event that re-fires the persistent callback. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int ep, rx, tx; +static struct sockaddr_in addr; +static int fires; + +static void send_one(const char* msg) { + assert(sendto(tx, msg, 4, 0, (struct sockaddr*)&addr, sizeof addr) == 4); +} + +static void on_ready(int epfd, struct epoll_event* ev, int n, void* ud) { + assert(n == 1); + assert(ev[0].events & EPOLLIN); + assert(ev[0].data.fd == rx); + char b[4]; + assert(recv(rx, b, 4, 0) == 4); + fires++; + + if (fires == 1) { + assert(memcmp(b, "one\0", 4) == 0); + send_one("two"); // a second producer event re-fires the same arm + return; + } + assert(fires == 2); + assert(memcmp(b, "two\0", 4) == 0); + printf("done\n"); + // Closing the watched fd makes the epoll terminal - nothing it watches can + // become ready again - so the callback stops keeping the runtime alive and the + // process exits (no explicit unregister needed). + close(rx); + close(tx); +} + +int main(void) { + ep = epoll_create1(0); + rx = socket(AF_INET, SOCK_DGRAM, 0); + tx = socket(AF_INET, SOCK_DGRAM, 0); + memset(&addr, 0, sizeof addr); + addr.sin_family = AF_INET; addr.sin_port = htons(0); + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + assert(bind(rx, (struct sockaddr*)&addr, sizeof addr) == 0); + socklen_t l = sizeof addr; + assert(getsockname(rx, (struct sockaddr*)&addr, &l) == 0); + + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.fd = rx; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, rx, &ev) == 0); + + // Arm once (no ASYNCIFY), then send the first datagram; it arrives after we + // return and wakes the callback. The callback drives the second send itself. + assert(emscripten_epoll_set_callback(ep, 4, on_ready, 0) == 0); + send_one("one"); + return 0; +} diff --git a/test/sockets/test_epoll_rdhup.c b/test/sockets/test_epoll_rdhup.c new file mode 100644 index 0000000000000..64fb381ff93f4 --- /dev/null +++ b/test/sockets/test_epoll_rdhup.c @@ -0,0 +1,79 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * EPOLLRDHUP over a real TCP connection: a self-contained loopback client/server + * (blocking, proxied to a worker) establishes a connection, the server + * half-closes its write side (shutdown(SHUT_WR) -> FIN), and a blocking + * epoll_wait on the client reports EPOLLRDHUP - the peer read-side hangup, + * distinct from a full EPOLLHUP. Also checks that EPOLLRDHUP is request-gated: + * a registration that didn't ask for it does not receive it. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(void) { + int listen_fd = socket(AF_INET, SOCK_STREAM, 0); + assert(listen_fd >= 0); + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + assert(bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0); + assert(listen(listen_fd, 4) == 0); + socklen_t l = sizeof(addr); + assert(getsockname(listen_fd, (struct sockaddr*)&addr, &l) == 0); + + int client_fd = socket(AF_INET, SOCK_STREAM, 0); + assert(client_fd >= 0); + assert(connect(client_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0); + + // The server-side 'connection' can land just after connect() returns, so wait + // for the listener to be readable before accepting. + struct pollfd lp = { .fd = listen_fd, .events = POLLIN }; + assert(poll(&lp, 1, -1) == 1 && (lp.revents & POLLIN)); + int peer_fd = accept(listen_fd, NULL, NULL); + assert(peer_fd >= 0); + + // Server half-closes its write side: the client's read side hangs up (FIN). + assert(shutdown(peer_fd, SHUT_WR) == 0); + + int ep = epoll_create1(0); + struct epoll_event ev = { .events = EPOLLIN | EPOLLRDHUP }; + ev.data.fd = client_fd; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, client_fd, &ev) == 0); + + struct epoll_event out[4]; + int n = epoll_wait(ep, out, 4, -1); // blocks until the FIN arrives + assert(n == 1); + assert(out[0].data.fd == client_fd); + assert(out[0].events & EPOLLRDHUP); // peer read-side hangup reported + assert(!(out[0].events & EPOLLHUP)); // not a full hangup: still half-open + + // EPOLLRDHUP is request-gated: a registration that didn't ask for it doesn't + // get it, even though the read side is hung up (it still reports EPOLLIN). + int ep2 = epoll_create1(0); + ev.events = EPOLLIN; + assert(epoll_ctl(ep2, EPOLL_CTL_ADD, client_fd, &ev) == 0); + assert(epoll_wait(ep2, out, 4, 0) == 1); + assert(out[0].events & EPOLLIN); + assert(!(out[0].events & EPOLLRDHUP)); + + close(ep); + close(ep2); + close(client_fd); + close(peer_fd); + close(listen_fd); + printf("EPOLL RDHUP PASS\n"); + return 0; +} diff --git a/test/sockets/test_epoll_socket_blocking.c b/test/sockets/test_epoll_socket_blocking.c new file mode 100644 index 0000000000000..e9a9e294a8f8a --- /dev/null +++ b/test/sockets/test_epoll_socket_blocking.c @@ -0,0 +1,84 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * A *blocking* epoll_wait() on a real socket is woken by a datagram that arrives + * *after* the wait has already blocked. The datagram is sent on a delay (from + * another thread under -pthread, or a timer under JSPI) so epoll_wait() must + * suspend - the proxied worker under PROXY_TO_PTHREAD, or the calling stack + * under JSPI - and be woken through the unified readiness wait-queue, the same + * SOCKFS.emit bridge poll() rides. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN_PTHREADS__ +#include +#endif + +static int rx = -1, tx = -1; +static struct sockaddr_in addr; + +static void send_ping(void* arg) { + assert(sendto(tx, "ping", 4, 0, (struct sockaddr*)&addr, sizeof(addr)) == 4); +} + +#ifdef __EMSCRIPTEN_PTHREADS__ +static void* sender(void* arg) { + usleep(100000); // let epoll_wait() block first + send_ping(NULL); + return NULL; +} +#endif + +int main(void) { + rx = socket(AF_INET, SOCK_DGRAM, 0); + tx = socket(AF_INET, SOCK_DGRAM, 0); + assert(rx >= 0 && tx >= 0); + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + assert(bind(rx, (struct sockaddr*)&addr, sizeof(addr)) == 0); + socklen_t l = sizeof(addr); + assert(getsockname(rx, (struct sockaddr*)&addr, &l) == 0); + + int ep = epoll_create1(0); + assert(ep >= 0); + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.fd = rx; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, rx, &ev) == 0); + + // Arrange the datagram to arrive only after epoll_wait() is already blocking, + // so it can only complete by being woken - not by the initial derivation. +#ifdef __EMSCRIPTEN_PTHREADS__ + pthread_t t; + assert(pthread_create(&t, NULL, sender, NULL) == 0); +#else + emscripten_async_call(send_ping, NULL, 100); +#endif + + struct epoll_event out[4]; + int n = epoll_wait(ep, out, 4, -1); // blocks; only the arrival can wake it + assert(n == 1 && (out[0].events & EPOLLIN)); + assert(out[0].data.fd == rx); + + char buf[4]; + assert(recv(rx, buf, sizeof(buf), 0) == 4 && memcmp(buf, "ping", 4) == 0); + + close(ep); + close(rx); + close(tx); + printf("EPOLL SOCKET BLOCKING PASS\n"); + return 0; +} diff --git a/test/test_core.py b/test/test_core.py index c3199bdcf0f16..0f34c84ab1753 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -399,6 +399,19 @@ def decorated(self, *args, **kwargs): return decorator +def needs_epoll(func): + # epoll is implemented in the JS (non-WASMFS) syscall layer and needs the FS. + assert callable(func) + + @wraps(func) + def decorated(self, *args, **kwargs): + if self.get_setting('WASMFS'): + self.skipTest('epoll is implemented in the JS (non-WASMFS) syscall layer') + self.set_setting('FORCE_FILESYSTEM') + return func(self, *args, **kwargs) + return decorated + + def make_no_decorator_for_setting(name): def outer_decorator(note): assert not callable(note) @@ -5755,6 +5768,75 @@ def test_poll(self): self.set_setting('FORCE_FILESYSTEM') self.do_core_test('test_poll.c') + @needs_epoll + def test_epoll(self): + self.do_runf('core/test_epoll.c', 'EPOLL PASS') + + @needs_epoll + def test_epoll_advanced(self): + self.do_runf('core/test_epoll_advanced.c', 'EPOLL ADVANCED PASS') + + @needs_epoll + def test_epoll_fairness(self): + # More ready fds than maxevents: successive waits rotate (round-robin) so no + # fd starves. + self.do_runf('core/test_epoll_fairness.c', 'done\n') + + @needs_epoll + @requires_node + def test_epoll_noderawfs(self): + # Regular-file streams under NODERAWFS carry no stream_ops; the readiness + # layer must not dereference a missing poll handler (poll/epoll on a file). + self.do_runf('core/test_epoll_noderawfs.c', 'EPOLL NODERAWFS PASS', cflags=['-sNODERAWFS']) + + @needs_epoll + def test_epoll_callback(self): + # emscripten_epoll_set_callback delivers an epoll set's readiness by a + # persistent callback with no blocking and no ASYNCIFY/JSPI. + self.do_runf('core/test_epoll_callback.c', 'done\n', cflags=['-sEXIT_RUNTIME']) + + @needs_epoll + def test_epoll_callback_overflow(self): + # maxevents < ready count: the callback re-triggers to drain the remainder + # across ticks (no app loop to re-call it). + self.do_runf('core/test_epoll_callback_overflow.c', 'done\n', cflags=['-sEXIT_RUNTIME']) + + @needs_epoll + def test_epoll_callback_replace(self): + # A second register replaces the callback (no stacking); a NULL callback + # unregisters regardless of maxevents. + self.do_runf('core/test_epoll_callback_replace.c', 'done\n', cflags=['-sEXIT_RUNTIME']) + + @needs_epoll + def test_epoll_callback_close(self): + # Closing the last watched fd makes the epoll terminal, so the callback stops + # keeping the runtime alive and the process exits (no explicit unregister). + self.do_runf('core/test_epoll_callback_close.c', 'done\n', cflags=['-sEXIT_RUNTIME']) + + @needs_epoll + def test_epoll_callback_nested(self): + # A callback on an outer epoll fires when a leaf edge propagates through an + # inner (nested) epoll. + self.do_runf('core/test_epoll_callback_nested.c', 'done\n', cflags=['-sEXIT_RUNTIME']) + + @needs_epoll + def test_epoll_callback_nested_close(self): + # Closing the inner epoll wakes the outer to drop its stale registration, so + # an outer callback watching only the inner stops holding the runtime. + self.do_runf('core/test_epoll_callback_nested_close.c', 'done\n', cflags=['-sEXIT_RUNTIME']) + + @needs_epoll + def test_epoll_callback_edge(self): + # EPOLLET on the callback path: fires once per edge, stays silent while + # continuously readable, re-fires only on a fresh edge. + self.do_runf('core/test_epoll_callback_edge.c', 'done\n', cflags=['-sEXIT_RUNTIME']) + + @needs_epoll + def test_epoll_callback_level(self): + # A structurally-always-ready level fd (EPOLLOUT on a writable end) re-fires + # the callback every tick: documents the spin contract (use EPOLLET/unregister). + self.do_runf('core/test_epoll_callback_level.c', 'done\n', cflags=['-sEXIT_RUNTIME']) + @no_wasmfs('st.f_ffree > st.f_files, same issue than in wasmfs.test_fs_nodefs_statvfs. https://github.com/emscripten-core/emscripten/issues/25035') def test_statvfs(self): self.do_core_test('test_statvfs.c') @@ -9695,6 +9777,22 @@ def test_poll_blocking_asyncify(self): self.skipTest('test requires setTimeout which is not supported under v8') self.do_runf('core/test_poll_blocking_asyncify.c', 'done\n') + @with_asyncify_and_jspi + @needs_epoll + def test_epoll_blocking_asyncify(self): + if self.get_setting('JSPI') and engine_is_v8(self.get_current_js_engine()): + self.skipTest('test requires setTimeout which is not supported under v8') + self.do_runf('core/test_epoll_blocking_asyncify.c', 'done\n') + + @with_asyncify_and_jspi + @needs_epoll + def test_epoll_wait_and_callback(self): + # A suspended blocking epoll_wait and a persistent callback on one epoll + # share a single ready list: they take disjoint slices, never the same edge. + if self.get_setting('JSPI') and engine_is_v8(self.get_current_js_engine()): + self.skipTest('test requires setTimeout which is not supported under v8') + self.do_runf('core/test_epoll_wait_and_callback.c', 'done\n', cflags=['-sEXIT_RUNTIME']) + @parameterized({ '': ([],), 'pthread': (['-pthread'],), diff --git a/test/test_sockets.py b/test/test_sockets.py index 53f5bf9d90c29..aae93b9bc3b8c 100644 --- a/test/test_sockets.py +++ b/test/test_sockets.py @@ -475,12 +475,62 @@ def test_noderawsockets_udp_ipv6(self): self.skipTest('no IPv6 loopback available') self.do_runf('sockets/test_udp_ipv6.c', 'UDP IPV6 PASS', cflags=['-sNODERAWSOCKETS']) + def test_noderawsockets_epoll_socket_blocking(self): + # A blocking epoll_wait() on a socket is woken by an incoming datagram + # through the unified readiness wait-queue (the SOCKFS.emit bridge), with + # main() proxied to a worker so the wait can suspend. + self.do_runf('sockets/test_epoll_socket_blocking.c', 'EPOLL SOCKET BLOCKING PASS', + cflags=['-sNODERAWSOCKETS', '-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME']) + + def test_noderawsockets_epoll_socket_blocking_jspi(self): + # Same, but the blocking epoll_wait() suspends the wasm stack under JSPI. + # NODERAWSOCKETS runs under node rather than the browser, so gate JSPI on + # node's own support (v24) instead of require_jspi's browser-test path. + if 'EMTEST_SKIP_JSPI' in os.environ: + self.skipTest('skipping JSPI (EMTEST_SKIP_JSPI is set)') + if not self.try_require_node_version(24): + self.skipTest('JSPI requires node v24') + if not common.check_node_version(26): + self.node_args += ['--experimental-wasm-stack-switching'] + self.cflags += ['-Wno-experimental'] + self.set_setting('JSPI') + self.do_runf('sockets/test_epoll_socket_blocking.c', 'EPOLL SOCKET BLOCKING PASS', + cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) + + def test_noderawsockets_epoll_rdhup(self): + # A blocking epoll_wait reports EPOLLRDHUP when the TCP peer half-closes its + # write side (FIN), distinct from a full EPOLLHUP, and only when requested. + self.do_runf('sockets/test_epoll_rdhup.c', 'EPOLL RDHUP PASS', + cflags=['-sNODERAWSOCKETS', '-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME']) + + def test_noderawsockets_epoll_rdhup_jspi(self): + # Same, but the blocking calls suspend the wasm stack under JSPI. Gate on + # node's own JSPI support (v24) since NODERAWSOCKETS runs under node. + if 'EMTEST_SKIP_JSPI' in os.environ: + self.skipTest('skipping JSPI (EMTEST_SKIP_JSPI is set)') + if not self.try_require_node_version(24): + self.skipTest('JSPI requires node v24') + if not common.check_node_version(26): + self.node_args += ['--experimental-wasm-stack-switching'] + self.cflags += ['-Wno-experimental'] + self.set_setting('JSPI') + self.do_runf('sockets/test_epoll_rdhup.c', 'EPOLL RDHUP PASS', + cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) + @also_with_proxy_to_pthread def test_noderawsockets_udp(self): # Self-contained loopback UDP echo: the server binds(:0)+getsockname for its # ephemeral port, the client sends a datagram, the server echoes it back. self.do_runf('sockets/test_udp_echo.c', 'UDP ECHO PASS', cflags=['-sNODERAWSOCKETS']) + def test_noderawsockets_epoll_callback(self): + # emscripten_epoll_set_callback woken repeatedly by arriving datagrams on a + # real socket via the SOCKFS -> wait-queue bridge, with no ASYNCIFY/JSPI. + # Not run under PROXY_TO_PTHREAD: the callback fires on the main-thread event + # loop, which is not where the proxied application thread runs (use a blocking + # epoll_wait from a pthread instead). + self.do_runf('sockets/test_epoll_callback.c', 'done', cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) + @also_with_proxy_to_pthread def test_noderawsockets_udp_connect(self): # Connected UDP: sendto() with an address gives EISCONN, send() reaches the diff --git a/tools/maint/gen_sig_info.py b/tools/maint/gen_sig_info.py index 52707167a61f3..15bc35b2a1e5b 100755 --- a/tools/maint/gen_sig_info.py +++ b/tools/maint/gen_sig_info.py @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -106,6 +107,7 @@ #include #include #include +#include #include // Internal emscripten headers diff --git a/tools/native_sigs.py b/tools/native_sigs.py index f9a7cde215c56..31a3489887145 100644 --- a/tools/native_sigs.py +++ b/tools/native_sigs.py @@ -421,6 +421,7 @@ '__syscall_connect': '__p____', '__syscall_epoll_ctl': '____p', '__syscall_epoll_pwait': '__p__pp', + '__syscall_epoll_pwait_nonblocking': '__p_', '__syscall_faccessat': '__p__', '__syscall_fchmodat2': '__p__', '__syscall_fchownat': '__p___', From 2a091c645570b98d835366613e9a1e100e42e516 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 1 Jul 2026 12:15:38 -0700 Subject: [PATCH 2/2] pr feedback --- .../test_codesize_hello_dylink_all.json | 4 +- test/sockets/test_epoll_rdhup.c | 2 +- test/sockets/test_epoll_socket_blocking.c | 5 +- test/sockets/test_tcp_backpressure.c | 2 +- test/sockets/test_tcp_client_bind.c | 2 +- test/sockets/test_tcp_client_semantics.c | 2 +- test/sockets/test_tcp_echo.c | 2 +- test/sockets/test_tcp_ipv6.c | 2 +- test/sockets/test_tcp_refused.c | 2 +- test/sockets/test_tcp_server.c | 2 +- test/sockets/test_udp_connect.c | 2 +- test/sockets/test_udp_echo.c | 2 +- test/sockets/test_udp_ipv6.c | 2 +- test/test_core.py | 54 ---------------- test/test_other.py | 48 ++++++++++++++ test/test_sockets.py | 62 +++++++++---------- 16 files changed, 93 insertions(+), 102 deletions(-) diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index c2ae6d3ac6ac2..ff5e1a998fc98 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 271378, + "a.out.js": 271358, "a.out.nodebug.wasm": 588038, - "total": 859416, + "total": 859396, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/sockets/test_epoll_rdhup.c b/test/sockets/test_epoll_rdhup.c index 64fb381ff93f4..7fcf78a70e84f 100644 --- a/test/sockets/test_epoll_rdhup.c +++ b/test/sockets/test_epoll_rdhup.c @@ -74,6 +74,6 @@ int main(void) { close(client_fd); close(peer_fd); close(listen_fd); - printf("EPOLL RDHUP PASS\n"); + printf("done\n"); return 0; } diff --git a/test/sockets/test_epoll_socket_blocking.c b/test/sockets/test_epoll_socket_blocking.c index e9a9e294a8f8a..f71f157f6ab72 100644 --- a/test/sockets/test_epoll_socket_blocking.c +++ b/test/sockets/test_epoll_socket_blocking.c @@ -62,6 +62,9 @@ int main(void) { // Arrange the datagram to arrive only after epoll_wait() is already blocking, // so it can only complete by being woken - not by the initial derivation. #ifdef __EMSCRIPTEN_PTHREADS__ + // Under PROXY_TO_PTHREAD main() runs on a worker that parks in epoll_wait, so + // its event loop can't fire an emscripten_async_call timer - the wake is a + // cross-thread memory notify. Send from a separate thread instead. pthread_t t; assert(pthread_create(&t, NULL, sender, NULL) == 0); #else @@ -79,6 +82,6 @@ int main(void) { close(ep); close(rx); close(tx); - printf("EPOLL SOCKET BLOCKING PASS\n"); + printf("done\n"); return 0; } diff --git a/test/sockets/test_tcp_backpressure.c b/test/sockets/test_tcp_backpressure.c index 993515526fad7..a10174ed0d6e6 100644 --- a/test/sockets/test_tcp_backpressure.c +++ b/test/sockets/test_tcp_backpressure.c @@ -36,7 +36,7 @@ long long sent_total = 0; const long long CAP = 512LL * 1024 * 1024; void test_success(void) { - printf("BACKPRESSURE PASS\n"); + printf("done\n"); if (fd >= 0) close(fd); #ifdef __EMSCRIPTEN__ emscripten_cancel_main_loop(); diff --git a/test/sockets/test_tcp_client_bind.c b/test/sockets/test_tcp_client_bind.c index 59e5b1d188668..c9fea5d3f4dec 100644 --- a/test/sockets/test_tcp_client_bind.c +++ b/test/sockets/test_tcp_client_bind.c @@ -36,7 +36,7 @@ bool connected = false; bool ping_sent = false; void test_success(void) { - printf("CLIENT BIND PASS\n"); + printf("done\n"); if (client_fd >= 0) close(client_fd); #ifdef __EMSCRIPTEN__ emscripten_cancel_main_loop(); diff --git a/test/sockets/test_tcp_client_semantics.c b/test/sockets/test_tcp_client_semantics.c index 62a6d0edb7b8e..9a46f858736b7 100644 --- a/test/sockets/test_tcp_client_semantics.c +++ b/test/sockets/test_tcp_client_semantics.c @@ -37,7 +37,7 @@ bool ping_sent = false; bool echoed = false; void test_success(void) { - printf("CLIENT SEMANTICS PASS\n"); + printf("done\n"); if (fd >= 0) close(fd); #ifdef __EMSCRIPTEN__ emscripten_cancel_main_loop(); diff --git a/test/sockets/test_tcp_echo.c b/test/sockets/test_tcp_echo.c index b5204a49b1287..a7c0457b0bd0c 100644 --- a/test/sockets/test_tcp_echo.c +++ b/test/sockets/test_tcp_echo.c @@ -36,7 +36,7 @@ bool connected = false; bool ping_sent = false; void test_success(void) { - printf("TCP ECHO PASS\n"); + printf("done\n"); if (client_fd >= 0) close(client_fd); #ifdef __EMSCRIPTEN__ // The socket is closed and the main loop cancelled, so node's event loop diff --git a/test/sockets/test_tcp_ipv6.c b/test/sockets/test_tcp_ipv6.c index 4e4a20330c14d..b9cdd80f34991 100644 --- a/test/sockets/test_tcp_ipv6.c +++ b/test/sockets/test_tcp_ipv6.c @@ -39,7 +39,7 @@ bool pong_sent = false; void set_nonblocking(int fd) { fcntl(fd, F_SETFL, O_NONBLOCK); } void test_success(void) { - printf("TCP IPV6 PASS\n"); + printf("done\n"); if (listen_fd >= 0) close(listen_fd); if (client_fd >= 0) close(client_fd); if (peer_fd >= 0) close(peer_fd); diff --git a/test/sockets/test_tcp_refused.c b/test/sockets/test_tcp_refused.c index 1fbcda3f33519..1f46f454c5792 100644 --- a/test/sockets/test_tcp_refused.c +++ b/test/sockets/test_tcp_refused.c @@ -28,7 +28,7 @@ int fd = -1; void test_success(void) { - printf("REFUSED PASS\n"); + printf("done\n"); if (fd >= 0) close(fd); #ifdef __EMSCRIPTEN__ emscripten_cancel_main_loop(); diff --git a/test/sockets/test_tcp_server.c b/test/sockets/test_tcp_server.c index 677cd8d2c925d..7b3921a7617c0 100644 --- a/test/sockets/test_tcp_server.c +++ b/test/sockets/test_tcp_server.c @@ -42,7 +42,7 @@ void set_nonblocking(int fd) { } void test_success(void) { - printf("TCP SERVER PASS\n"); + printf("done\n"); if (listen_fd >= 0) close(listen_fd); if (client_fd >= 0) close(client_fd); if (peer_fd >= 0) close(peer_fd); diff --git a/test/sockets/test_udp_connect.c b/test/sockets/test_udp_connect.c index 4af53a3cf1908..403bcbf1c18c2 100644 --- a/test/sockets/test_udp_connect.c +++ b/test/sockets/test_udp_connect.c @@ -40,7 +40,7 @@ void set_nonblocking(int fd) { } void test_success(void) { - printf("UDP CONNECT PASS\n"); + printf("done\n"); if (server_fd >= 0) close(server_fd); if (client_fd >= 0) close(client_fd); if (other_fd >= 0) close(other_fd); diff --git a/test/sockets/test_udp_echo.c b/test/sockets/test_udp_echo.c index c8f3085ee3a6f..bed4b286c17aa 100644 --- a/test/sockets/test_udp_echo.c +++ b/test/sockets/test_udp_echo.c @@ -40,7 +40,7 @@ void set_nonblocking(int fd) { } void test_success(void) { - printf("UDP ECHO PASS\n"); + printf("done\n"); if (server_fd >= 0) close(server_fd); if (client_fd >= 0) close(client_fd); #ifdef __EMSCRIPTEN__ diff --git a/test/sockets/test_udp_ipv6.c b/test/sockets/test_udp_ipv6.c index adf66df62e2e4..5fa48ee4a9d89 100644 --- a/test/sockets/test_udp_ipv6.c +++ b/test/sockets/test_udp_ipv6.c @@ -37,7 +37,7 @@ bool pong_sent = false; void set_nonblocking(int fd) { fcntl(fd, F_SETFL, O_NONBLOCK); } void test_success(void) { - printf("UDP IPV6 PASS\n"); + printf("done\n"); if (server_fd >= 0) close(server_fd); if (client_fd >= 0) close(client_fd); #ifdef __EMSCRIPTEN__ diff --git a/test/test_core.py b/test/test_core.py index 0f34c84ab1753..ee720347125d6 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -5776,12 +5776,6 @@ def test_epoll(self): def test_epoll_advanced(self): self.do_runf('core/test_epoll_advanced.c', 'EPOLL ADVANCED PASS') - @needs_epoll - def test_epoll_fairness(self): - # More ready fds than maxevents: successive waits rotate (round-robin) so no - # fd starves. - self.do_runf('core/test_epoll_fairness.c', 'done\n') - @needs_epoll @requires_node def test_epoll_noderawfs(self): @@ -5789,54 +5783,6 @@ def test_epoll_noderawfs(self): # layer must not dereference a missing poll handler (poll/epoll on a file). self.do_runf('core/test_epoll_noderawfs.c', 'EPOLL NODERAWFS PASS', cflags=['-sNODERAWFS']) - @needs_epoll - def test_epoll_callback(self): - # emscripten_epoll_set_callback delivers an epoll set's readiness by a - # persistent callback with no blocking and no ASYNCIFY/JSPI. - self.do_runf('core/test_epoll_callback.c', 'done\n', cflags=['-sEXIT_RUNTIME']) - - @needs_epoll - def test_epoll_callback_overflow(self): - # maxevents < ready count: the callback re-triggers to drain the remainder - # across ticks (no app loop to re-call it). - self.do_runf('core/test_epoll_callback_overflow.c', 'done\n', cflags=['-sEXIT_RUNTIME']) - - @needs_epoll - def test_epoll_callback_replace(self): - # A second register replaces the callback (no stacking); a NULL callback - # unregisters regardless of maxevents. - self.do_runf('core/test_epoll_callback_replace.c', 'done\n', cflags=['-sEXIT_RUNTIME']) - - @needs_epoll - def test_epoll_callback_close(self): - # Closing the last watched fd makes the epoll terminal, so the callback stops - # keeping the runtime alive and the process exits (no explicit unregister). - self.do_runf('core/test_epoll_callback_close.c', 'done\n', cflags=['-sEXIT_RUNTIME']) - - @needs_epoll - def test_epoll_callback_nested(self): - # A callback on an outer epoll fires when a leaf edge propagates through an - # inner (nested) epoll. - self.do_runf('core/test_epoll_callback_nested.c', 'done\n', cflags=['-sEXIT_RUNTIME']) - - @needs_epoll - def test_epoll_callback_nested_close(self): - # Closing the inner epoll wakes the outer to drop its stale registration, so - # an outer callback watching only the inner stops holding the runtime. - self.do_runf('core/test_epoll_callback_nested_close.c', 'done\n', cflags=['-sEXIT_RUNTIME']) - - @needs_epoll - def test_epoll_callback_edge(self): - # EPOLLET on the callback path: fires once per edge, stays silent while - # continuously readable, re-fires only on a fresh edge. - self.do_runf('core/test_epoll_callback_edge.c', 'done\n', cflags=['-sEXIT_RUNTIME']) - - @needs_epoll - def test_epoll_callback_level(self): - # A structurally-always-ready level fd (EPOLLOUT on a writable end) re-fires - # the callback every tick: documents the spin contract (use EPOLLET/unregister). - self.do_runf('core/test_epoll_callback_level.c', 'done\n', cflags=['-sEXIT_RUNTIME']) - @no_wasmfs('st.f_ffree > st.f_files, same issue than in wasmfs.test_fs_nodefs_statvfs. https://github.com/emscripten-core/emscripten/issues/25035') def test_statvfs(self): self.do_core_test('test_statvfs.c') diff --git a/test/test_other.py b/test/test_other.py index 85d3ac9eaf7f1..711bceb978f8d 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -13284,6 +13284,54 @@ def test_emscripten_main_loop_settimeout(self): def test_emscripten_main_loop_setimmediate(self): self.do_runf('test_emscripten_main_loop_setimmediate.c') + # epoll is implemented in the JS (non-WASMFS) syscall layer and needs the FS. + # These exercise the JS API/readiness logic, which does not vary by wasm + # config, so they live here rather than in the test_core.py config matrix. + def test_epoll_fairness(self): + # More ready fds than maxevents: successive waits rotate (round-robin) so no + # fd starves. + self.do_runf('core/test_epoll_fairness.c', 'done\n', cflags=['-sFORCE_FILESYSTEM']) + + def test_epoll_callback(self): + # emscripten_epoll_set_callback delivers an epoll set's readiness by a + # persistent callback with no blocking and no ASYNCIFY/JSPI. + self.do_runf('core/test_epoll_callback.c', 'done\n', cflags=['-sFORCE_FILESYSTEM', '-sEXIT_RUNTIME']) + + def test_epoll_callback_overflow(self): + # maxevents < ready count: the callback re-triggers to drain the remainder + # across ticks (no app loop to re-call it). + self.do_runf('core/test_epoll_callback_overflow.c', 'done\n', cflags=['-sFORCE_FILESYSTEM', '-sEXIT_RUNTIME']) + + def test_epoll_callback_replace(self): + # A second register replaces the callback (no stacking); a NULL callback + # unregisters regardless of maxevents. + self.do_runf('core/test_epoll_callback_replace.c', 'done\n', cflags=['-sFORCE_FILESYSTEM', '-sEXIT_RUNTIME']) + + def test_epoll_callback_close(self): + # Closing the last watched fd makes the epoll terminal, so the callback stops + # keeping the runtime alive and the process exits (no explicit unregister). + self.do_runf('core/test_epoll_callback_close.c', 'done\n', cflags=['-sFORCE_FILESYSTEM', '-sEXIT_RUNTIME']) + + def test_epoll_callback_nested(self): + # A callback on an outer epoll fires when a leaf edge propagates through an + # inner (nested) epoll. + self.do_runf('core/test_epoll_callback_nested.c', 'done\n', cflags=['-sFORCE_FILESYSTEM', '-sEXIT_RUNTIME']) + + def test_epoll_callback_nested_close(self): + # Closing the inner epoll wakes the outer to drop its stale registration, so + # an outer callback watching only the inner stops holding the runtime. + self.do_runf('core/test_epoll_callback_nested_close.c', 'done\n', cflags=['-sFORCE_FILESYSTEM', '-sEXIT_RUNTIME']) + + def test_epoll_callback_edge(self): + # EPOLLET on the callback path: fires once per edge, stays silent while + # continuously readable, re-fires only on a fresh edge. + self.do_runf('core/test_epoll_callback_edge.c', 'done\n', cflags=['-sFORCE_FILESYSTEM', '-sEXIT_RUNTIME']) + + def test_epoll_callback_level(self): + # A structurally-always-ready level fd (EPOLLOUT on a writable end) re-fires + # the callback every tick: documents the spin contract (use EPOLLET/unregister). + self.do_runf('core/test_epoll_callback_level.c', 'done\n', cflags=['-sFORCE_FILESYSTEM', '-sEXIT_RUNTIME']) + @requires_pthreads @no_bun('https://github.com/emscripten-core/emscripten/issues/26197') def test_pthread_trap(self): diff --git a/test/test_sockets.py b/test/test_sockets.py index aae93b9bc3b8c..33e44bb847979 100644 --- a/test/test_sockets.py +++ b/test/test_sockets.py @@ -394,7 +394,7 @@ def _run_against_echo_server(self, src, expected): def test_noderawsockets_echo(self): # With -sNODERAWSOCKETS the client does a non-blocking connect, send and # recv over a real OS socket against a loopback echo server we run here. - self._run_against_echo_server('sockets/test_tcp_echo.c', 'TCP ECHO PASS') + self._run_against_echo_server('sockets/test_tcp_echo.c', 'done\n') def test_noderawsockets_client_bind(self): # A client that bind()s an explicit source port has it honored by connect(), @@ -411,7 +411,7 @@ def test_noderawsockets_client_bind(self): thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() try: - self.do_runf('sockets/test_tcp_client_bind.c', 'CLIENT BIND PASS', + self.do_runf('sockets/test_tcp_client_bind.c', 'done\n', cflags=['-sNODERAWSOCKETS'], args=[str(port), str(src_port)]) finally: server.shutdown() @@ -421,11 +421,11 @@ def test_noderawsockets_client_bind(self): def test_noderawsockets_client_semantics(self): # EISCONN on a second connect, shutdown(SHUT_WR) leaving reads working, and # EPIPE on a write after that. - self._run_against_echo_server('sockets/test_tcp_client_semantics.c', 'CLIENT SEMANTICS PASS') + self._run_against_echo_server('sockets/test_tcp_client_semantics.c', 'done\n') def test_noderawsockets_refused(self): # A connect to a loopback port with nothing listening reports ECONNREFUSED. - self.do_runf('sockets/test_tcp_refused.c', 'REFUSED PASS', cflags=['-sNODERAWSOCKETS']) + self.do_runf('sockets/test_tcp_refused.c', 'done\n', cflags=['-sNODERAWSOCKETS']) def test_noderawsockets_backpressure(self): # A sink server that accepts but never reads, so the client's writes fill @@ -441,7 +441,7 @@ def handle(self): thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() try: - self.do_runf('sockets/test_tcp_backpressure.c', 'BACKPRESSURE PASS', + self.do_runf('sockets/test_tcp_backpressure.c', 'done\n', cflags=['-sNODERAWSOCKETS'], args=[str(port)]) finally: done.set() @@ -454,12 +454,12 @@ def test_noderawsockets_server(self): # Self-contained loopback accept+echo, exercising bind(:0)+getsockname # (synchronous ephemeral port), listen, accept, non-blocking connect, send # and recv over real OS sockets via the tcp_wrap server path. - self.do_runf('sockets/test_tcp_server.c', 'TCP SERVER PASS', cflags=['-sNODERAWSOCKETS']) + self.do_runf('sockets/test_tcp_server.c', 'done\n', cflags=['-sNODERAWSOCKETS']) def test_noderawsockets_server_autobind(self): # listen() without a prior bind() must auto-bind an ephemeral port and # getsockname() must report it (POSIX), then accept+echo as usual. - self.do_runf('sockets/test_tcp_server.c', 'TCP SERVER PASS', + self.do_runf('sockets/test_tcp_server.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-DNO_EXPLICIT_BIND']) def test_noderawsockets_tcp_ipv6(self): @@ -467,61 +467,55 @@ def test_noderawsockets_tcp_ipv6(self): # listen, accept, non-blocking connect, send/recv on AF_INET6 sockets. if not HAS_IPV6_LOOPBACK: self.skipTest('no IPv6 loopback available') - self.do_runf('sockets/test_tcp_ipv6.c', 'TCP IPV6 PASS', cflags=['-sNODERAWSOCKETS']) + self.do_runf('sockets/test_tcp_ipv6.c', 'done\n', cflags=['-sNODERAWSOCKETS']) def test_noderawsockets_udp_ipv6(self): # Self-contained IPv6 UDP loopback echo over ::1 on AF_INET6 sockets. if not HAS_IPV6_LOOPBACK: self.skipTest('no IPv6 loopback available') - self.do_runf('sockets/test_udp_ipv6.c', 'UDP IPV6 PASS', cflags=['-sNODERAWSOCKETS']) + self.do_runf('sockets/test_udp_ipv6.c', 'done\n', cflags=['-sNODERAWSOCKETS']) def test_noderawsockets_epoll_socket_blocking(self): # A blocking epoll_wait() on a socket is woken by an incoming datagram # through the unified readiness wait-queue (the SOCKFS.emit bridge), with # main() proxied to a worker so the wait can suspend. - self.do_runf('sockets/test_epoll_socket_blocking.c', 'EPOLL SOCKET BLOCKING PASS', + self.do_runf('sockets/test_epoll_socket_blocking.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME']) - def test_noderawsockets_epoll_socket_blocking_jspi(self): - # Same, but the blocking epoll_wait() suspends the wasm stack under JSPI. - # NODERAWSOCKETS runs under node rather than the browser, so gate JSPI on - # node's own support (v24) instead of require_jspi's browser-test path. - if 'EMTEST_SKIP_JSPI' in os.environ: - self.skipTest('skipping JSPI (EMTEST_SKIP_JSPI is set)') - if not self.try_require_node_version(24): - self.skipTest('JSPI requires node v24') + def setup_jspi_node(self): + # These tests run on node via do_runf even though the class is a + # BrowserCore, so require_jspi()'s is_browser_test() early-return skips the + # node handling. The new JSPI API requires node >= 24, so skip below that. + if not common.check_node_version(24): + self.skipTest('JSPI requires node >= 24') if not common.check_node_version(26): self.node_args += ['--experimental-wasm-stack-switching'] self.cflags += ['-Wno-experimental'] self.set_setting('JSPI') - self.do_runf('sockets/test_epoll_socket_blocking.c', 'EPOLL SOCKET BLOCKING PASS', + + def test_noderawsockets_epoll_socket_blocking_jspi(self): + # Same, but the blocking epoll_wait() suspends the wasm stack under JSPI. + self.setup_jspi_node() + self.do_runf('sockets/test_epoll_socket_blocking.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) def test_noderawsockets_epoll_rdhup(self): # A blocking epoll_wait reports EPOLLRDHUP when the TCP peer half-closes its # write side (FIN), distinct from a full EPOLLHUP, and only when requested. - self.do_runf('sockets/test_epoll_rdhup.c', 'EPOLL RDHUP PASS', + self.do_runf('sockets/test_epoll_rdhup.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME']) def test_noderawsockets_epoll_rdhup_jspi(self): - # Same, but the blocking calls suspend the wasm stack under JSPI. Gate on - # node's own JSPI support (v24) since NODERAWSOCKETS runs under node. - if 'EMTEST_SKIP_JSPI' in os.environ: - self.skipTest('skipping JSPI (EMTEST_SKIP_JSPI is set)') - if not self.try_require_node_version(24): - self.skipTest('JSPI requires node v24') - if not common.check_node_version(26): - self.node_args += ['--experimental-wasm-stack-switching'] - self.cflags += ['-Wno-experimental'] - self.set_setting('JSPI') - self.do_runf('sockets/test_epoll_rdhup.c', 'EPOLL RDHUP PASS', + # Same, but the blocking calls suspend the wasm stack under JSPI. + self.setup_jspi_node() + self.do_runf('sockets/test_epoll_rdhup.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) @also_with_proxy_to_pthread def test_noderawsockets_udp(self): # Self-contained loopback UDP echo: the server binds(:0)+getsockname for its # ephemeral port, the client sends a datagram, the server echoes it back. - self.do_runf('sockets/test_udp_echo.c', 'UDP ECHO PASS', cflags=['-sNODERAWSOCKETS']) + self.do_runf('sockets/test_udp_echo.c', 'done\n', cflags=['-sNODERAWSOCKETS']) def test_noderawsockets_epoll_callback(self): # emscripten_epoll_set_callback woken repeatedly by arriving datagrams on a @@ -529,13 +523,13 @@ def test_noderawsockets_epoll_callback(self): # Not run under PROXY_TO_PTHREAD: the callback fires on the main-thread event # loop, which is not where the proxied application thread runs (use a blocking # epoll_wait from a pthread instead). - self.do_runf('sockets/test_epoll_callback.c', 'done', cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) + self.do_runf('sockets/test_epoll_callback.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) @also_with_proxy_to_pthread def test_noderawsockets_udp_connect(self): # Connected UDP: sendto() with an address gives EISCONN, send() reaches the # peer, and datagrams from a non-peer socket are filtered out. - self.do_runf('sockets/test_udp_connect.c', 'UDP CONNECT PASS', cflags=['-sNODERAWSOCKETS']) + self.do_runf('sockets/test_udp_connect.c', 'done\n', cflags=['-sNODERAWSOCKETS']) @requires_native_clang @requires_python_dev_packages