Skip to content

Fix the WSAPoll backend of Io\Poll accepting handles that are not sockets - #25135

Closed
nicolas-grekas wants to merge 1 commit into
php:PHP-8.6from
nicolas-grekas:io-poll-wsapoll-non-sockets
Closed

nicolas-grekas wants to merge 1 commit into
php:PHP-8.6from
nicolas-grekas:io-poll-wsapoll-non-sockets

Conversation

@nicolas-grekas

Copy link
Copy Markdown
Contributor

Found in revoltphp/event-loop#135, where the Windows CI hangs on callbacks on STDOUT.

WSAPoll() can only poll sockets, but the WSAPoll backend accepts any fd. Files and pipes give a CRT fd, so Context::add() succeeds for them. Then WSAPoll() reports POLLNVAL for them, or fails with WSAENOTSOCK when they're alone. In both cases wait() drops them and returns at once. The watcher is never reported:

// STDOUT is a pipe
$ctx = new Io\Poll\Context();
$ctx->add(new StreamPollHandle(STDOUT), [Io\Poll\Event::Write]); // no exception
$ctx->wait(); // returns [] at once, and again on every call

This PR checks the fd with getsockopt(SO_TYPE) in add(), the way stream_select() tells sockets apart on Windows. Anything else fails with ERROR_NOSUPPORT, as #23805 does for epoll. An event loop can then catch the FailedHandleAddException and fall back to stream_select(). Revolt doesn't pick its Io\Poll driver on Windows for now because of this.

I left the WSAENOTSOCK branch of wait() as is. This branch now only sees sockets that were closed after being added. It drops those sockets like the poll backend drops POLLNVAL.

/cc @bukka

…kets

WSAPoll() can only poll sockets, but the backend accepted any fd. A file
or a pipe such as STDOUT was then dropped by wait() without being
reported. Adding such a handle now fails with ERR_NOSUPPORT, as epoll
does for regular files.

@bukka bukka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

@bukka bukka closed this in f2fbe47 Nov 1, 2026
@kelunik

kelunik commented Nov 1, 2026

Copy link
Copy Markdown
Member

@bukka Thanks for landing this. With ERROR_NOSUPPORT, it's now clear that the WSAPoll backend can't watch STDIN/STDOUT when they're pipes, files or a console, instead of silently dropping them. For now, Revolt will keep using its stream_select() driver on Windows.

What are your plans for non-socket handles in the WSAPoll backend, and what would you recommend? Is supporting them on your roadmap? Or should event loops treat Io\Poll on Windows as sockets-only and keep a stream_select() fallback for stdio?

I discussed this with Claude. Below is its suggestion. It's unverified, so please take it as a starting point, not a proposal:

Extend the WSAPoll backend instead of adding a separate WaitForMultipleObjects() backend. WaitForMultipleObjects() on its own doesn't give real readiness for these handles: pipes opened without overlapped I/O and disk files are effectively always signaled, and a console output handle can't be waited on in any useful way.

  • add(): sockets go into the WSAPoll set as now. For any other fd, look up the handle with _get_osfhandle() and record its GetFileType() class instead of returning ERROR_NOSUPPORT.

  • wait() with only sockets: plain WSAPoll(), unchanged.

  • wait() with non-socket handles: probe them first.

    • Disk files are always ready.
    • Pipes: PeekNamedPipe() for read and HUP, NtQueryInformationFile(FilePipeLocalInformation) → WriteQuotaAvailable for write.
    • Console input: PeekConsoleInput(). Console output is always writable.

    If nothing is ready, wait in slices: WSAPoll() on the sockets with a short timeout, waiting on the console input handles between slices. This is roughly php_select()'s approach.

The trade-offs are roughly stream_select()'s: a few ms of latency and some CPU cost, but only when non-socket handles are registered. Alternatives it considered:

  • WSAEventSelect() + WaitForMultipleObjects() for everything: forces sockets to non-blocking, allows only one association per socket, FD_WRITE is edge-triggered, and there's a limit of 64 handles.
  • Helper threads, as libuv does with zero-byte pipe reads.
  • A full IOCP/AFD backend in the style of wepoll: much more work.

Does this direction make sense to you, or do you have something else in mind?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants