Skip to content

Commit 646fbbd

Browse files
authored
Merge pull request #208 from clue-labs/windows-connections
Fix reporting refused connections with StreamSelectLoop on Windows
2 parents e79f422 + 6a89446 commit 646fbbd

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)