8000 Fix reporting refused connections with StreamSelectLoop on Windows · reactphp/event-loop@6a89446 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6a89446

Browse files
committed
Fix reporting refused connections with StreamSelectLoop on Windows
We do not usually use or expose the `exceptfds` parameter passed to the underlying `select`. However, Windows does not report failed connection attempts in `writefds` passed to `select` like most other platforms. Instead, it uses `writefds` only for successful connection attempts and `exceptfds` for failed connection attempts. See also https://docs.microsoft.com/de-de/windows/win32/api/winsock2/nf-winsock2-select We work around this by adding all sockets that look like a pending connection attempt to `exceptfds` automatically on Windows and merge it back later. This ensures the public API matches other loop implementations across all platforms (see also test suite or rather test matrix). Lacking better APIs, every write-only socket that has not yet read any data is assumed to be in a pending connection attempt state.
1 parent 6d45a19 commit 6a89446

File tree

2 files changed

+22
-7
lines changed

2 files changed

+22
-7
lines changed

src/StreamSelectLoop.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,30 @@ private function waitForStreamActivity($timeout)
269269
private function streamSelect(array &$read, array &$write, $timeout)
270270
{
271271
if ($read || $write) {
272+
// We do not usually use or expose the `exceptfds` parameter passed to the underlying `select`.
273+
// However, Windows does not report failed connection attempts in `writefds` passed to `select` like most other platforms.
274+
// Instead, it uses `writefds` only for successful connection attempts and `exceptfds` for failed connection attempts.
275+
// We work around this by adding all sockets that look like a pending connection attempt to `exceptfds` automatically on Windows and merge it back later.
276+
// This ensures the public API matches other loop implementations across all platforms (see also test suite or rather test matrix).
277+
// Lacking better APIs, every write-only socket that has not yet read any data is assumed to be in a pending connection attempt state.
278+
// @link https://docs.microsoft.com/de-de/windows/win32/api/winsock2/nf-winsock2-select
272279
$except = null;
280+
if (\DIRECTORY_SEPARATOR === '\\') {
281+
$except = array();
282+
foreach ($write as $key => $socket) {
283+
if (!isset($read[$key]) && @\ftell($socket) === 0) {
284+
$except[$key] = $socket;
285+
}
286+
}
287+
}
273288

274289
// suppress warnings that occur, when stream_select is interrupted by a signal
275-
return @\stream_select($read, $write, $except, $timeout === null ? null : 0, $timeout);
290+
$ret = @\stream_select($read, $write, $except, $timeout === null ? null : 0, $timeout);
291+
292+
if ($except) {
293+
$write = \array_merge($write, $except);
294+
}
295+
return $ret;
276296
}
277297

278298
if ($timeout > 0) {

tests/AbstractLoopTest.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,11 @@ public function testAddWriteStreamTriggersWhenSocketConnectionSucceeds()
111111

112112
public function testAddWriteStreamTriggersWhenSocketConnectionRefused()
113113
{
114-
// @link https://github.com/reactphp/event-loop/issues/206
115-
if ($this->loop instanceof StreamSelectLoop && DIRECTORY_SEPARATOR === '\\') {
116-
$this->markTestIncomplete('StreamSelectLoop does not currently support detecting connection refused errors on Windows');
117-
}
118-
119114
// first verify the operating system actually refuses the connection and no firewall is in place
120115
// use higher timeout because Windows retires multiple times and has a noticeable delay
121116
// @link https://stackoverflow.com/questions/19440364/why-do-failed-attempts-of-socket-connect-take-1-sec-on-windows
122117
$errno = $errstr = null;
123-
if (@stream_socket_client('127.0.0.1:1', $errno, $errstr, 10.0) !== false || $errno !== SOCKET_ECONNREFUSED) {
118+
if (@stream_socket_client('127.0.0.1:1', $errno, $errstr, 10.0) !== false || (defined('SOCKET_ECONNREFUSED') && $errno !== SOCKET_ECONNREFUSED)) {
124119
$this->markTestSkipped('Expected host to refuse connection, but got error ' . $errno . ': ' . $errstr);
125120
}
126121

0 commit comments

Comments
 (0)
0