• Win32: the PREFER_POLL code paths call WSAPoll() in ways it rejects (W

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Sun Aug 2 19:12:37 2026
    open https://gitlab.synchro.net/main/sbbs/-/issues/1212

    ## Summary

    `PREFER_POLL` is defined only for non-Win32 builds — `src/build/Common.gmake:272`, added in 663ca552e6 (2021-03-31), which replaced an earlier `#ifdef _WIN32` split. Windows therefore compiles the `select()` half of every `#ifdef PREFER_POLL` in the tree, and the `poll()` half has never been compiled or executed there, even though `src/xpdev/sockwrap.h:187` maps `poll()` → `WSAPoll()` and defines `nfds_t`/`pollfd` for exactly that purpose.

    This came up because the Win32 `select()` path has a hard 64-socket ceiling (`FD_SETSIZE` defaults to 64 in `winsock2.h`, and nothing in the tree raised it). In `js_internal.cpp`'s JS event loop, `FD_SET()` silently discards every socket past the 64th, and since events are inserted at the head of the list (`js_internal.cpp:784`) while the fill loop walks it forward, the **oldest** event is the first one dropped — for a JS server that's the listening socket registered at startup. Result: a Win32 JS ircd hub with ≥64 sockets stops accepting new connections entirely while every established connection keeps working. That was breaking vert.synchro.net (57 direct server links); the kernel still completes the TCP handshake from the listen backlog, so it presents as a network problem rather than a server bug.

    Raising `FD_SETSIZE` for the MSBuild projects fixes that, and is what I'd like to ship for the immediate problem. Switching Windows to `poll()` is the other candidate fix, and is arguably the right long-term answer — but the code isn't ready for it. This issue records what's in the way, so nobody flips the define expecting it to just work.

    ## Confirmed problems

    Probed on Windows 11 26200, MSVC 19.44, against the real `WSAPoll()`:

    **1. Empty descriptor set → `WSAEINVAL`.** `js_internal.cpp:1334` calls `poll(fds, cfd, timeout)` with `cfd == 0` (and `fds == NULL`) whenever a script has only timer events pending — the `sc == 0` case at `js_internal.cpp:1202`. On \*nix that's a portable sleep. On Windows it fails immediately, falls into the error branch at `js_internal.cpp:1356-1362`, and `JS_ReportError()`s the event loop dead. Any JS using `setTimeout`/`setInterval` without sockets would break.

    **2. `POLLPRI` is not supported.** `js_global.cpp:4157` and `js_global.cpp:4182` set `events = POLLPRI` for the except-set of `socket_select()`. `WSAPoll()` rejects it outright rather than degrading.

    ```
    1. WSAPoll(NULL, 0, 100) = -1 err=10022 <- WSAEINVAL
    2. WSAPoll(events=POLLPRI) = -1 err=10022 <- WSAEINVAL
    3. WSAPoll(listener, POLLIN) = 1 revents=0x100 <- POLLRDNORM, correct
    4. WSAPoll(100 fds) = 1 listener revents=0x100
    ```

    Cases 3 and 4 are the point of the exercise and they work: a listening socket with a pending connection is correctly reported readable, and >64 descriptors in a single call are fine. The mechanism is sound on Windows; only these two call sites are wrong.

    ## The documented connect bug did not reproduce

    `sockwrap.h:183` warns:

    NOTE: WSAPoll() has a bug where a non-blocking socket which has connect() called on it that is trying to connect to a closed port will timeout instead of returning a failure, even with POLLOUT specified.

    Non-blocking `connect()` to a closed loopback port, `WSAPoll()` vs `select()`:

    ```
    WSAPoll(POLLOUT) after 2016 ms: ret=1 revents=0x13 (POLLERR|POLLHUP|POLLOUT) select() after 2016 ms: ret=1 writable=0 except=1
    ```

    Identical timing, and `WSAPoll()` did report the failure. That is **one** scenario (RST-refused, loopback) on **one** OS build, so it is not evidence the note is obsolete — a dropped-SYN/firewalled destination was not tested, and older Windows versions are still supported. Recording it only so the next person knows the note needs re-verification rather than assuming it holds.

    ## If someone enables PREFER_POLL on Win32

    - Skip the `poll()` call when `nfds == 0` and sleep for the timeout instead.
    - Give `POLLPRI` a Windows substitute, or let the except-set path degrade.
    - Re-verify the `sockwrap.h:183` connect behavior on a dropped-SYN destination and on older Windows.
    - Note the blast radius: defining `PREFER_POLL` project-wide also flips `multisock.c`'s `xpms_accept()` — the accept path for *every* Windows server — plus the `sockwrap` helpers and `js_socket`, not just the JS engine.

    ## Probe source

    <details>
    <summary>wsapoll_test.c — cases 1-4</summary>

    ```c
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #include <stdio.h>

    #pragma comment(lib, "ws2_32.lib")

    int main(void)
    {
    WSADATA wd;
    WSAPOLLFD pfd;
    SOCKET lsock, csock;
    struct sockaddr_in sa;
    int len, r;
    u_long nb = 1;

    WSAStartup(MAKEWORD(2, 2), &wd);

    /* 1. Empty set - js_internal.cpp calls poll(NULL, 0, timeout) whenever a
    script has only timer events pending. */
    r = WSAPoll(NULL, 0, 100);
    printf("1. WSAPoll(NULL, 0, 100) = %d err=%d\n", r, WSAGetLastError());

    lsock = socket(AF_INET, SOCK_STREAM, 0);
    memset(&sa, 0, sizeof sa);
    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    sa.sin_port = 0;
    bind(lsock, (struct sockaddr *)&sa, sizeof sa);
    listen(lsock, 5);
    len = sizeof sa;
    getsockname(lsock, (struct sockaddr *)&sa, &len);

    /* 2. POLLPRI in events - js_global.cpp:4157,4182 use this for the
    except-set of socket_select(). */
    pfd.fd = lsock;
    pfd.events = POLLPRI;
    pfd.revents = 0;
    r = WSAPoll(&pfd, 1, 100);
    printf("2. WSAPoll(events=POLLPRI) = %d err=%d revents=0x%x\n",
    r, WSAGetLastError(), pfd.revents);

    /* 3. POLLIN on a listening socket with a pending connection - this is
    what an accept callback needs. */
    csock = socket(AF_INET, SOCK_STREAM, 0);
    ioctlsocket(csock, FIONBIO, &nb);
    connect(csock, (struct sockaddr *)&sa, sizeof sa);
    Sleep(200);
    pfd.fd = lsock;
    pfd.events = POLLIN;
    pfd.revents = 0;
    r = WSAPoll(&pfd, 1, 1000);
    printf("3. WSAPoll(listener, POLLIN) = %d err=%d revents=0x%x (POLLRDNORM=0x%x)\n",
    r, WSAGetLastError(), pfd.revents, POLLRDNORM);

    /* 4. 100 sockets in one call - the case select() cannot express. */
    {
    WSAPOLLFD many[100];
    SOCKET pad[100];
    int i;
    for (i = 0; i < 100; i++) {
    pad[i] = socket(AF_INET, SOCK_DGRAM, 0);
    memset(&sa, 0, sizeof sa);
    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    sa.sin_port = 0;
    bind(pad[i], (struct sockaddr *)&sa, sizeof sa);
    many[i].fd = pad[i];
    many[i].events = POLLIN;
    many[i].revents = 0;
    }
    many[99].fd = lsock; /* listener last, still has a pending connect */
    r = WSAPoll(many, 100, 1000);
    printf("4. WSAPoll(100 fds) = %d err=%d listener revents=0x%x\n",
    r, WSAGetLastError(), many[99].revents);
    }

    return 0;
    }
    ```

    </details>

    <details>
    <summary>wsapoll_connect.c — the sockwrap.h:183 check</summary>

    ```c
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #include <stdio.h>

    #pragma comment(lib, "ws2_32.lib")

    static SOCKET start_connect(void)
    {
    SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in sa;
    u_long nb = 1;

    ioctlsocket(s, FIONBIO, &nb);
    memset(&sa, 0, sizeof sa);
    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    sa.sin_port = htons(1); /* nothing listens here */
    connect(s, (struct sockaddr *)&sa, sizeof sa);
    return s;
    }

    int main(void)
    {
    WSADATA wd;
    WSAPOLLFD pfd;
    SOCKET s;
    fd_set wfds, efds;
    struct timeval tv;
    DWORD t0;
    int r;

    WSAStartup(MAKEWORD(2, 2), &wd);

    s = start_connect();
    pfd.fd = s;
    pfd.events = POLLOUT;
    pfd.revents = 0;
    t0 = GetTickCount();
    r = WSAPoll(&pfd, 1, 3000);
    printf("WSAPoll(POLLOUT) after %lu ms: ret=%d err=%d revents=0x%x\n",
    (unsigned long)(GetTickCount() - t0), r, WSAGetLastError(), pfd.revents);
    closesocket(s);

    s = start_connect();
    FD_ZERO(&wfds);
    FD_ZERO(&efds);
    FD_SET(s, &wfds);
    FD_SET(s, &efds);
    tv.tv_sec = 3;
    tv.tv_usec = 0;
    t0 = GetTickCount();
    r = select(0, NULL, &wfds, &efds, &tv);
    printf("select() after %lu ms: ret=%d writable=%d except=%d\n",
    (unsigned long)(GetTickCount() - t0), r,
    FD_ISSET(s, &wfds) ? 1 : 0, FD_ISSET(s, &efds) ? 1 : 0);
    closesocket(s);

    return 0;
    }
    ```

    </details>

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Deucе@1:103/705 to GitLab note in main/sbbs on Sun Aug 2 19:24:20 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1212#note_9896

    IIRC, The main issue with WSAPoll() (and why we didn't switch Windows to use it) is that it's implemented using the Windows select() so has the same limits and worse, does not fully match the POSIX poll() semantics.
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Deucе@1:103/705 to GitLab note in main/sbbs on Sun Aug 2 19:25:27 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1212#note_9897

    See [this page](https://learn.microsoft.com/en-us/windows/win32/winsock/maximum-number-of-sockets-supported-2) for details.
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Sun Aug 2 19:57:21 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1212#note_9899

    The semantics point is right, and it's why this issue recommends against flipping the define rather than for it — `WSAPoll(NULL, 0, t)` returns `WSAEINVAL` where POSIX `poll()` treats it as a sleep, and `POLLPRI` is rejected outright.

    On the capacity point I couldn't reproduce a 64-descriptor limit. Probe built with the **default** `FD_SETSIZE` (64), polling 512 descriptors where the only readable one is placed last:

    ```
    FD_SETSIZE = 64 (default)
    WSAPoll(512 fds) = 1 err=0 first ready index = 511 (listener is 511)
    ```

    If `WSAPoll()` were backed by a 64-entry `fd_set`, index 511 could not be reported at all. Whatever it does internally, its observable capacity isn't `FD_SETSIZE`.

    Tested only on Windows 11 26200 (MSVC 19.44) — if the recollection is from Win7/Win8-era `WSAPoll()`, it may well have been accurate then and changed since. That's its own argument for keeping Windows on `select()` while older versions are supported, which is where this issue lands anyway.

    Worth noting the linked page is also what sanctions the `FD_SETSIZE` fix that just landed in 031df44f62:

    If an application is designed to be capable of working with more than 64 sockets using the **select** and **WSAPoll** functions, the implementor should define the manifest **FD_SETSIZE** in every source file before including the *Winsock2.h* header file. One way of doing this may be to include the definition within the compiler options in the makefile. For example, you could add "-DFD_SETSIZE=128" as an option to the compiler command line for Microsoft C++.

    and that it puts the provider's own ceiling at available memory, with `FD_SETSIZE` affecting "only … the **FD_XXX** macros". The `FD_SET and select` heading grouping `WSAPoll` in with `select` is probably the source of the confusion — `WSAPoll()` doesn't take an `fd_set` at all.

    <details>
    <summary>wsapoll_scale.c — the 512-descriptor probe</summary>

    ```c
    /* Is WSAPoll() itself limited to FD_SETSIZE (64) descriptors, as it would be
    if it were implemented over Windows select()?
    Built WITHOUT redefining FD_SETSIZE, so FD_SETSIZE is the default 64. */ #include <winsock2.h>
    #include <ws2tcpip.h>
    #include <stdio.h>

    #pragma comment(lib, "ws2_32.lib")

    #define N 512

    int main(void)
    {
    WSADATA wd;
    WSAPOLLFD fds[N];
    SOCKET pad[N], lsock, csock;
    struct sockaddr_in sa;
    u_long nb = 1;
    int i, len, r, ready_idx = -1;

    WSAStartup(MAKEWORD(2, 2), &wd);
    printf("FD_SETSIZE = %d (default)\n", (int)FD_SETSIZE);

    /* N-1 permanently-quiet UDP sockets */
    for (i = 0; i < N - 1; i++) {
    pad[i] = socket(AF_INET, SOCK_DGRAM, 0);
    memset(&sa, 0, sizeof sa);
    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    sa.sin_port = 0;
    if (bind(pad[i], (struct sockaddr *)&sa, sizeof sa) != 0) {
    printf("bind failed at %d, err=%d\n", i, WSAGetLastError());
    return 1;
    }
    fds[i].fd = pad[i];
    fds[i].events = POLLIN;
    fds[i].revents = 0;
    }

    /* the ONLY readable descriptor, placed last - far beyond 64 */
    lsock = socket(AF_INET, SOCK_STREAM, 0);
    memset(&sa, 0, sizeof sa);
    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    sa.sin_port = 0;
    bind(lsock, (struct sockaddr *)&sa, sizeof sa);
    listen(lsock, 5);
    len = sizeof sa;
    getsockname(lsock, (struct sockaddr *)&sa, &len);
    csock = socket(AF_INET, SOCK_STREAM, 0);
    ioctlsocket(csock, FIONBIO, &nb);
    connect(csock, (struct sockaddr *)&sa, sizeof sa);
    Sleep(300);

    fds[N - 1].fd = lsock;
    fds[N - 1].events = POLLIN;
    fds[N - 1].revents = 0;

    r = WSAPoll(fds, N, 2000);
    for (i = 0; i < N; i++) {
    if (fds[i].revents != 0) {
    ready_idx = i;
    break;
    }
    }
    printf("WSAPoll(%d fds) = %d err=%d first ready index = %d (listener is %d)\n",
    N, r, WSAGetLastError(), ready_idx, N - 1);

    return 0;
    }
    ```

    </details>

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Sun Aug 2 20:05:52 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1212#note_9901

    Closing — with the descriptor ceiling gone there's no remaining reason to want `poll()` on Windows.

    Capacity was the only advantage on offer, and 031df44f62 removed it by defining `FD_SETSIZE=1024` for every MSBuild project. `WSAPoll()` is O(n) over its array just as Winsock's `select()` is over `fd_count`, so there's no performance case either. What's left is all cost: the two semantic divergences above, unverified behavior on the older Windows versions still supported, and a blast radius covering `multisock.c`'s `xpms_accept()` — every Windows server's accept loop.

    Leaving the findings here in case someone revisits it: if Windows is ever moved to `poll()`, the `nfds == 0` and `POLLPRI` call sites have to be fixed first, and the `sockwrap.h:183` connect-failure note needs re-verification on a dropped-SYN destination rather than an RST-refused one.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Sun Aug 2 20:05:52 2026
    close https://gitlab.synchro.net/main/sbbs/-/issues/1212
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Sun Aug 2 20:12:57 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1212#note_9902

    Correction to the note above: I claimed capacity was no longer an advantage of `poll()`. That was wrong, and reopening.

    `FD_SETSIZE` **raised** the ceiling from 64 to 1024; it didn't remove it. `fd_set` is a fixed `SOCKET fd_array[FD_SETSIZE]`, so `select()`'s limit is a compile-time constant. `poll()`'s array is caller-allocated with `nfds` passed at runtime — `js_internal.cpp:1203` already sizes it with `calloc(sc, sizeof(*fds))` — so it's bounded by memory, matching the linked page's "limited only by available memory."

    That difference is real on Windows, not just on paper. Same probe as before, built with the **default** `FD_SETSIZE` (64), 2048 descriptors with only the last one readable:

    ```
    FD_SETSIZE = 64 (default)
    WSAPoll(2048 fds) = 1 err=0 first ready index = 2047 (listener is 2047)
    ```

    So the accurate framing is a tradeoff rather than an absence of advantage:

    - `select()` with `FD_SETSIZE=1024` is ~10x the busiest known case (vert's IRCd hub peaks around 60 sockets in one context), and raising it further is a one-line change. The cost is stack rather than time — `FD_ZERO` is just `fd_count=0` (`winsock2.h:173`), so a large set adds no per-call work, but `js_global.cpp:3961` holds three `fd_set`s on the stack and `js_internal.cpp` two. At 1024 that's ~12KB/~8KB on Win32; at 8192 it would be ~98KB/~64KB, which starts to matter on a default thread stack.
    - `poll()` has no such ceiling at all, but costs the two `WSAEINVAL` call sites documented above, unverified behavior on the older Windows versions still supported, and a change reaching `multisock.c`'s `xpms_accept()` — every Windows server's accept loop.

    Nothing in the tree is close to 1024 sockets in a single JS context, so there's no reason to take that on now. But it's a bounded-vs-unbounded difference rather than a settled non-issue, so this stays open with the prerequisites recorded.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Sun Aug 2 20:12:58 2026
    reopen https://gitlab.synchro.net/main/sbbs/-/issues/1212

    ## Summary

    `PREFER_POLL` is defined only for non-Win32 builds — `src/build/Common.gmake:272`, added in 663ca552e6 (2021-03-31), which replaced an earlier `#ifdef _WIN32` split. Windows therefore compiles the `select()` half of every `#ifdef PREFER_POLL` in the tree, and the `poll()` half has never been compiled or executed there, even though `src/xpdev/sockwrap.h:187` maps `poll()` → `WSAPoll()` and defines `nfds_t`/`pollfd` for exactly that purpose.

    This came up because the Win32 `select()` path has a hard 64-socket ceiling (`FD_SETSIZE` defaults to 64 in `winsock2.h`, and nothing in the tree raised it). In `js_internal.cpp`'s JS event loop, `FD_SET()` silently discards every socket past the 64th, and since events are inserted at the head of the list (`js_internal.cpp:784`) while the fill loop walks it forward, the **oldest** event is the first one dropped — for a JS server that's the listening socket registered at startup. Result: a Win32 JS ircd hub with ≥64 sockets stops accepting new connections entirely while every established connection keeps working. That was breaking vert.synchro.net (57 direct server links); the kernel still completes the TCP handshake from the listen backlog, so it presents as a network problem rather than a server bug.

    Raising `FD_SETSIZE` for the MSBuild projects fixes that, and is what I'd like to ship for the immediate problem. Switching Windows to `poll()` is the other candidate fix, and is arguably the right long-term answer — but the code isn't ready for it. This issue records what's in the way, so nobody flips the define expecting it to just work.

    ## Confirmed problems

    Probed on Windows 11 26200, MSVC 19.44, against the real `WSAPoll()`:

    **1. Empty descriptor set → `WSAEINVAL`.** `js_internal.cpp:1334` calls `poll(fds, cfd, timeout)` with `cfd == 0` (and `fds == NULL`) whenever a script has only timer events pending — the `sc == 0` case at `js_internal.cpp:1202`. On \*nix that's a portable sleep. On Windows it fails immediately, falls into the error branch at `js_internal.cpp:1356-1362`, and `JS_ReportError()`s the event loop dead. Any JS using `setTimeout`/`setInterval` without sockets would break.

    **2. `POLLPRI` is not supported.** `js_global.cpp:4157` and `js_global.cpp:4182` set `events = POLLPRI` for the except-set of `socket_select()`. `WSAPoll()` rejects it outright rather than degrading.

    ```
    1. WSAPoll(NULL, 0, 100) = -1 err=10022 <- WSAEINVAL
    2. WSAPoll(events=POLLPRI) = -1 err=10022 <- WSAEINVAL
    3. WSAPoll(listener, POLLIN) = 1 revents=0x100 <- POLLRDNORM, correct
    4. WSAPoll(100 fds) = 1 listener revents=0x100
    ```

    Cases 3 and 4 are the point of the exercise and they work: a listening socket with a pending connection is correctly reported readable, and >64 descriptors in a single call are fine. The mechanism is sound on Windows; only these two call sites are wrong.

    ## The documented connect bug did not reproduce

    `sockwrap.h:183` warns:

    NOTE: WSAPoll() has a bug where a non-blocking socket which has connect() called on it that is trying to connect to a closed port will timeout instead of returning a failure, even with POLLOUT specified.

    Non-blocking `connect()` to a closed loopback port, `WSAPoll()` vs `select()`:

    ```
    WSAPoll(POLLOUT) after 2016 ms: ret=1 revents=0x13 (POLLERR|POLLHUP|POLLOUT) select() after 2016 ms: ret=1 writable=0 except=1
    ```

    Identical timing, and `WSAPoll()` did report the failure. That is **one** scenario (RST-refused, loopback) on **one** OS build, so it is not evidence the note is obsolete — a dropped-SYN/firewalled destination was not tested, and older Windows versions are still supported. Recording it only so the next person knows the note needs re-verification rather than assuming it holds.

    ## If someone enables PREFER_POLL on Win32

    - Skip the `poll()` call when `nfds == 0` and sleep for the timeout instead.
    - Give `POLLPRI` a Windows substitute, or let the except-set path degrade.
    - Re-verify the `sockwrap.h:183` connect behavior on a dropped-SYN destination and on older Windows.
    - Note the blast radius: defining `PREFER_POLL` project-wide also flips `multisock.c`'s `xpms_accept()` — the accept path for *every* Windows server — plus the `sockwrap` helpers and `js_socket`, not just the JS engine.

    ## Probe source

    <details>
    <summary>wsapoll_test.c — cases 1-4</summary>

    ```c
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #include <stdio.h>

    #pragma comment(lib, "ws2_32.lib")

    int main(void)
    {
    WSADATA wd;
    WSAPOLLFD pfd;
    SOCKET lsock, csock;
    struct sockaddr_in sa;
    int len, r;
    u_long nb = 1;

    WSAStartup(MAKEWORD(2, 2), &wd);

    /* 1. Empty set - js_internal.cpp calls poll(NULL, 0, timeout) whenever a
    script has only timer events pending. */
    r = WSAPoll(NULL, 0, 100);
    printf("1. WSAPoll(NULL, 0, 100) = %d err=%d\n", r, WSAGetLastError());

    lsock = socket(AF_INET, SOCK_STREAM, 0);
    memset(&sa, 0, sizeof sa);
    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    sa.sin_port = 0;
    bind(lsock, (struct sockaddr *)&sa, sizeof sa);
    listen(lsock, 5);
    len = sizeof sa;
    getsockname(lsock, (struct sockaddr *)&sa, &len);

    /* 2. POLLPRI in events - js_global.cpp:4157,4182 use this for the
    except-set of socket_select(). */
    pfd.fd = lsock;
    pfd.events = POLLPRI;
    pfd.revents = 0;
    r = WSAPoll(&pfd, 1, 100);
    printf("2. WSAPoll(events=POLLPRI) = %d err=%d revents=0x%x\n",
    r, WSAGetLastError(), pfd.revents);

    /* 3. POLLIN on a listening socket with a pending connection - this is
    what an accept callback needs. */
    csock = socket(AF_INET, SOCK_STREAM, 0);
    ioctlsocket(csock, FIONBIO, &nb);
    connect(csock, (struct sockaddr *)&sa, sizeof sa);
    Sleep(200);
    pfd.fd = lsock;
    pfd.events = POLLIN;
    pfd.revents = 0;
    r = WSAPoll(&pfd, 1, 1000);
    printf("3. WSAPoll(listener, POLLIN) = %d err=%d revents=0x%x (POLLRDNORM=0x%x)\n",
    r, WSAGetLastError(), pfd.revents, POLLRDNORM);

    /* 4. 100 sockets in one call - the case select() cannot express. */
    {
    WSAPOLLFD many[100];
    SOCKET pad[100];
    int i;
    for (i = 0; i < 100; i++) {
    pad[i] = socket(AF_INET, SOCK_DGRAM, 0);
    memset(&sa, 0, sizeof sa);
    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    sa.sin_port = 0;
    bind(pad[i], (struct sockaddr *)&sa, sizeof sa);
    many[i].fd = pad[i];
    many[i].events = POLLIN;
    many[i].revents = 0;
    }
    many[99].fd = lsock; /* listener last, still has a pending connect */
    r = WSAPoll(many, 100, 1000);
    printf("4. WSAPoll(100 fds) = %d err=%d listener revents=0x%x\n",
    r, WSAGetLastError(), many[99].revents);
    }

    return 0;
    }
    ```

    </details>

    <details>
    <summary>wsapoll_connect.c — the sockwrap.h:183 check</summary>

    ```c
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #include <stdio.h>

    #pragma comment(lib, "ws2_32.lib")

    static SOCKET start_connect(void)
    {
    SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in sa;
    u_long nb = 1;

    ioctlsocket(s, FIONBIO, &nb);
    memset(&sa, 0, sizeof sa);
    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    sa.sin_port = htons(1); /* nothing listens here */
    connect(s, (struct sockaddr *)&sa, sizeof sa);
    return s;
    }

    int main(void)
    {
    WSADATA wd;
    WSAPOLLFD pfd;
    SOCKET s;
    fd_set wfds, efds;
    struct timeval tv;
    DWORD t0;
    int r;

    WSAStartup(MAKEWORD(2, 2), &wd);

    s = start_connect();
    pfd.fd = s;
    pfd.events = POLLOUT;
    pfd.revents = 0;
    t0 = GetTickCount();
    r = WSAPoll(&pfd, 1, 3000);
    printf("WSAPoll(POLLOUT) after %lu ms: ret=%d err=%d revents=0x%x\n",
    (unsigned long)(GetTickCount() - t0), r, WSAGetLastError(), pfd.revents);
    closesocket(s);

    s = start_connect();
    FD_ZERO(&wfds);
    FD_ZERO(&efds);
    FD_SET(s, &wfds);
    FD_SET(s, &efds);
    tv.tv_sec = 3;
    tv.tv_usec = 0;
    t0 = GetTickCount();
    r = select(0, NULL, &wfds, &efds, &tv);
    printf("select() after %lu ms: ret=%d writable=%d except=%d\n",
    (unsigned long)(GetTickCount() - t0), r,
    FD_ISSET(s, &wfds) ? 1 : 0, FD_ISSET(s, &efds) ? 1 : 0);
    closesocket(s);

    return 0;
    }
    ```

    </details>

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Deucе@1:103/705 to GitLab note in main/sbbs on Sun Aug 2 20:27:41 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1212#note_9903

    Worth noting that *every* file must be compiled with the updated limit. The usual fix for this is to define your own struct for the expanded fd_set.

    Details [here](https://devblogs.microsoft.com/oldnewthing/20221102-00/?p=107343).
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Sun Aug 2 21:36:08 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1212#note_9909

    Agreed on the principle — that's why it went into `src/Directory.Build.props` rather than a per-file `#define`, so every project under `src/` gets it from one place.

    Audited the exchange surface: the only `fd_set`-typed API in the tree is `sbbs.h`'s `js_socket_add()`/`js_socket_isset()`, called only from `js_global.cpp` — same DLL, same define. `xpdev`, `smblib` and the 3rd-party headers all pass `SOCKET`. The Borland-built `sbbsctrl`/`useredit` never touch `fd_set`. Every `<PreprocessorDefinitions>` under `src/` inherits `%(PreprocessorDefinitions)`, so no project drops it.

    One real gap: `src/doors/syncscumm/msvc/Directory.Build.props` and `src/doors/syncscumm/build-msvc/Directory.Build.props` shadow the `src/` one, so ScummVM's bundled enet/curl networking compiles at 64. It `select()`s on its own `fd_set`s and never hands one to our code, so nothing mismatches — but it shows the uniformity is a property of the current layout, not something the build enforces.

    The `fd_setN`-style struct is the stronger form and would let `FD_SETSIZE` go back to default. With the surface at one function pair in one module I'd leave it as-is, but it's worth doing if that ever grows — noting it here so the tradeoff is on the record.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)