diff --git a/ChangeLog.md b/ChangeLog.md index 10284f6e15d1d..1c739e47f4c2b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,17 @@ See docs/process.md for more on how version tagging works. 6.0.2 (in development) ---------------------- +- 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`. As part of this, the (undocumented) `stream_ops.poll` + FS-backend handler signature changed from `poll(stream, timeout)` to + `poll(stream)` returning the current readiness mask; out-of-tree custom FS + backends with a `poll` handler must update. (#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/site/source/docs/api_reference/emscripten.h.rst b/site/source/docs/api_reference/emscripten.h.rst index fdb9567ffcd86..6d98e7a732e8a 100644 --- a/site/source/docs/api_reference/emscripten.h.rst +++ b/site/source/docs/api_reference/emscripten.h.rst @@ -1280,6 +1280,69 @@ Functions :param em_socket_callback callback: Pointer to a callback function. The callback returns a file descriptor and the arbitrary ``userData`` passed to this function. +.. c:function:: int emscripten_dns_lookup_async(const char *node, const char *service, const struct addrinfo *hints) + + Asynchronous :c:func:`getaddrinfo`. Takes the same ``node``/``service``/``hints`` + inputs and returns a file descriptor that becomes readable once resolution + completes. Wait on it with ``poll``/``select``/``epoll`` or, without blocking, by + adding it to an epoll set and using :c:func:`emscripten_epoll_set_callback`, then read + the result with :c:func:`emscripten_dns_lookup_result`. The caller owns the fd + and should ``close()`` it. + + With ``-sNODERAWSOCKETS`` a hostname is resolved asynchronously via ``node:dns``; + otherwise (and for numeric or ``/etc/hosts`` names) resolution is synchronous, + as :c:func:`getaddrinfo`, and the fd is simply readable on the next turn. + + :param node: The hostname or numeric address to resolve. + :param service: The service name or port string (may be ``NULL``). + :param hints: ``addrinfo`` filter (``ai_family``/``ai_socktype``/etc.; may be ``NULL``). + :returns: A pollable file descriptor, or ``-1`` on failure to start the lookup. + + +.. c:function:: int emscripten_dns_lookup_result(int fd, struct addrinfo **res) + + Reads the outcome of a lookup started by :c:func:`emscripten_dns_lookup_async`, + once its ``fd`` is readable. + + :param int fd: The file descriptor returned by :c:func:`emscripten_dns_lookup_async`. + :param res: On success, receives the head of the resulting ``addrinfo`` list (free it with :c:func:`freeaddrinfo`, as for :c:func:`getaddrinfo`). + :returns: ``0`` on success, or an ``EAI_*`` error code on failure (``EAI_AGAIN`` if the lookup has not completed yet). + + +.. c:type:: em_epoll_callback + + Function pointer type for the :c:func:`emscripten_epoll_set_callback` callback, + defined as: :: + + typedef void (*em_epoll_callback)(int epfd, struct epoll_event *events, int nready, void *userdata); + + ``events`` is a runtime-owned buffer of ``nready`` ready entries, valid only for + the duration of the call. + + +.. c:function:: int emscripten_epoll_set_callback(int epfd, int maxevents, em_epoll_callback callback, void *userdata) + + Register a persistent readiness callback on an existing epoll fd (built with + :c:func:`epoll_create1`/:c:func:`epoll_ctl`): instead of blocking in + :c:func:`epoll_wait`, the runtime calls ``callback`` every time the set makes + progress, delivering up to ``maxevents`` ready events. The interest is armed once + and reused across every delivery. Unlike :c:func:`epoll_wait` it never blocks the + calling stack, so it works without ASYNCIFY/JSPI, and it keeps the runtime alive + while armed. + + There is at most one callback per epoll: calling again replaces it. A ``NULL`` + ``callback`` unregisters; the interest also ends when the epoll fd is closed. + Per-fd ``EPOLLET``/``EPOLLONESHOT`` behave exactly as with the blocking + :c:func:`epoll_wait`. + + :param epfd: An epoll fd from :c:func:`epoll_create1`. + :param maxevents: Maximum number of ready events to deliver per callback. + :param callback: Invoked with ``epfd``, the runtime-owned ``events`` buffer, the ready count ``nready``, and ``userdata``. ``NULL`` unregisters. + :param userdata: Opaque pointer passed through to the callback. + :returns: ``0`` on success, or ``-errno`` (``-EBADF``/``-EINVAL``). + + + Unaligned types =============== diff --git a/src/lib/libcore.js b/src/lib/libcore.js index 5e8265111c251..15d44d76ee203 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -947,53 +947,61 @@ addToLibrary({ return inetPton4(DNS.lookup_name(nameString)); }, - getaddrinfo__deps: ['$DNS', '$inetPton4', '$inetNtop4', '$inetPton6', '$inetNtop6', '$writeSockaddr', 'malloc', 'htonl'], - getaddrinfo__proxy: 'sync', - getaddrinfo: (node, service, hint, out) => { - // Note getaddrinfo currently only returns a single addrinfo with ai_next defaulting to NULL. When NULL - // hints are specified or ai_family set to AF_UNSPEC or ai_socktype or ai_protocol set to 0 then we - // really should provide a linked list of suitable addrinfo values. - var addrs = []; - var canon = null; - var addr = 0; - var port = 0; - var flags = 0; - var family = {{{ cDefs.AF_UNSPEC }}}; - var type = 0; - var proto = 0; - var ai, last; - - function allocaddrinfo(family, type, proto, canon, addr, port) { - var sa, salen, ai; - var errno; - - salen = family === {{{ cDefs.AF_INET6 }}} ? + // The encode/mint stage: turn a resolved descriptor ({entries, type, proto, + // port}, addr in parsed inetPton form) into an addrinfo linked list and return + // the head (0 for an empty list). This is the sole point that mints C memory, + // and the whole chain is freed uniformly by freeaddrinfo - one ownership rule. + // (A future ring/aio backend would add a sibling encoder here, e.g. one that + // writes into a caller buffer, without touching parse/resolve.) + $writeAddrInfoList__deps: ['$inetNtop4', '$inetNtop6', '$writeSockaddr', 'malloc'], + $writeAddrInfoList: (desc) => { + var head = 0, prev = 0; + for (var entry of desc.entries) { + var family = entry.family; + var salen = family === {{{ cDefs.AF_INET6 }}} ? {{{ C_STRUCTS.sockaddr_in6.__size__ }}} : {{{ C_STRUCTS.sockaddr_in.__size__ }}}; - addr = family === {{{ cDefs.AF_INET6 }}} ? - inetNtop6(addr) : - inetNtop4(addr); - sa = _malloc(salen); - errno = writeSockaddr(sa, family, addr, port); + var sa = _malloc(salen); + var errno = writeSockaddr(sa, family, family === {{{ cDefs.AF_INET6 }}} ? inetNtop6(entry.addr) : inetNtop4(entry.addr), desc.port); #if ASSERTIONS assert(!errno); #endif - - ai = _malloc({{{ C_STRUCTS.addrinfo.__size__ }}}); + var ai = _malloc({{{ C_STRUCTS.addrinfo.__size__ }}}); {{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_family, 'family', 'i32') }}}; - {{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_socktype, 'type', 'i32') }}}; - {{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_protocol, 'proto', 'i32') }}}; - {{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_canonname, 'canon', '*') }}}; + {{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_socktype, 'desc.type', 'i32') }}}; + {{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_protocol, 'desc.proto', 'i32') }}}; + {{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_canonname, '0', '*') }}}; {{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_addr, 'sa', '*') }}}; - if (family === {{{ cDefs.AF_INET6 }}}) { - {{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_addrlen, C_STRUCTS.sockaddr_in6.__size__, 'i32') }}}; + {{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_addrlen, 'salen', 'i32') }}}; + {{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_next, '0', 'i32') }}}; + if (prev) { + {{{ makeSetValue('prev', C_STRUCTS.addrinfo.ai_next, 'ai', '*') }}}; } else { - {{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_addrlen, C_STRUCTS.sockaddr_in.__size__, 'i32') }}}; + head = ai; } - {{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_next, '0', 'i32') }}}; - - return ai; + prev = ai; } + return head; + }, + + // Shared getaddrinfo core. Allocates nothing: returns a resolved descriptor + // {entries, type, proto, port} (entries are {family, addr} with addr in parsed + // inetPton form), a negative EAI_* code on failure, or - under NODERAWSOCKETS, + // for a hostname needing DNS - a {node, family, type, proto, port} descriptor + // (no entries) for the caller to resolve. The result is minted from a + // descriptor by writeAddrInfoList at the point ownership passes to the caller. + $getAddrInfo__deps: ['$DNS', '$inetPton4', '$inetPton6', 'htonl', '$UTF8ToString', +#if NODERAWSOCKETS + '$nodeSockHelpers', +#endif + ], + $getAddrInfo: (node, service, hint) => { + var addr = 0; + var port = 0; + var flags = 0; + var family = {{{ cDefs.AF_UNSPEC }}}; + var type = 0; + var proto = 0; if (hint) { flags = {{{ makeGetValue('hint', C_STRUCTS.addrinfo.ai_flags, 'i32') }}}; @@ -1063,9 +1071,7 @@ addToLibrary({ addr = [0, 0, 0, _htonl(1)]; } } - ai = allocaddrinfo(family, type, proto, null, addr, port); - {{{ makeSetValue('out', '0', 'ai', '*') }}}; - return 0; + return { entries: [{ family, addr }], type, proto, port }; } // @@ -1096,9 +1102,7 @@ addToLibrary({ } } if (addr != null) { - ai = allocaddrinfo(family, type, proto, node, addr, port); - {{{ makeSetValue('out', '0', 'ai', '*') }}}; - return 0; + return { entries: [{ family, addr }], type, proto, port }; } if (flags & {{{ cDefs.AI_NUMERICHOST }}}) { return {{{ cDefs.EAI_NONAME }}}; @@ -1107,6 +1111,22 @@ addToLibrary({ // // try as a hostname // +#if NODERAWSOCKETS + // /etc/hosts resolves synchronously (read fresh through emscripten's FS). + var hosts = nodeSockHelpers.readHosts(node).filter((e) => + family === {{{ cDefs.AF_UNSPEC }}} || e.family === family); + if (hosts.length) { + var entries = hosts.map((e) => ({ + family: e.family, + addr: e.family === {{{ cDefs.AF_INET6 }}} ? inetPton6(e.addr) : inetPton4(e.addr), + })); + return { entries, type, proto, port }; + } + // A real hostname needs a DNS lookup; hand the request back to the caller to + // resolve asynchronously (getaddrinfo suspends under JSPI / returns + // EAI_AGAIN otherwise; emscripten_dns_lookup_async drives the poll-fd flow). + return { node, family, type, proto, port }; +#else // resolve the hostname to a temporary fake address node = DNS.lookup_name(node); addr = inetPton4(node); @@ -1115,9 +1135,45 @@ addToLibrary({ } else if (family === {{{ cDefs.AF_INET6 }}}) { addr = [0, 0, _htonl(0xffff), addr]; } - ai = allocaddrinfo(family, type, proto, null, addr, port); - {{{ makeSetValue('out', '0', 'ai', '*') }}}; - return 0; + return { entries: [{ family, addr }], type, proto, port }; +#endif + }, + + getaddrinfo__deps: ['$getAddrInfo', '$writeAddrInfoList', +#if NODERAWSOCKETS && ASYNCIFY == 2 + '$nodeSockHelpers', +#endif + ], + getaddrinfo__proxy: 'sync', +#if NODERAWSOCKETS && ASYNCIFY == 2 + // Under JSPI a hostname miss suspends the wasm stack on the real node:dns + // lookup (returning a promise) rather than reporting EAI_AGAIN. A resolved + // descriptor (numeric/hosts) or error does not suspend. + getaddrinfo__async: true, +#endif + getaddrinfo: (node, service, hint, out) => { + // parse -> (resolve) -> mint. One descriptor threads through all three. + var desc = getAddrInfo(node, service, hint); + if (typeof desc === 'object') { + if (desc.entries) { + {{{ makeSetValue('out', '0', 'writeAddrInfoList(desc)', '*') }}}; + return 0; + } +#if NODERAWSOCKETS && ASYNCIFY == 2 + // JSPI: suspend on the real node:dns lookup, which fills desc.entries, then + // mint from the same descriptor. + return nodeSockHelpers.resolveAddrInfo(desc).then((eai) => { + if (eai) return eai; + {{{ makeSetValue('out', '0', 'writeAddrInfoList(desc)', '*') }}}; + return 0; + }); +#elif NODERAWSOCKETS + // No synchronous DNS available: numeric and /etc/hosts names resolve above, + // anything else must be resolved out-of-band via emscripten_dns_lookup_async. + return {{{ cDefs.EAI_AGAIN }}}; +#endif + } + return desc; }, getnameinfo__deps: ['$DNS', '$readSockaddr', '$stringToUTF8'], diff --git a/src/lib/libfs.js b/src/lib/libfs.js index 0e0fe3f5a730e..1916f47811d15 100644 --- a/src/lib/libfs.js +++ b/src/lib/libfs.js @@ -134,6 +134,15 @@ FS.staticInit();`; readMode = {{{ cDefs.S_IRUGO }}} | {{{ cDefs.S_IXUGO }}}; writeMode = {{{ cDefs.S_IWUGO }}}; mounted = null; +#if USE_CLOSURE_COMPILER + // Closure (@struct) requires these declared ahead of time. The readiness + // wait-queue is populated lazily, and only on nodes that derive real + // readiness (sockets, pipes, an epoll's own node). + /** @type {Set|null} */ + listeners = null; + /** @type {number} */ + exclTurn = 0; +#endif constructor(parent, name, mode, rdev) { if (!parent) { parent = this; // root node sets parent to itself @@ -164,6 +173,48 @@ FS.staticInit();`; get isDevice() { return FS.isChrdev(this.mode); } + // The per-inode readiness wait-queue. The node carries a Set of listener + // entries {cb}; producers (SOCKFS, PIPEFS) call notifyListeners on a + // readiness transition, and poll()/epoll consume it. It lives on the node + // (not the fd) so dup'd fds share one queue. Only nodes that derive real + // readiness (sockets, pipes, and an epoll's own node) ever use this - + // always-ready types (regular files, ttys) never register or notify. + addListener(cb, exclusive = false) { + var entry = {cb, exclusive}; + var listeners = (this.listeners ??= new Set()); + listeners.add(entry); + return {listeners, entry}; + } + notifyListeners(flags) { + // Iterates the set without copying, which is safe ONLY under a + // load-bearing contract that every internal listener must honour: + // 1. A listener must not run user code synchronously (a poll waiter only + // resolves a Promise; an epoll registration only re-lists + + // re-notifies; the epoll callback only schedules a tick). User code + // runs on a later tick, never inside this loop. + // 2. A listener may delete entries only from ITS OWN waiter, never from + // a sibling node's set that may be mid-iteration. (Deleting an entry + // of the set being iterated here is fine - a Set tolerates removal of + // a not-yet-visited entry mid-iteration; mutating a *different* node's + // set is fine because that set is not being iterated.) + // Violating either gives silently skipped wakeups that are near-impossible + // to reproduce. Any new producer/listener must preserve it. + if (!this.listeners) return; + // Fire every non-exclusive listener. Among EPOLLEXCLUSIVE registrations + // (one fd watched by several epolls) wake only one, rotating round-robin + // per node, to avoid a thundering herd. (Only epoll registrations are ever + // exclusive; poll waiters and a node's own consumers are not.) + var excl; + for (var entry of this.listeners) { + if (entry.exclusive) (excl ||= []).push(entry); + else entry.cb(flags); + } + if (excl) { + var i = (this.exclTurn || 0) % excl.length; + this.exclTurn = i + 1; + excl[i].cb(flags); + } + } }, // diff --git a/src/lib/libpipefs.js b/src/lib/libpipefs.js index c99ffb7bc29b6..90f9a71c56770 100644 --- a/src/lib/libpipefs.js +++ b/src/lib/libpipefs.js @@ -21,23 +21,6 @@ addToLibrary({ // able to read from the read end after write end is closed. refcnt : 2, timestamp: new Date(), -#if PTHREADS || ASYNCIFY - readableHandlers: [], - registerReadableHandler: (callback) => { - callback.registerCleanupFunc(() => { - const i = pipe.readableHandlers.indexOf(callback); - if (i !== -1) pipe.readableHandlers.splice(i, 1); - }); - pipe.readableHandlers.push(callback); - }, - notifyReadableHandlers: () => { - while (pipe.readableHandlers.length > 0) { - const cb = pipe.readableHandlers.shift(); - if (cb) cb({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}); - } - pipe.readableHandlers = []; - } -#endif }; pipe.buckets.push({ @@ -53,6 +36,8 @@ addToLibrary({ rNode.pipe = pipe; wNode.pipe = pipe; + // The read end's node carries the poll wait-queue; writes wake it. + pipe.readNode = rNode; var readableStream = FS.createStream({ path: rName, @@ -97,7 +82,7 @@ addToLibrary({ blocks: 0, }; }, - poll(stream, timeout, notifyCallback) { + poll(stream) { var pipe = stream.node.pipe; if ((stream.flags & {{{ cDefs.O_ACCMODE }}}) === {{{ cDefs.O_WRONLY }}}) { @@ -108,10 +93,6 @@ addToLibrary({ return ({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}); } } - -#if PTHREADS || ASYNCIFY - if (notifyCallback) pipe.registerReadableHandler(notifyCallback); -#endif return 0; }, dup(stream) { @@ -233,9 +214,7 @@ addToLibrary({ if (freeBytesInCurrBuffer >= dataLen) { currBucket.buffer.set(data, currBucket.offset); currBucket.offset += dataLen; -#if PTHREADS || ASYNCIFY - pipe.notifyReadableHandlers(); -#endif + pipe.readNode?.notifyListeners({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}); return dataLen; } else if (freeBytesInCurrBuffer > 0) { currBucket.buffer.set(data.subarray(0, freeBytesInCurrBuffer), currBucket.offset); @@ -267,12 +246,15 @@ addToLibrary({ newBucket.buffer.set(data); } -#if PTHREADS || ASYNCIFY - pipe.notifyReadableHandlers(); -#endif + pipe.readNode?.notifyListeners({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}); return dataLen; }, close(stream) { + // The fd is going away: wake anything waiting on it (poll/epoll) with + // POLLNVAL so a blocking wait unblocks and an epoll registration is + // evicted on the next derive - mirroring SOCKFS, and letting an epoll + // callback's keepalive drop once its last watched fd is gone. + stream.node?.notifyListeners({{{ cDefs.POLLNVAL }}}); var pipe = stream.node.pipe; pipe.refcnt--; if (pipe.refcnt === 0) { diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index d3a5b64b62605..611217ab6102c 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -636,7 +636,10 @@ sigs = { emscripten_destroy_audio_context__sig: 'vi', emscripten_destroy_web_audio_node__sig: 'vi', emscripten_destroy_worker__sig: 'vi', + emscripten_dns_lookup_async__sig: 'ippp', + emscripten_dns_lookup_result__sig: 'iip', 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/libsockfs.js b/src/lib/libsockfs.js index 66bcdcb162a42..537d6c2579b6d 100644 --- a/src/lib/libsockfs.js +++ b/src/lib/libsockfs.js @@ -23,6 +23,25 @@ addToLibrary({ }, emit(event, param) { SOCKFS.callbacks[event]?.(param); + // Bridge socket readiness into the inode wait-queue (poll/epoll). The + // 'error' event carries [fd, ...]; the rest carry the fd directly. + var fd = event === 'error' ? param[0] : param; + var flags = { + 'message': {{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}, + 'open': {{{ cDefs.POLLOUT }}}, + 'connection': {{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}, + 'close': {{{ cDefs.POLLIN }}} | {{{ cDefs.POLLHUP }}}, + 'error': {{{ cDefs.POLLERR }}}, + }[event]; + // 'listen' has no readiness mapping; skip it. + if (flags) FS.getStream(fd)?.node?.notifyListeners(flags); + }, + // Mark an async-completion pseudo-socket ready: flip it readable and wake + // its waiters through the generic wait-queue. A future ring/aio completion + // fd would reuse the same mechanism rather than re-adding one. + finishDns(sock) { + sock.dnsDone = true; + sock.stream.node?.notifyListeners({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}); }, mount(mount) { #if expectToReceiveOnModule('websocket') @@ -116,6 +135,11 @@ addToLibrary({ stream_ops: { poll(stream) { var sock = stream.node.sock; + // A DNS request fd (emscripten_dns_lookup_async) is readable once the + // lookup completes; read the result with emscripten_dns_lookup_result. + if (sock.dns) { + return sock.dnsDone ? ({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}) : 0; + } return sock.sock_ops.poll(sock); }, ioctl(stream, request, varargs) { @@ -138,6 +162,10 @@ addToLibrary({ }, close(stream) { var sock = stream.node.sock; + // The fd is going away: wake waiters with POLLNVAL so they don't hang. + stream.node?.notifyListeners({{{ cDefs.POLLNVAL }}}); + // A DNS request fd is a pseudo-socket with no backend resources. + if (sock.dns) return; sock.sock_ops.close(sock); } }, @@ -417,7 +445,8 @@ addToLibrary({ if (sock.connecting) { mask |= {{{ cDefs.POLLOUT }}}; } else { - mask |= {{{ cDefs.POLLHUP }}}; + // A closed peer is both a full hangup and a read-side hangup. + mask |= {{{ cDefs.POLLHUP }}} | {{{ cDefs.POLLRDHUP }}}; } } @@ -555,6 +584,8 @@ addToLibrary({ // push to queue for accept to pick up sock.pending.push(newsock); SOCKFS.emit('connection', newsock.stream.fd); + // A queued client makes the listening socket readable (POLLIN). + sock.stream.node?.notifyListeners({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}); } else { // create a peer on the listen socket so calling sendto // with the listen socket and an address will resolve @@ -788,4 +819,71 @@ addToLibrary({ emscripten_set_socket_close_callback__deps: ['$_setNetworkCallback'], emscripten_set_socket_close_callback: (userData, callback) => _setNetworkCallback('close', userData, callback), + + // Asynchronous getaddrinfo: same (node, service, hint) inputs as the sync call. + // Returns a pollable fd that becomes readable when resolution completes (wait + // on it with poll/select or emscripten_epoll_set_callback); read the result + // with emscripten_dns_lookup_result. Returns -1 on failure to allocate the fd. + // Without -sNODERAWSOCKETS this resolves synchronously (like getaddrinfo) and + // the fd is simply readable on the next turn. + emscripten_dns_lookup_async__deps: ['$SOCKFS', '$getAddrInfo', '$safeSetTimeout', +#if NODERAWSOCKETS + '$nodeSockHelpers', +#endif + ], + emscripten_dns_lookup_async__proxy: 'sync', + emscripten_dns_lookup_async: (node, service, hint) => { + var sock; + try { + sock = SOCKFS.createSocket({{{ cDefs.AF_INET }}}, {{{ cDefs.SOCK_STREAM }}}, 0); + } catch (e) { + return -1; + } + sock.dns = true; + // Read the request synchronously (the input pointers are only valid now). No + // C memory is allocated here; the resolved descriptor is stashed on the sock + // and minted into an addrinfo only when the caller takes it via + // emscripten_dns_lookup_result. + var desc = getAddrInfo(node, service, hint); +#if NODERAWSOCKETS + if (typeof desc === 'object' && !desc.entries) { + // A real hostname: resolve via node:dns (fills desc.entries), then stash + // the same descriptor for the caller to mint from. + nodeSockHelpers.resolveAddrInfo(desc).then((eai) => { + sock.dnsResult = eai; + sock.dnsDesc = desc; + SOCKFS.finishDns(sock); + }); + return sock.stream.fd; + } +#endif + // Resolved synchronously (numeric/`/etc/hosts`/fake/null-node success, or a + // validation error). Deliver readiness on a later turn regardless, so the + // contract is uniformly async (the caller can poll or attach a listener + // first); safeSetTimeout keeps the runtime alive until it fires. + if (typeof desc === 'object') { + sock.dnsDesc = desc; + sock.dnsResult = 0; + } else { + sock.dnsResult = desc; + } + safeSetTimeout(() => SOCKFS.finishDns(sock), 0); + return sock.stream.fd; + }, + + // Read the outcome of a completed async lookup: 0 on success - minting the + // addrinfo list and writing its head to *res (freed with freeaddrinfo, as for + // getaddrinfo) - or an EAI_* code on failure (EAI_AGAIN if not yet complete). + // The memory is allocated here, so a caller that closes the fd without reading + // leaks nothing. The caller owns the fd and should close() it. + emscripten_dns_lookup_result__deps: ['$SOCKFS', '$writeAddrInfoList'], + emscripten_dns_lookup_result__proxy: 'sync', + emscripten_dns_lookup_result: (fd, res) => { + var sock = SOCKFS.getSocket(fd); + if (!sock || !sock.dns || !sock.dnsDone) return {{{ cDefs.EAI_AGAIN }}}; + if (sock.dnsResult === 0) { + {{{ makeSetValue('res', '0', 'writeAddrInfoList(sock.dnsDesc)', '*') }}}; + } + return sock.dnsResult; + }, }); diff --git a/src/lib/libsockfs_node.js b/src/lib/libsockfs_node.js index 067b435f2d75e..436465e7c015f 100644 --- a/src/lib/libsockfs_node.js +++ b/src/lib/libsockfs_node.js @@ -54,7 +54,7 @@ null; var NodeSockFSLibrary = { // Node plumbing shared by the interface methods below. - $nodeSockHelpers__deps: ['$SOCKFS', '$ERRNO_CODES'], + $nodeSockHelpers__deps: ['$SOCKFS', '$ERRNO_CODES', '$FS', '$inetPton4', '$inetPton6'], $nodeSockHelpers: { // node builtins, resolved once each. getBuiltinModule works in both // CommonJS and ESM output, with require as the fallback. @@ -67,6 +67,67 @@ var NodeSockFSLibrary = { getDgram() { return nodeSockHelpers.dgramModule ??= (process.getBuiltinModule || require)('dgram'); }, + getDns() { + return nodeSockHelpers.dnsModule ??= (process.getBuiltinModule || require)('dns'); + }, + // Look up `name` in /etc/hosts, read fresh on each call through emscripten's + // FS so live edits (MEMFS or a mounted real fs) are honored. Returns a list + // of {family, addr}; a missing or unreadable file is just empty. + readHosts(name) { + var out = []; + var text; + try { + text = FS.readFile('/etc/hosts', { encoding: 'utf8' }); + } catch (e) { + return out; + } + for (var line of text.split('\n')) { + var hash = line.indexOf('#'); + if (hash !== -1) line = line.slice(0, hash); + var parts = line.split(/\s+/).filter((p) => p.length); + if (parts.length < 2 || !parts.slice(1).includes(name)) continue; + var addr = parts[0]; + out.push({ family: addr.includes(':') ? {{{ cDefs.AF_INET6 }}} : {{{ cDefs.AF_INET }}}, addr }); + } + return out; + }, + // Map a node:dns error to an EAI_* code. node:dns surfaces either getaddrinfo + // EAI_* names or libuv/system codes; the transient ones become EAI_AGAIN and + // everything else a hard "name not found". + eaiForDns(e) { + switch (e && e.code) { + case 'EAI_AGAIN': + case 'ETIMEDOUT': + case 'ESERVFAIL': + case 'EREFUSED': + return {{{ cDefs.EAI_AGAIN }}}; + default: + return {{{ cDefs.EAI_NONAME }}}; + } + }, + // The resolve stage: take a needs-DNS descriptor (from getAddrInfo) and fill + // in desc.entries via node:dns. Returns a promise of the EAI_* code (0 on + // success). Pure: no fd, no C allocation - just name -> addresses, so a + // future ring/aio backend can reuse it verbatim. + resolveAddrInfo(desc) { + var opts = { all: true }; + if (desc.family === {{{ cDefs.AF_INET }}}) opts.family = 4; + else if (desc.family === {{{ cDefs.AF_INET6 }}}) opts.family = 6; + return new Promise((resolve) => { + nodeSockHelpers.getDns().lookup(desc.node, opts, (err, addresses) => { + if (err) { + resolve(nodeSockHelpers.eaiForDns(err)); + } else { + desc.entries = addresses.map((a) => { + var fam = a.family === 6 ? {{{ cDefs.AF_INET6 }}} : {{{ cDefs.AF_INET }}}; + return { family: fam, addr: fam === {{{ cDefs.AF_INET6 }}} ? inetPton6(a.address) : inetPton4(a.address) }; + }); + resolve(addresses.length ? 0 : {{{ cDefs.EAI_NONAME }}}); + } + }); + }); + }, + // True when node:dgram exposes both synchronous bindSync and connectSync // (a recent addition), letting UDP run entirely on the public API. A runtime // missing either falls back to the private udp_wrap handle, which provides @@ -341,7 +402,10 @@ var NodeSockFSLibrary = { } else if (sock.connection && sock.state === {{{ SOCK_STATE_CONNECTED }}} && !sock.writeBlocked) { mask |= {{{ cDefs.POLLOUT }}}; } - if (sock.readClosed) mask |= {{{ cDefs.POLLHUP }}}; + // A peer FIN / read-side hangup (recv will see EOF) is POLLRDHUP; only a + // fully closed connection is a POLLHUP. + if (sock.readClosed) mask |= {{{ cDefs.POLLRDHUP }}}; + if (sock.state === {{{ SOCK_STATE_CLOSED }}}) mask |= {{{ cDefs.POLLHUP }}}; return mask; }, ioctl(sock, request, arg) { @@ -507,6 +571,8 @@ var NodeSockFSLibrary = { try { conn.resume(); } catch (e) {} // paused by pauseOnConnect sock.pending.push(newsock); SOCKFS.emit('connection', newsock.stream.fd); + // A queued client makes the listening socket readable (POLLIN). + sock.stream.node?.notifyListeners({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}); }); server.on('error', (e) => { sock.error = nodeSockHelpers.nodeErrToErrno(e); diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 46267660a2eee..d762d6bb3023b 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -583,11 +583,36 @@ var SyscallsLibrary = { var stream = SYSCALLS.getStreamFromFD(fd); return 0; // we can't do anything synchronously; the in-memory FS is already synced to }, + // The inode readiness wait-queue. It lives on the FS node (so dup'd fds share + // one queue) as a Set of identity entries {cb}: identity lets the same `cb` be + // registered more than once (a dup'd fd) and removed in O(1) without copying. + // Producers (SOCKFS.emit, pipe writes, epoll_ctl) feed it; poll()/epoll_wait + // consume it. + // Derive readiness for one fd against its requested `events`: POLLNVAL for a + // closed/bad fd, default readable+writable for types without a poll handler. + // POLLERR/POLLHUP/POLLNVAL are output-only conditions reported regardless of + // `events` (a bad fd reports POLLNVAL even if the caller didn't ask for it). + $pollOne__internal: true, + $pollOne__deps: ['$FS'], + $pollOne: (fd, events) => { + var flags = {{{ cDefs.POLLNVAL }}}; + var stream = FS.getStream(fd); + if (stream) { + // Streams without a poll handler (regular files, incl. NODERAWFS/NODEFS + // which leave stream_ops unset) are treated as always readable+writable. + if (stream.stream_ops?.poll) { + flags = stream.stream_ops.poll(stream); + } else { + flags = {{{ cDefs.POLLIN | cDefs.POLLOUT }}}; + } + } + return flags & (events | {{{ cDefs.POLLERR }}} | {{{ cDefs.POLLHUP }}} | {{{ cDefs.POLLNVAL }}}); + }, __syscall_poll__proxy: 'sync', __syscall_poll__async: 'auto', __syscall_poll__deps: ['$doPoll', #if PTHREADS || ASYNCIFY - '$doPollAsync', + '$readPollfds', '$writePollfds', '$pollWait', #endif ], __syscall_poll: (fds, nfds, timeout) => { @@ -597,131 +622,515 @@ var SyscallsLibrary = { #else const isAsyncContext = true; #endif + // When proxied from a worker (PTHREADS) or able to suspend (ASYNCIFY/JSPI), + // block on the wait-queue: read the interests out of memory, wait, then + // write revents back into the (still-live) pollfd array and resolve. if (isAsyncContext) { - return doPollAsync(fds, nfds, timeout); +#if RUNTIME_DEBUG + dbg('async poll start'); +#endif + var pfds = readPollfds(fds, nfds); + return new Promise((resolve) => pollWait(pfds, timeout, (count) => { + writePollfds(fds, pfds); + resolve(count); + })); } #endif - - var count = doPoll(fds, nfds, 0, undefined); + var count = doPoll(fds, nfds); #if ASSERTIONS if (!count && timeout != 0) warnOnce('non-zero poll() timeout not supported: ' + timeout) #endif return count; }, + // Synchronous poll(): derive each fd in place, writing revents and returning + // the ready count. Used by the non-suspending syscall paths. + $doPoll__internal: true, + $doPoll__deps: ['$pollOne'], + $doPoll: (fds, nfds) => { + var count = 0; + for (var i = 0; i < nfds; i++) { + var pollfd = fds + {{{ C_STRUCTS.pollfd.__size__ }}} * i; + var revents = pollOne( + {{{ makeGetValue('pollfd', C_STRUCTS.pollfd.fd, 'i32') }}}, + {{{ makeGetValue('pollfd', C_STRUCTS.pollfd.events, 'i16') }}}); + if (revents) count++; + {{{ makeSetValue('pollfd', C_STRUCTS.pollfd.revents, 'revents', 'i16') }}}; + } + return count; + }, #if PTHREADS || ASYNCIFY - $doPollAsync__internal: true, - $doPollAsync__deps: ['$FS', '$doPoll'], - $doPollAsync: (fds, nfds, timeout) => { -#if RUNTIME_DEBUG - dbg('async poll start'); + // Read a pollfd array into JS interests (fd + events); the source buffer need + // not persist afterwards. + $readPollfds__internal: true, + $readPollfds: (fds, nfds) => { + var pfds = []; + for (var i = 0; i < nfds; i++) { + var pollfd = fds + {{{ C_STRUCTS.pollfd.__size__ }}} * i; + pfds.push({ + fd: {{{ makeGetValue('pollfd', C_STRUCTS.pollfd.fd, 'i32') }}}, + events: {{{ makeGetValue('pollfd', C_STRUCTS.pollfd.events, 'i16') }}}, + revents: 0, + }); + } + return pfds; + }, + // Write JS interests (fd + events + revents) into a pollfd array. + $writePollfds__internal: true, + $writePollfds: (fds, pfds) => { + for (var i = 0; i < pfds.length; i++) { + var pollfd = fds + {{{ C_STRUCTS.pollfd.__size__ }}} * i; + {{{ makeSetValue('pollfd', C_STRUCTS.pollfd.fd, 'pfds[i].fd', 'i32') }}}; + {{{ makeSetValue('pollfd', C_STRUCTS.pollfd.events, 'pfds[i].events', 'i16') }}}; + {{{ makeSetValue('pollfd', C_STRUCTS.pollfd.revents, 'pfds[i].revents', 'i16') }}}; + } + }, + // Wait for readiness across a set of interests {fd, events, revents}, calling + // complete(count) once: now if any are ready (or timeout 0), else register one + // waiter per fd on its node wait-queue and re-derive the whole set on any wake + // (the wake flags are just the trigger), completing then or after `timeout`. + $pollWait__internal: true, + $pollWait__deps: ['$FS', '$pollOne'], + $pollWait: (pfds, timeout, complete) => { + var regs = []; + var timer; + var done = false; + function derive() { + var count = 0; + for (var p of pfds) { + p.revents = pollOne(p.fd, p.events); + if (p.revents) count++; + } + return count; + } + function teardown() { + done = true; + for (var r of regs) r.listeners.delete(r.entry); + if (timer) clearTimeout(timer); + } + function finish(count) { + if (done) return; + teardown(); + complete(count); + } + var count = derive(); + if (count || !timeout) { + finish(count); + } else { + var recheck = () => { + if (done) return; + var c = derive(); + if (c) finish(c); + }; + for (var p of pfds) { + var stream = FS.getStream(p.fd); + if (stream) regs.push(stream.node.addListener(recheck)); + } + if (timeout > 0) timer = setTimeout(() => finish(0), timeout); + } + }, #endif + // libc routes zero-timeout poll() calls here: the same synchronous + // readiness derivation as __syscall_poll, but as a plain import that never + // suspends, so probes stay callable from any context (under JSPI, + // __syscall_poll is a suspending import and traps when called from a stack + // that wasn't entered through a promising export). + __syscall_poll_nonblocking__proxy: 'sync', + __syscall_poll_nonblocking__deps: ['$doPoll'], + __syscall_poll_nonblocking: (fds, nfds) => { + return doPoll(fds, nfds); + }, - // Enable event handlers only when the poll call is proxied from a worker. - // TODO: Could use `Promise.withResolvers` here if we know its available. - var resolve; - var promise = new Promise((resolve_) => { resolve = resolve_; }); - var cleanupFuncs = []; - var notifyDone = false; + // 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) { + // Wake any parent epoll watching this epoll fd so it re-derives and + // drops the now-stale registration (via doEpollWait's shared check), + // matching the socket/pipe close notify. + stream.node?.notifyListeners({{{ cDefs.POLLNVAL }}}); + 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(); + }, + }, + }), - function asyncPollComplete(count) { - if (notifyDone) { - return; - } - notifyDone = true; -#if RUNTIME_DEBUG - dbg('asyncPollComplete', count); + // 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 - cleanupFuncs.forEach(cb => cb()); - resolve(count); + }, + + // 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); + }, + + __syscall_epoll_create1__deps: ['$newEpollInstance'], + __syscall_epoll_create1__proxy: 'sync', + __syscall_epoll_create1: (flags) => { + return newEpollInstance().fd; + }, + + __syscall_epoll_ctl__deps: ['$FS', '$pollOne', '$rdllistAdd', '$epollEvict', '$reconcileEpollKeepalive'], + __syscall_epoll_ctl__proxy: 'sync', + __syscall_epoll_ctl: (epfd, op, fd, ev) => { + var ep = FS.getStream(epfd); + if (!ep?.epoll) return -{{{ cDefs.EBADF }}}; + 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 == epfd) return -{{{ cDefs.EINVAL }}}; - function makeNotifyCallback(stream, pollfd) { - var cb = (flags) => { - if (notifyDone) { - return; + // 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 }}}; } -#if RUNTIME_DEBUG - dbg(`async poll notify: stream=${stream}`); -#endif - var events = {{{ makeGetValue('pollfd', C_STRUCTS.pollfd.events, 'i16') }}}; - flags &= events | {{{ cDefs.POLLERR }}} | {{{ cDefs.POLLHUP }}}; -#if ASSERTIONS - assert(flags) -#endif - {{{ makeSetValue('pollfd', C_STRUCTS.pollfd.revents, 'flags', 'i16') }}}; - asyncPollComplete(1); } - cb.registerCleanupFunc = (f) => { - if (f) cleanupFuncs.push(f); - } - return cb; + } else { // EPOLL_CTL_MOD + if (!has) return -{{{ cDefs.ENOENT }}}; + // EPOLLEXCLUSIVE may only be set at ADD time. + if (events & {{{ cDefs.EPOLLEXCLUSIVE }}}) return -{{{ cDefs.EINVAL }}}; } - if (timeout > 0) { - var t = setTimeout(() => { -#if RUNTIME_DEBUG - dbg('poll: timeout', timeout); -#endif - asyncPollComplete(0); - }, timeout); - cleanupFuncs.push(() => clearTimeout(t)); + + // `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 }}})); } - // A zero timeout never registers notifications: the derivation alone - // answers, matching the non-blocking probe. - var count = doPoll(fds, nfds, timeout, makeNotifyCallback); - if (count || !timeout) { - asyncPollComplete(count); + // 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 promise; + return 0; }, -#endif - // The shared readiness derivation: one pass over the pollfds, writing - // revents and returning the ready count. With a nonzero `timeout`, a - // readiness notification is also registered on each stream by the same - // `stream_ops.poll` call that derives it, so there is no window between - // registration and derivation; a zero `timeout` means the caller will not - // wait, so no notification is registered (the plain probe). - $doPoll__internal: true, - $doPoll__deps: ['$FS'], - $doPoll: (fds, nfds, timeout, makeNotifyCallback) => { - var count = 0; - for (var i = 0; i < nfds; i++) { - var pollfd = fds + {{{ C_STRUCTS.pollfd.__size__ }}} * i; - var fd = {{{ makeGetValue('pollfd', C_STRUCTS.pollfd.fd, 'i32') }}}; - var events = {{{ makeGetValue('pollfd', C_STRUCTS.pollfd.events, 'i16') }}}; - var flags = {{{ cDefs.POLLNVAL }}}; - var stream = FS.getStream(fd); - if (stream) { - if (stream.stream_ops.poll) { - flags = timeout - ? stream.stream_ops.poll(stream, timeout, makeNotifyCallback(stream, pollfd)) - : stream.stream_ops.poll(stream, -1); - } else { - flags = {{{ cDefs.POLLIN | cDefs.POLLOUT }}}; + + // 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. } - flags &= events | {{{ cDefs.POLLERR }}} | {{{ cDefs.POLLHUP }}}; - if (flags) count++; - {{{ makeSetValue('pollfd', C_STRUCTS.pollfd.revents, 'flags', 'i16') }}}; + 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; + }, + + __syscall_epoll_pwait__proxy: 'sync', + __syscall_epoll_pwait__async: 'auto', + __syscall_epoll_pwait__deps: ['$FS', '$doEpollWait'], + __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 }}}; +#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; }, - // libc routes zero-timeout poll() calls here: the same synchronous - // readiness derivation as __syscall_poll, but as a plain import that never - // suspends, so probes stay callable from any context (under JSPI, - // __syscall_poll is a suspending import and traps when called from a stack - // that wasn't entered through a promising export). - __syscall_poll_nonblocking__proxy: 'sync', - __syscall_poll_nonblocking__deps: ['$doPoll'], - __syscall_poll_nonblocking: (fds, nfds) => { - return doPoll(fds, nfds, 0, undefined); - }, - // 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 }}}, + + // 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', '$safeSetTimeout', '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 fresh tick (the callback must not run in the + // producer's/caller's stack), re-deriving the current ready set at that tick. + function wake() { + if (it.scheduled) return; + it.scheduled = true; + safeSetTimeout(() => { + it.scheduled = false; + if (ep.interest !== it) return; // cleared before the tick fired + var c = doEpollWait(ep, buf, maxevents); + if (c) { + {{{ 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 quite a + // blocking epoll_wait loop - there the app owns the loop and may block + // elsewhere, whereas here the runtime drives the macrotask queue. A + // level-triggered fd that is structurally always ready and never + // drained (e.g. EPOLLOUT on a writable socket) will therefore spin the + // event loop; use EPOLLET or unregister for such fds. + if (ep.interest === it && ep.rdlHead) wake(); + } + }, 0); + } + 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; + }, __syscall_getcwd__deps: ['$lengthBytesUTF8', '$stringToUTF8'], __syscall_getcwd: (buf, size) => { if (size === 0) return -{{{ cDefs.EINVAL }}}; diff --git a/src/struct_info.json b/src/struct_info.json index 71f715202e145..a396a4645580f 100644 --- a/src/struct_info.json +++ b/src/struct_info.json @@ -130,7 +130,8 @@ "POLLWRNORM", "POLLIN", "POLLOUT", - "POLLNVAL" + "POLLNVAL", + "POLLRDHUP" ], "structs": { "pollfd": [ @@ -140,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": [ @@ -181,7 +206,8 @@ "NI_NAMEREQD", "EAI_NONAME", "EAI_SOCKTYPE", - "EAI_BADFLAGS" + "EAI_BADFLAGS", + "EAI_AGAIN" ], "structs": { "addrinfo": [ diff --git a/src/struct_info_generated.json b/src/struct_info_generated.json index 2c67ff4412ecf..d367f40d77ec8 100644 --- a/src/struct_info_generated.json +++ b/src/struct_info_generated.json @@ -63,6 +63,7 @@ "EADV": 122, "EAFNOSUPPORT": 5, "EAGAIN": 6, + "EAI_AGAIN": -3, "EAI_BADFLAGS": -1, "EAI_FAMILY": -6, "EAI_NONAME": -2, @@ -260,6 +261,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, @@ -352,6 +366,7 @@ "POLLIN": 1, "POLLNVAL": 32, "POLLOUT": 4, + "POLLRDHUP": 8192, "POLLRDNORM": 64, "POLLWRNORM": 256, "PROT_WRITE": 2, @@ -1017,6 +1032,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 66d35c178bab2..c6d4f9b9e1359 100644 --- a/src/struct_info_generated_wasm64.json +++ b/src/struct_info_generated_wasm64.json @@ -63,6 +63,7 @@ "EADV": 122, "EAFNOSUPPORT": 5, "EAGAIN": 6, + "EAI_AGAIN": -3, "EAI_BADFLAGS": -1, "EAI_FAMILY": -6, "EAI_NONAME": -2, @@ -260,6 +261,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, @@ -352,6 +366,7 @@ "POLLIN": 1, "POLLNVAL": 32, "POLLOUT": 4, + "POLLRDHUP": 8192, "POLLRDNORM": 64, "POLLWRNORM": 256, "PROT_WRITE": 2, @@ -1017,6 +1032,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/emscripten.h b/system/include/emscripten/emscripten.h index 43d2f2899dd0e..2ee09f9f1a468 100644 --- a/system/include/emscripten/emscripten.h +++ b/system/include/emscripten/emscripten.h @@ -68,6 +68,24 @@ void emscripten_set_socket_connection_callback(void *userData, em_socket_callbac void emscripten_set_socket_message_callback(void *userData, em_socket_callback callback); void emscripten_set_socket_close_callback(void *userData, em_socket_callback callback); +// Asynchronous getaddrinfo. emscripten_dns_lookup_async() takes the same +// node/service/hints inputs as getaddrinfo() and returns a file descriptor that +// becomes readable once resolution completes, or -1 on error. Wait on it with +// poll/select/epoll, or without blocking by adding it to an epoll set and using +// emscripten_epoll_set_callback(). Once readable, call emscripten_dns_lookup_result() +// to read the outcome: 0 on success - writing the addrinfo list head to *res +// (free it with freeaddrinfo, as for getaddrinfo) - or an EAI_* code on failure. +// The caller owns the fd and should close() it. +// With -sNODERAWSOCKETS a hostname is resolved asynchronously via node:dns; +// otherwise resolution is synchronous (as getaddrinfo) and the fd is simply +// readable on the next turn. +struct addrinfo; +int emscripten_dns_lookup_async(const char *node, const char *service, const struct addrinfo *hints); +int emscripten_dns_lookup_result(int fd, struct addrinfo **res); + +// emscripten_epoll_set_callback (a non-blocking epoll readiness callback) lives +// in its own header, . + void _emscripten_push_main_loop_blocker(em_arg_callback_func func, void *arg, const char *name); void _emscripten_push_uncounted_main_loop_blocker(em_arg_callback_func func, void *arg, const char *name); #define emscripten_push_main_loop_blocker(func, arg) \ 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/lib/libc/musl/src/network/freeaddrinfo.c b/system/lib/libc/musl/src/network/freeaddrinfo.c index c4016d9f7c246..6e075a261adea 100644 --- a/system/lib/libc/musl/src/network/freeaddrinfo.c +++ b/system/lib/libc/musl/src/network/freeaddrinfo.c @@ -7,11 +7,15 @@ void freeaddrinfo(struct addrinfo *p) { #if __EMSCRIPTEN__ - // Emscripten's usage of this structure is very simple: we always allocate - // ai_addr, and do not use the linked list aspect at all. There is also no - // aliasing with aibuf. - free(p->ai_addr); - free(p); + // Emscripten allocates each node and its ai_addr separately (no aibuf block, + // no aliasing), and getaddrinfo may return a linked list, so walk it freeing + // each node and its address. + while (p) { + struct addrinfo *next = p->ai_next; + free(p->ai_addr); + free(p); + p = next; + } #else size_t cnt; for (cnt=1; p->ai_next; cnt++, p=p->ai_next); diff --git a/test/codesize/test_codesize_cxx_ctors1.json b/test/codesize/test_codesize_cxx_ctors1.json index 4285ad06aa31c..6fbaf43be773a 100644 --- a/test/codesize/test_codesize_cxx_ctors1.json +++ b/test/codesize/test_codesize_cxx_ctors1.json @@ -1,10 +1,10 @@ { - "a.out.js": 19005, - "a.out.js.gz": 7881, + "a.out.js": 19020, + "a.out.js.gz": 7891, "a.out.nodebug.wasm": 132603, "a.out.nodebug.wasm.gz": 49939, - "total": 151608, - "total_gz": 57820, + "total": 151623, + "total_gz": 57830, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_ctors2.json b/test/codesize/test_codesize_cxx_ctors2.json index d221613044e2b..858070994e640 100644 --- a/test/codesize/test_codesize_cxx_ctors2.json +++ b/test/codesize/test_codesize_cxx_ctors2.json @@ -1,10 +1,10 @@ { - "a.out.js": 18982, - "a.out.js.gz": 7866, + "a.out.js": 18997, + "a.out.js.gz": 7875, "a.out.nodebug.wasm": 132029, "a.out.nodebug.wasm.gz": 49599, - "total": 151011, - "total_gz": 57465, + "total": 151026, + "total_gz": 57474, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_except.json b/test/codesize/test_codesize_cxx_except.json index c0c7871f5774e..2cb741783b390 100644 --- a/test/codesize/test_codesize_cxx_except.json +++ b/test/codesize/test_codesize_cxx_except.json @@ -1,10 +1,10 @@ { - "a.out.js": 22980, - "a.out.js.gz": 8860, + "a.out.js": 22995, + "a.out.js.gz": 8869, "a.out.nodebug.wasm": 172523, "a.out.nodebug.wasm.gz": 57480, - "total": 195503, - "total_gz": 66340, + "total": 195518, + "total_gz": 66349, "sent": [ "__cxa_begin_catch", "__cxa_end_catch", diff --git a/test/codesize/test_codesize_cxx_except_wasm.json b/test/codesize/test_codesize_cxx_except_wasm.json index 8b3b4b4151867..8984aae196864 100644 --- a/test/codesize/test_codesize_cxx_except_wasm.json +++ b/test/codesize/test_codesize_cxx_except_wasm.json @@ -1,10 +1,10 @@ { - "a.out.js": 18804, - "a.out.js.gz": 7798, + "a.out.js": 18819, + "a.out.js.gz": 7807, "a.out.nodebug.wasm": 147928, "a.out.nodebug.wasm.gz": 55349, - "total": 166732, - "total_gz": 63147, + "total": 166747, + "total_gz": 63156, "sent": [ "_abort_js", "_tzset_js", diff --git a/test/codesize/test_codesize_cxx_except_wasm_legacy.json b/test/codesize/test_codesize_cxx_except_wasm_legacy.json index 10f73e875e3f8..8fad12c9577c5 100644 --- a/test/codesize/test_codesize_cxx_except_wasm_legacy.json +++ b/test/codesize/test_codesize_cxx_except_wasm_legacy.json @@ -1,10 +1,10 @@ { - "a.out.js": 18882, - "a.out.js.gz": 7824, + "a.out.js": 18897, + "a.out.js.gz": 7833, "a.out.nodebug.wasm": 145734, "a.out.nodebug.wasm.gz": 54976, - "total": 164616, - "total_gz": 62800, + "total": 164631, + "total_gz": 62809, "sent": [ "_abort_js", "_tzset_js", diff --git a/test/codesize/test_codesize_cxx_lto.json b/test/codesize/test_codesize_cxx_lto.json index 7e6b360d58d04..5ad9328299228 100644 --- a/test/codesize/test_codesize_cxx_lto.json +++ b/test/codesize/test_codesize_cxx_lto.json @@ -1,10 +1,10 @@ { - "a.out.js": 18345, - "a.out.js.gz": 7574, + "a.out.js": 18360, + "a.out.js.gz": 7584, "a.out.nodebug.wasm": 102090, "a.out.nodebug.wasm.gz": 39548, - "total": 120435, - "total_gz": 47122, + "total": 120450, + "total_gz": 47132, "sent": [ "a (emscripten_resize_heap)", "b (_setitimer_js)", diff --git a/test/codesize/test_codesize_cxx_mangle.json b/test/codesize/test_codesize_cxx_mangle.json index 9baddc1843609..88624f71675a5 100644 --- a/test/codesize/test_codesize_cxx_mangle.json +++ b/test/codesize/test_codesize_cxx_mangle.json @@ -1,10 +1,10 @@ { - "a.out.js": 23030, - "a.out.js.gz": 8882, + "a.out.js": 23045, + "a.out.js.gz": 8891, "a.out.nodebug.wasm": 238952, "a.out.nodebug.wasm.gz": 79833, - "total": 261982, - "total_gz": 88715, + "total": 261997, + "total_gz": 88724, "sent": [ "__cxa_begin_catch", "__cxa_end_catch", diff --git a/test/codesize/test_codesize_cxx_noexcept.json b/test/codesize/test_codesize_cxx_noexcept.json index 873b0f66064fe..3a6de8a6c78b6 100644 --- a/test/codesize/test_codesize_cxx_noexcept.json +++ b/test/codesize/test_codesize_cxx_noexcept.json @@ -1,10 +1,10 @@ { - "a.out.js": 19005, - "a.out.js.gz": 7881, + "a.out.js": 19020, + "a.out.js.gz": 7891, "a.out.nodebug.wasm": 134603, "a.out.nodebug.wasm.gz": 50779, - "total": 153608, - "total_gz": 58660, + "total": 153623, + "total_gz": 58670, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_file_preload.expected.js b/test/codesize/test_codesize_file_preload.expected.js index 8daa384197ad4..dd5df8a1f0735 100644 --- a/test/codesize/test_codesize_file_preload.expected.js +++ b/test/codesize/test_codesize_file_preload.expected.js @@ -1453,6 +1453,53 @@ var FS = { get isDevice() { return FS.isChrdev(this.mode); } + // The per-inode readiness wait-queue. The node carries a Set of listener + // entries {cb}; producers (SOCKFS, PIPEFS) call notifyListeners on a + // readiness transition, and poll()/epoll consume it. It lives on the node + // (not the fd) so dup'd fds share one queue. Only nodes that derive real + // readiness (sockets, pipes, and an epoll's own node) ever use this - + // always-ready types (regular files, ttys) never register or notify. + addListener(cb, exclusive = false) { + var entry = { + cb, + exclusive + }; + var listeners = (this.listeners ??= new Set); + listeners.add(entry); + return { + listeners, + entry + }; + } + notifyListeners(flags) { + // Iterates the set without copying, which is safe ONLY under a + // load-bearing contract that every internal listener must honour: + // 1. A listener must not run user code synchronously (a poll waiter only + // resolves a Promise; an epoll registration only re-lists + + // re-notifies; the epoll callback only schedules a tick). User code + // runs on a later tick, never inside this loop. + // 2. A listener may delete entries only from ITS OWN waiter, never from + // a sibling node's set that may be mid-iteration. (Deleting an entry + // of the set being iterated here is fine - a Set tolerates removal of + // a not-yet-visited entry mid-iteration; mutating a *different* node's + // set is fine because that set is not being iterated.) + // Violating either gives silently skipped wakeups that are near-impossible + // to reproduce. Any new producer/listener must preserve it. + if (!this.listeners) return; + // Fire every non-exclusive listener. Among EPOLLEXCLUSIVE registrations + // (one fd watched by several epolls) wake only one, rotating round-robin + // per node, to avoid a thundering herd. (Only epoll registrations are ever + // exclusive; poll waiters and a node's own consumers are not.) + var excl; + for (var entry of this.listeners) { + if (entry.exclusive) (excl ||= []).push(entry); else entry.cb(flags); + } + if (excl) { + var i = (this.exclTurn || 0) % excl.length; + this.exclTurn = i + 1; + excl[i].cb(flags); + } + } }, lookupPath(path, opts = {}) { if (!path) { diff --git a/test/codesize/test_codesize_file_preload.json b/test/codesize/test_codesize_file_preload.json index d904c76ae01c6..c25d0144c5dc6 100644 --- a/test/codesize/test_codesize_file_preload.json +++ b/test/codesize/test_codesize_file_preload.json @@ -1,10 +1,10 @@ { - "a.out.js": 22083, - "a.out.js.gz": 9172, + "a.out.js": 22098, + "a.out.js.gz": 9181, "a.out.nodebug.wasm": 1666, "a.out.nodebug.wasm.gz": 945, - "total": 23749, - "total_gz": 10117, + "total": 23764, + "total_gz": 10126, "sent": [ "a (fd_write)" ], diff --git a/test/codesize/test_codesize_files_js_fs.json b/test/codesize/test_codesize_files_js_fs.json index f6bd94e592e83..b1511fdcf9ccc 100644 --- a/test/codesize/test_codesize_files_js_fs.json +++ b/test/codesize/test_codesize_files_js_fs.json @@ -1,10 +1,10 @@ { - "a.out.js": 17653, - "a.out.js.gz": 7218, + "a.out.js": 17668, + "a.out.js.gz": 7226, "a.out.nodebug.wasm": 381, "a.out.nodebug.wasm.gz": 258, - "total": 18034, - "total_gz": 7476, + "total": 18049, + "total_gz": 7484, "sent": [ "a (fd_write)", "b (fd_read)", diff --git a/test/codesize/test_codesize_hello_O0.json b/test/codesize/test_codesize_hello_O0.json index 409fd6f1df9cb..8d8687db55acb 100644 --- a/test/codesize/test_codesize_hello_O0.json +++ b/test/codesize/test_codesize_hello_O0.json @@ -1,10 +1,10 @@ { - "a.out.js": 23503, - "a.out.js.gz": 8594, + "a.out.js": 23533, + "a.out.js.gz": 8606, "a.out.nodebug.wasm": 15115, "a.out.nodebug.wasm.gz": 7464, - "total": 38618, - "total_gz": 16058, + "total": 38648, + "total_gz": 16070, "sent": [ "fd_write" ], diff --git a/test/codesize/test_codesize_hello_dylink.json b/test/codesize/test_codesize_hello_dylink.json index f60caa0df139c..b28914ec31720 100644 --- a/test/codesize/test_codesize_hello_dylink.json +++ b/test/codesize/test_codesize_hello_dylink.json @@ -1,10 +1,10 @@ { - "a.out.js": 26005, - "a.out.js.gz": 11108, + "a.out.js": 26020, + "a.out.js.gz": 11116, "a.out.nodebug.wasm": 17861, "a.out.nodebug.wasm.gz": 9019, - "total": 43866, - "total_gz": 20127, + "total": 43881, + "total_gz": 20135, "sent": [ "__syscall_stat64", "emscripten_resize_heap", diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 5776bfea92a8e..f9240d9f2e2f2 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": 268000, - "a.out.nodebug.wasm": 587907, - "total": 855907, + "a.out.js": 271900, + "a.out.nodebug.wasm": 587930, + "total": 859830, "sent": [ "IMG_Init", "IMG_Load", @@ -454,7 +454,10 @@ "emscripten_date_now", "emscripten_debugger", "emscripten_destroy_worker", + "emscripten_dns_lookup_async", + "emscripten_dns_lookup_result", "emscripten_enter_soft_fullscreen", + "emscripten_epoll_set_callback", "emscripten_err", "emscripten_errn", "emscripten_exit_fullscreen", diff --git a/test/codesize/test_codesize_minimal_O0.expected.js b/test/codesize/test_codesize_minimal_O0.expected.js index f0d9c55e44901..5da5177b1a4c2 100644 --- a/test/codesize/test_codesize_minimal_O0.expected.js +++ b/test/codesize/test_codesize_minimal_O0.expected.js @@ -891,6 +891,8 @@ Module['FS_createPreloadedFile'] = FS.createPreloadedFile; 'inetNtop6', 'readSockaddr', 'writeSockaddr', + 'writeAddrInfoList', + 'getAddrInfo', 'readEmAsmArgs', 'jstoi_q', 'getExecutableName', diff --git a/test/codesize/test_codesize_minimal_O0.json b/test/codesize/test_codesize_minimal_O0.json index c3b9b8418bbc3..db75ed4fad484 100644 --- a/test/codesize/test_codesize_minimal_O0.json +++ b/test/codesize/test_codesize_minimal_O0.json @@ -1,10 +1,10 @@ { - "a.out.js": 18700, - "a.out.js.gz": 6771, + "a.out.js": 18730, + "a.out.js.gz": 6782, "a.out.nodebug.wasm": 1015, "a.out.nodebug.wasm.gz": 602, - "total": 19715, - "total_gz": 7373, + "total": 19745, + "total_gz": 7384, "sent": [], "imports": [], "exports": [ diff --git a/test/codesize/test_unoptimized_code_size.json b/test/codesize/test_unoptimized_code_size.json index 24a316623c3be..cd1b67c0eb5e0 100644 --- a/test/codesize/test_unoptimized_code_size.json +++ b/test/codesize/test_unoptimized_code_size.json @@ -1,16 +1,16 @@ { - "hello_world.js": 55706, - "hello_world.js.gz": 17578, + "hello_world.js": 55746, + "hello_world.js.gz": 17591, "hello_world.wasm": 15115, "hello_world.wasm.gz": 7464, "no_asserts.js": 25373, "no_asserts.js.gz": 8666, "no_asserts.wasm": 12229, "no_asserts.wasm.gz": 6004, - "strict.js": 52808, - "strict.js.gz": 16552, + "strict.js": 52848, + "strict.js.gz": 16564, "strict.wasm": 15115, "strict.wasm.gz": 7461, - "total": 176346, - "total_gz": 63725 + "total": 176426, + "total_gz": 63750 } 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.out b/test/core/test_epoll.out new file mode 100644 index 0000000000000..192e8c750c729 --- /dev/null +++ b/test/core/test_epoll.out @@ -0,0 +1 @@ +EPOLL PASS 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_advanced.out b/test/core/test_epoll_advanced.out new file mode 100644 index 0000000000000..051f18ce51b69 --- /dev/null +++ b/test/core/test_epoll_advanced.out @@ -0,0 +1 @@ +EPOLL ADVANCED PASS 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_noderawfs.out b/test/core/test_epoll_noderawfs.out new file mode 100644 index 0000000000000..ba1253af12aa4 --- /dev/null +++ b/test/core/test_epoll_noderawfs.out @@ -0,0 +1 @@ +EPOLL NODERAWFS PASS 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..496f06c17eb68 --- /dev/null +++ b/test/core/test_epoll_wait_and_callback.c @@ -0,0 +1,92 @@ +/* + * 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. + * + * Determinism comes from how the two consumers run when the wait-queue is poked: + * - the blocking wait's waiter runs synchronously in the producer's stack and + * drains the ready list immediately; + * - the callback only schedules a fresh tick, draining later. + * So the blocking wait always wins the edge that is ready at the instant it is + * woken; whatever became ready afterwards is left on the shared list for the + * callback's deferred tick. + */ + +#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 idx(int fd) { + for (int i = 0; i < 3; i++) if (rfd[i] == fd) return i; + return -1; +} + +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; + } + // The blocking wait took exactly one; the callback delivers the other two. + if (seen[0] && seen[1] && seen[2]) { + assert(emscripten_epoll_set_callback(ep, 8, 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]; + 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; + + // Return to the event loop; the callback (kept alive by its own keepalive) + // delivers the remaining two off the shared list and prints "done". + return 0; +} diff --git a/test/sockets/test_dns_async.c b/test/sockets/test_dns_async.c new file mode 100644 index 0000000000000..21c2a2cdbe6ee --- /dev/null +++ b/test/sockets/test_dns_async.c @@ -0,0 +1,124 @@ +/* + * 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 -sNODERAWSOCKETS DNS path. getaddrinfo() resolves numeric and + * /etc/hosts names synchronously (the latter read through emscripten's FS) and + * returns EAI_AGAIN for a real hostname. emscripten_dns_lookup_async() is the + * asynchronous getaddrinfo: it takes the same node/service/hints and returns a + * pollable fd; emscripten_dns_lookup_result() then yields the addrinfo payload. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int lookup_fd = -1; + +static void fail(const char* why) { + printf("DNS ASYNC FAIL: %s\n", why); + abort(); +} + +// getaddrinfo() of an AF_INET hostname, returning its first address (or *err). +static unsigned ipv4_of(const char* name, int* err_out) { + struct addrinfo hints = {0}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + struct addrinfo* res = NULL; + int err = getaddrinfo(name, NULL, &hints, &res); + if (err_out) *err_out = err; + if (err != 0) return 0; + unsigned addr = ((struct sockaddr_in*)res->ai_addr)->sin_addr.s_addr; + freeaddrinfo(res); + return addr; +} + +static void main_loop(void) { + fd_set fdr; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_SET(lookup_fd, &fdr); + select(lookup_fd + 1, &fdr, NULL, NULL, &tv); + if (!FD_ISSET(lookup_fd, &fdr)) { + return; // resolution still pending + } + + // The result is delivered directly as an addrinfo payload, in the same format + // getaddrinfo() produces (and freed the same way). + struct addrinfo* res = NULL; + int result = emscripten_dns_lookup_result(lookup_fd, &res); + close(lookup_fd); + if (result != 0) fail("async lookup failed"); + assert(res); + if (res->ai_socktype != SOCK_STREAM) fail("async result lost the requested socktype"); + unsigned addr = ((struct sockaddr_in*)res->ai_addr)->sin_addr.s_addr; + freeaddrinfo(res); + if (addr != htonl(INADDR_LOOPBACK)) fail("localhost did not resolve to 127.0.0.1"); + + printf("DNS ASYNC PASS\n"); + emscripten_cancel_main_loop(); +} + +int main(void) { + // Seed /etc/hosts (through emscripten's FS) with names node:dns could never + // resolve, including one mapped to multiple addresses. + mkdir("/etc", 0777); + FILE* f = fopen("/etc/hosts", "w"); + assert(f); + fputs("# test hosts\n" + "10.1.2.3 statichost.test\n" + "192.0.2.1 multi.test\n" + "192.0.2.2 multi.test\n", + f); + fclose(f); + + // /etc/hosts resolves synchronously through getaddrinfo. + int err = 0; + unsigned static_addr = ipv4_of("statichost.test", &err); + if (err != 0) fail("static host not resolved from /etc/hosts"); + if (static_addr != inet_addr("10.1.2.3")) fail("static host wrong address"); + + // A name with several addresses comes back as an addrinfo linked list, freed + // as a whole by freeaddrinfo. + struct addrinfo mhints = {0}; + mhints.ai_family = AF_INET; + mhints.ai_socktype = SOCK_STREAM; + struct addrinfo* mres = NULL; + if (getaddrinfo("multi.test", NULL, &mhints, &mres) != 0) fail("multi host not resolved"); + int count = 0, seen1 = 0, seen2 = 0; + for (struct addrinfo* ai = mres; ai; ai = ai->ai_next) { + unsigned a = ((struct sockaddr_in*)ai->ai_addr)->sin_addr.s_addr; + if (a == inet_addr("192.0.2.1")) seen1 = 1; + if (a == inet_addr("192.0.2.2")) seen2 = 1; + count++; + } + freeaddrinfo(mres); + if (count != 2 || !seen1 || !seen2) fail("multi host did not return both addresses"); + + // A real hostname not in /etc/hosts has no synchronous resolution. + ipv4_of("localhost", &err); + if (err != EAI_AGAIN) fail("real hostname should be EAI_AGAIN"); + + // Resolve it asynchronously; the result arrives via the pollable fd. + struct addrinfo hints = {0}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + lookup_fd = emscripten_dns_lookup_async("localhost", NULL, &hints); + if (lookup_fd < 0) fail("async lookup did not return an fd"); + + emscripten_set_main_loop(main_loop, 0, 0); + return 0; +} diff --git a/test/sockets/test_dns_async_default.c b/test/sockets/test_dns_async_default.c new file mode 100644 index 0000000000000..543d3ae033dbb --- /dev/null +++ b/test/sockets/test_dns_async_default.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. + * + * The async getaddrinfo API is available without -sNODERAWSOCKETS too: there it + * resolves synchronously (the same fake address getaddrinfo() returns) and the + * fd is simply readable on the next turn, so integration code need not branch on + * the backend. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int lookup_fd = -1; + +static void fail(const char* why) { + printf("DNS ASYNC DEFAULT FAIL: %s\n", why); + abort(); +} + +static void main_loop(void) { + fd_set fdr; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_SET(lookup_fd, &fdr); + select(lookup_fd + 1, &fdr, NULL, NULL, &tv); + if (!FD_ISSET(lookup_fd, &fdr)) { + return; + } + + struct addrinfo* res = NULL; + int result = emscripten_dns_lookup_result(lookup_fd, &res); + close(lookup_fd); + if (result != 0) fail("async lookup failed"); + assert(res && res->ai_addr); + if (res->ai_socktype != SOCK_STREAM) fail("async result lost the requested socktype"); + freeaddrinfo(res); + + printf("DNS ASYNC DEFAULT PASS\n"); + emscripten_cancel_main_loop(); +} + +int main(void) { + struct addrinfo hints = {0}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + lookup_fd = emscripten_dns_lookup_async("example.com", NULL, &hints); + if (lookup_fd < 0) fail("async lookup did not return an fd"); + + emscripten_set_main_loop(main_loop, 0, 0); + return 0; +} diff --git a/test/sockets/test_dns_async_net.c b/test/sockets/test_dns_async_net.c new file mode 100644 index 0000000000000..a473161b30ccd --- /dev/null +++ b/test/sockets/test_dns_async_net.c @@ -0,0 +1,67 @@ +/* + * 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. + * + * Asynchronous getaddrinfo over the real network with -sNODERAWSOCKETS: a real + * public hostname has no synchronous resolution (EAI_AGAIN), then resolves via + * emscripten_dns_lookup_async(), whose result is delivered as an addrinfo + * payload. This hits the real network (like test_getaddrinfo). + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char* HOST = "google.com"; + +static int lookup_fd = -1; + +static void fail(const char* why) { + printf("DNS ASYNC NET FAIL: %s\n", why); + abort(); +} + +static void main_loop(void) { + fd_set fdr; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_SET(lookup_fd, &fdr); + select(lookup_fd + 1, &fdr, NULL, NULL, &tv); + if (!FD_ISSET(lookup_fd, &fdr)) { + return; // resolution still in flight + } + + struct addrinfo* res = NULL; + int result = emscripten_dns_lookup_result(lookup_fd, &res); + close(lookup_fd); + if (result != 0) fail("async lookup failed"); + assert(res && res->ai_addr); + freeaddrinfo(res); + + printf("DNS ASYNC NET PASS\n"); + emscripten_cancel_main_loop(); +} + +int main(void) { + struct addrinfo hints = {0}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + // No synchronous DNS without JSPI: the name is EAI_AGAIN up front. + struct addrinfo* res = NULL; + if (getaddrinfo(HOST, NULL, &hints, &res) != EAI_AGAIN) fail("host should be EAI_AGAIN"); + + lookup_fd = emscripten_dns_lookup_async(HOST, NULL, &hints); + if (lookup_fd < 0) fail("async lookup did not return an fd"); + + emscripten_set_main_loop(main_loop, 0, 0); + return 0; +} diff --git a/test/sockets/test_dns_callback.c b/test/sockets/test_dns_callback.c new file mode 100644 index 0000000000000..b444aa04916dc --- /dev/null +++ b/test/sockets/test_dns_callback.c @@ -0,0 +1,65 @@ +/* + * 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 fd returned by emscripten_dns_lookup_async() becomes readable once the + * lookup completes. It is added to an epoll set and emscripten_epoll_set_callback() + * awaits it without blocking, driving the lookup purely through that callback + * with no main loop or blocking poll/select. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int ep; + +static void fail(const char* why) { + printf("DNS CALLBACK FAIL: %s\n", why); + abort(); +} + +static void on_ready(int epfd, struct epoll_event* events, int nready, void* ud) { + if (nready != 1 || !(events[0].events & EPOLLIN)) fail("lookup fd did not become readable"); + int fd = events[0].data.fd; + + struct addrinfo* res = NULL; + int result = emscripten_dns_lookup_result(fd, &res); + close(fd); + close(ep); + if (result != 0) fail("async lookup failed"); + assert(res); + unsigned addr = ((struct sockaddr_in*)res->ai_addr)->sin_addr.s_addr; + freeaddrinfo(res); + if (addr != htonl(INADDR_LOOPBACK)) fail("localhost did not resolve to 127.0.0.1"); + + printf("DNS CALLBACK PASS\n"); +} + +int main(void) { + struct addrinfo hints = {0}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + int lookup_fd = emscripten_dns_lookup_async("localhost", NULL, &hints); + if (lookup_fd < 0) fail("async lookup did not return an fd"); + + ep = epoll_create1(0); + struct epoll_event ev = { .events = EPOLLIN }; + ev.data.fd = lookup_fd; + if (epoll_ctl(ep, EPOLL_CTL_ADD, lookup_fd, &ev) != 0) fail("epoll_ctl add"); + + // The armed callback keeps the runtime alive; on_ready fires when the lookup + // fd becomes readable. Closing the epoll fd in the callback ends the interest. + emscripten_epoll_set_callback(ep, 1, on_ready, NULL); + return 0; +} diff --git a/test/sockets/test_dns_jspi.c b/test/sockets/test_dns_jspi.c new file mode 100644 index 0000000000000..f88bd6f0ab994 --- /dev/null +++ b/test/sockets/test_dns_jspi.c @@ -0,0 +1,38 @@ +/* + * 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. + * + * With -sNODERAWSOCKETS under JSPI, getaddrinfo() of a real public hostname + * blocks on the node:dns lookup by suspending the wasm stack, and resolves + * directly - no EAI_AGAIN + async prewarm + retry needed. This resolves over + * the real network. + */ + +#include +#include +#include +#include +#include +#include + +static const char* HOST = "google.com"; + +int main(void) { + struct addrinfo hints = {0}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + struct addrinfo* res = NULL; + int err = getaddrinfo(HOST, NULL, &hints, &res); + if (err != 0) { + printf("DNS JSPI FAIL: getaddrinfo err=%d\n", err); + return 1; + } + assert(res); + freeaddrinfo(res); + + printf("DNS JSPI PASS\n"); + 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 27f3423460df7..8b1bef1a693f9 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -5755,6 +5755,95 @@ def test_poll(self): self.set_setting('FORCE_FILESYSTEM') self.do_core_test('test_poll.c') + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + def test_epoll(self): + self.set_setting('FORCE_FILESYSTEM') + self.do_core_test('test_epoll.c') + + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + def test_epoll_advanced(self): + self.set_setting('FORCE_FILESYSTEM') + self.do_core_test('test_epoll_advanced.c') + + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + def test_epoll_fairness(self): + # More ready fds than maxevents: successive waits rotate (round-robin) so no + # fd starves. + self.set_setting('FORCE_FILESYSTEM') + self.do_runf('core/test_epoll_fairness.c', 'done\n') + + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + @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.set_setting('NODERAWFS') + self.do_core_test('test_epoll_noderawfs.c') + + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + 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.set_setting('FORCE_FILESYSTEM') + self.set_setting('EXIT_RUNTIME') + self.do_runf('core/test_epoll_callback.c', 'done\n') + + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + 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.set_setting('FORCE_FILESYSTEM') + self.set_setting('EXIT_RUNTIME') + self.do_runf('core/test_epoll_callback_overflow.c', 'done\n') + + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + def test_epoll_callback_replace(self): + # A second register replaces the callback (no stacking); a NULL callback + # unregisters regardless of maxevents. + self.set_setting('FORCE_FILESYSTEM') + self.set_setting('EXIT_RUNTIME') + self.do_runf('core/test_epoll_callback_replace.c', 'done\n') + + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + 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.set_setting('FORCE_FILESYSTEM') + self.set_setting('EXIT_RUNTIME') + self.do_runf('core/test_epoll_callback_close.c', 'done\n') + + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + def test_epoll_callback_nested(self): + # A callback on an outer epoll fires when a leaf edge propagates through an + # inner (nested) epoll. + self.set_setting('FORCE_FILESYSTEM') + self.set_setting('EXIT_RUNTIME') + self.do_runf('core/test_epoll_callback_nested.c', 'done\n') + + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + 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.set_setting('FORCE_FILESYSTEM') + self.set_setting('EXIT_RUNTIME') + self.do_runf('core/test_epoll_callback_nested_close.c', 'done\n') + + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + 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.set_setting('FORCE_FILESYSTEM') + self.set_setting('EXIT_RUNTIME') + self.do_runf('core/test_epoll_callback_edge.c', 'done\n') + + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + 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.set_setting('FORCE_FILESYSTEM') + self.set_setting('EXIT_RUNTIME') + self.do_runf('core/test_epoll_callback_level.c', 'done\n') + @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') @@ -9672,6 +9761,25 @@ 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 + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + 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.set_setting('FORCE_FILESYSTEM') + self.do_runf('core/test_epoll_blocking_asyncify.c', 'done\n') + + @with_asyncify_and_jspi + @no_wasmfs('epoll is implemented in the JS (non-WASMFS) syscall layer') + 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.set_setting('FORCE_FILESYSTEM') + self.set_setting('EXIT_RUNTIME') + self.do_runf('core/test_epoll_wait_and_callback.c', 'done\n') + @parameterized({ '': ([],), 'pthread': (['-pthread'],), diff --git a/test/test_sockets.py b/test/test_sockets.py index 53f5bf9d90c29..3764befe602d4 100644 --- a/test/test_sockets.py +++ b/test/test_sockets.py @@ -475,12 +475,108 @@ 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_dns_async(self): + # getaddrinfo() resolves numeric and /etc/hosts names (read via emscripten's + # FS) synchronously and returns EAI_AGAIN for a real hostname. + # emscripten_dns_lookup_async() is the async getaddrinfo: a pollable fd whose + # emscripten_dns_lookup_result() yields the addrinfo payload directly. + self.do_runf('sockets/test_dns_async.c', 'DNS ASYNC PASS', cflags=['-sNODERAWSOCKETS']) + + @also_with_proxy_to_pthread + def test_noderawsockets_dns_callback(self): + # The async lookup fd becomes readable on completion; it is added to an epoll + # set and emscripten_epoll_set_callback() awaits it without blocking, driving the + # lookup purely via its callback, with no main loop or blocking poll/select. + # EXIT_RUNTIME so the proxy_to_pthread variant tears down cleanly once the + # callback's keepalive is released (otherwise the worker lingers). + self.do_runf('sockets/test_dns_callback.c', 'DNS CALLBACK PASS', cflags=['-sNODERAWSOCKETS', '-sFORCE_FILESYSTEM', '-sEXIT_RUNTIME']) + + def test_noderawsockets_dns_async_net(self): + # A real public hostname is EAI_AGAIN synchronously, then resolves via the + # async getaddrinfo, whose result is delivered as an addrinfo payload. This + # hits the real network (like test_getaddrinfo). + self.do_runf('sockets/test_dns_async_net.c', 'DNS ASYNC NET PASS', cflags=['-sNODERAWSOCKETS']) + + def test_dns_async_default(self): + # The async getaddrinfo API is available without -sNODERAWSOCKETS, resolving + # synchronously (the same fake address getaddrinfo() returns) and delivering + # it via the pollable fd. + self.do_runf('sockets/test_dns_async_default.c', 'DNS ASYNC DEFAULT PASS') + + @also_with_proxy_to_pthread + def test_noderawsockets_dns_jspi(self): + # Under JSPI, getaddrinfo() of a real public hostname blocks on the + # node:dns lookup (suspending the wasm stack) and resolves directly, + # without the EAI_AGAIN + async retry needed in non-JSPI builds. This + # hits the real network (like test_getaddrinfo). 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_dns_jspi.c', 'DNS JSPI PASS', cflags=['-sNODERAWSOCKETS']) + @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