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)