From 06ecbb73accd87ac66a3fd2e9dc251e41c440628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 24 Dec 2019 11:36:16 +0100 Subject: [PATCH 1/4] Add TCP/IP connection tests --- tests/AbstractLoopTest.php | 106 +++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/tests/AbstractLoopTest.php b/tests/AbstractLoopTest.php index 2e46b66e..127f2bb7 100644 --- a/tests/AbstractLoopTest.php +++ b/tests/AbstractLoopTest.php @@ -2,6 +2,8 @@ namespace React\Tests\EventLoop; +use React\EventLoop\StreamSelectLoop; + abstract class AbstractLoopTest extends TestCase { /** @@ -36,6 +38,110 @@ public function createSocketPair() return $sockets; } + public function testAddReadStreamTriggersWhenSocketReceivesData() + { + list ($input, $output) = $this->createSocketPair(); + + $loop = $this->loop; + $timeout = $loop->addTimer(0.1, function () use ($input, $loop) { + $loop->removeReadStream($input); + }); + + $called = 0; + $this->loop->addReadStream($input, function () use (&$called, $loop, $input, $timeout) { + ++$called; + $loop->removeReadStream($input); + $loop->cancelTimer($timeout); + }); + + fwrite($output, "foo\n"); + + $this->loop->run(); + + $this->assertEquals(1, $called); + } + + public function testAddReadStreamTriggersWhenSocketCloses() + { + list ($input, $output) = $this->createSocketPair(); + + $loop = $this->loop; + $timeout = $loop->addTimer(0.1, function () use ($input, $loop) { + $loop->removeReadStream($input); + }); + + $called = 0; + $this->loop->addReadStream($input, function () use (&$called, $loop, $input, $timeout) { + ++$called; + $loop->removeReadStream($input); + $loop->cancelTimer($timeout); + }); + + fclose($output); + + $this->loop->run(); + + $this->assertEquals(1, $called); + } + + public function testAddWriteStreamTriggersWhenSocketConnectionSucceeds() + { + $server = stream_socket_server('127.0.0.1:0'); + + $errno = $errstr = null; + $connecting = stream_socket_client(stream_socket_get_name($server, false), $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); + + $loop = $this->loop; + $timeout = $loop->addTimer(0.1, function () use ($connecting, $loop) { + $loop->removeWriteStream($connecting); + }); + + $called = 0; + $this->loop->addWriteStream($connecting, function () use (&$called, $loop, $connecting, $timeout) { + ++$called; + $loop->removeWriteStream($connecting); + $loop->cancelTimer($timeout); + }); + + $this->loop->run(); + + $this->assertEquals(1, $called); + } + + public function testAddWriteStreamTriggersWhenSocketConnectionRefused() + { + // @link https://github.com/reactphp/event-loop/issues/206 + if ($this->loop instanceof StreamSelectLoop && DIRECTORY_SEPARATOR === '\\') { + $this->markTestIncomplete('StreamSelectLoop does not currently support detecting connection refused errors on Windows'); + } + + // first verify the operating system actually refuses the connection and no firewall is in place + // use higher timeout because Windows retires multiple times and has a noticeable delay + // @link https://stackoverflow.com/questions/19440364/why-do-failed-attempts-of-socket-connect-take-1-sec-on-windows + $errno = $errstr = null; + if (@stream_socket_client('127.0.0.1:1', $errno, $errstr, 10.0) !== false || $errno !== SOCKET_ECONNREFUSED) { + $this->markTestSkipped('Expected host to refuse connection, but got error ' . $errno . ': ' . $errstr); + } + + $connecting = stream_socket_client('127.0.0.1:1', $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); + + $loop = $this->loop; + $timeout = $loop->addTimer(10.0, function () use ($connecting, $loop) { + $loop->removeWriteStream($connecting); + }); + + $called = 0; + $this->loop->addWriteStream($connecting, function () use (&$called, $loop, $connecting, $timeout) { + ++$called; + $loop->removeWriteStream($connecting); + $loop->cancelTimer($timeout); + }); + + $this->loop->run(); + + $this->assertEquals(1, $called); + } + public function testAddReadStream() { list ($input, $output) = $this->createSocketPair(); From 527c60af4d4fbd9fc76a6a227ed18dbd264ef9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 23 Dec 2019 22:23:09 +0100 Subject: [PATCH 2/4] Test ext-uv on Windows --- .travis.yml | 14 +++++++++++ tests/AbstractLoopTest.php | 48 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/.travis.yml b/.travis.yml index 7182febc..1cd43c2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,6 +51,20 @@ matrix: - php -r "file_put_contents(php_ini_loaded_file(),'extension=event'.PHP_EOL,FILE_APPEND);" install: - composer install + - name: "Windows PHP 7.4 with ext-uv" + os: windows + language: shell # no built-in php support + before_install: + - curl -OL https://windows.php.net/downloads/pecl/releases/uv/0.2.4/php_uv-0.2.4-7.4-nts-vc15-x64.zip # latest version as of 2019-12-23 + - choco install php --version=7.4.0 # latest version supported by ext-uv as of 2019-12-23 + - choco install composer + - export PATH="$(powershell -Command '("Process", "Machine" | % { [Environment]::GetEnvironmentVariable("PATH", $_) -Split ";" -Replace "\\$", "" } | Select -Unique | % { cygpath $_ }) -Join ":"')" + - php -r "\$z=new ZipArchive();\$z->open(glob('php_uv*.zip')[0]);\$z->extractTo(dirname(php_ini_loaded_file()).'/ext','php_uv.dll');\$z->extractTo(dirname(php_ini_loaded_file()),'libuv.dll');" + - php -r "file_put_contents(php_ini_loaded_file(),'extension_dir=ext'.PHP_EOL,FILE_APPEND);" + - php -r "file_put_contents(php_ini_loaded_file(),'extension=sockets'.PHP_EOL,FILE_APPEND);" # ext-sockets needs to be loaded before ext-uv + - php -r "file_put_contents(php_ini_loaded_file(),'extension=uv'.PHP_EOL,FILE_APPEND);" + install: + - composer install allow_failures: - php: hhvm - os: windows diff --git a/tests/AbstractLoopTest.php b/tests/AbstractLoopTest.php index 127f2bb7..44157d2c 100644 --- a/tests/AbstractLoopTest.php +++ b/tests/AbstractLoopTest.php @@ -3,6 +3,7 @@ namespace React\Tests\EventLoop; use React\EventLoop\StreamSelectLoop; +use React\EventLoop\ExtUvLoop; abstract class AbstractLoopTest extends TestCase { @@ -144,6 +145,10 @@ public function testAddWriteStreamTriggersWhenSocketConnectionRefused() public function testAddReadStream() { + if ($this->loop instanceof ExtUvLoop && DIRECTORY_SEPARATOR === '\\') { + $this->markTestIncomplete('Ticking ExtUvLoop not supported on Windows'); + } + list ($input, $output) = $this->createSocketPair(); $this->loop->addReadStream($input, $this->expectCallableExactly(2)); @@ -157,6 +162,10 @@ public function testAddReadStream() public function testAddReadStreamIgnoresSecondCallable() { + if ($this->loop instanceof ExtUvLoop && DIRECTORY_SEPARATOR === '\\') { + $this->markTestIncomplete('Ticking ExtUvLoop not supported on Windows'); + } + list ($input, $output) = $this->createSocketPair(); $this->loop->addReadStream($input, $this->expectCallableExactly(2)); @@ -206,6 +215,10 @@ private function subAddReadStreamReceivesDataFromStreamReference() public function testAddWriteStream() { + if ($this->loop instanceof ExtUvLoop && DIRECTORY_SEPARATOR === '\\') { + $this->markTestIncomplete('Ticking ExtUvLoop not supported on Windows'); + } + list ($input) = $this->createSocketPair(); $this->loop->addWriteStream($input, $this->expectCallableExactly(2)); @@ -215,6 +228,10 @@ public function testAddWriteStream() public function testAddWriteStreamIgnoresSecondCallable() { + if ($this->loop instanceof ExtUvLoop && DIRECTORY_SEPARATOR === '\\') { + $this->markTestIncomplete('Ticking ExtUvLoop not supported on Windows'); + } + list ($input) = $this->createSocketPair(); $this->loop->addWriteStream($input, $this->expectCallableExactly(2)); @@ -225,6 +242,10 @@ public function testAddWriteStreamIgnoresSecondCallable() public function testRemoveReadStreamInstantly() { + if ($this->loop instanceof ExtUvLoop && DIRECTORY_SEPARATOR === '\\') { + $this->markTestIncomplete('Ticking ExtUvLoop not supported on Windows'); + } + list ($input, $output) = $this->createSocketPair(); $this->loop->addReadStream($input, $this->expectCallableNever()); @@ -236,6 +257,10 @@ public function testRemoveReadStreamInstantly() public function testRemoveReadStreamAfterReading() { + if ($this->loop instanceof ExtUvLoop && DIRECTORY_SEPARATOR === '\\') { + $this->markTestIncomplete('Ticking ExtUvLoop not supported on Windows'); + } + list ($input, $output) = $this->createSocketPair(); $this->loop->addReadStream($input, $this->expectCallableOnce()); @@ -251,6 +276,10 @@ public function testRemoveReadStreamAfterReading() public function testRemoveWriteStreamInstantly() { + if ($this->loop instanceof ExtUvLoop && DIRECTORY_SEPARATOR === '\\') { + $this->markTestIncomplete('Ticking ExtUvLoop not supported on Windows'); + } + list ($input) = $this->createSocketPair(); $this->loop->addWriteStream($input, $this->expectCallableNever()); @@ -260,6 +289,10 @@ public function testRemoveWriteStreamInstantly() public function testRemoveWriteStreamAfterWriting() { + if ($this->loop instanceof ExtUvLoop && DIRECTORY_SEPARATOR === '\\') { + $this->markTestIncomplete('Ticking ExtUvLoop not supported on Windows'); + } + list ($input) = $this->createSocketPair(); $this->loop->addWriteStream($input, $this->expectCallableOnce()); @@ -271,6 +304,10 @@ public function testRemoveWriteStreamAfterWriting() public function testRemoveStreamForReadOnly() { + if ($this->loop instanceof ExtUvLoop && DIRECTORY_SEPARATOR === '\\') { + $this->markTestIncomplete('Ticking ExtUvLoop not supported on Windows'); + } + list ($input, $output) = $this->createSocketPair(); $this->loop->addReadStream($input, $this->expectCallableNever()); @@ -283,6 +320,10 @@ public function testRemoveStreamForReadOnly() public function testRemoveStreamForWriteOnly() { + if ($this->loop instanceof ExtUvLoop && DIRECTORY_SEPARATOR === '\\') { + $this->markTestIncomplete('Ticking ExtUvLoop not supported on Windows'); + } + list ($input, $output) = $this->createSocketPair(); fwrite($output, "foo\n"); @@ -505,6 +546,10 @@ public function testFutureTick() public function testFutureTickFiresBeforeIO() { + if ($this->loop instanceof ExtUvLoop && DIRECTORY_SEPARATOR === '\\') { + $this->markTestIncomplete('Ticking ExtUvLoop not supported on Windows'); + } + list ($stream) = $this->createSocketPair(); $this->loop->addWriteStream( @@ -525,6 +570,9 @@ function () { $this->tickLoop($this->loop); } + /** + * @depends testFutureTickFiresBeforeIO + */ public function testRecursiveFutureTick() { list ($stream) = $this->createSocketPair(); From 6d45a1935111d34f2f39d1490edc45cb2a7f97c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 29 Dec 2019 20:44:44 +0100 Subject: [PATCH 3/4] Fix reporting refused connections with ExtUvLoop The underlying `epoll_wait()` reports `EPOLLOUT|EPOLLERR|EPOLLHUP` on the affected file descriptor, which `ext-uv` emits as an error code `EBADF` with no events attached. We explicitly re-enable all active events on this error event to invoke the writable listener for this condition to match other event loop implementations and successfully detect this as a refused connection attempt. All tests are now green. --- src/ExtUvLoop.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ExtUvLoop.php b/src/ExtUvLoop.php index 70471b59..002d6a2e 100644 --- a/src/ExtUvLoop.php +++ b/src/ExtUvLoop.php @@ -294,13 +294,15 @@ private function pollStream($stream) private function createStreamListener() { $callback = function ($event, $status, $events, $stream) { - if (!isset($this->streamEvents[(int) $stream])) { - return; - } - - if (($events | 4) === 4) { - // Disconnected - return; + // libuv automatically stops polling on error, re-enable polling to match other loop implementations + if ($status !== 0) { + $this->pollStream($stream); + + // libuv may report no events on error, but this should still invoke stream listeners to report closed connections + // re-enable both readable and writable, correct listeners will be checked below anyway + if ($events === 0) { + $events = \UV::READABLE | \UV::WRITABLE; + } } if (isset($this->readStreams[(int) $stream]) && ($events & \UV::READABLE)) { From 6a894465c3f974fb26f153f79dab0d4909e42bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 30 Dec 2019 20:59:03 +0100 Subject: [PATCH 4/4] 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. --- src/StreamSelectLoop.php | 22 +++++++++++++++++++++- tests/AbstractLoopTest.php | 7 +------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/StreamSelectLoop.php b/src/StreamSelectLoop.php index 3362d3e5..4426844a 100644 --- a/src/StreamSelectLoop.php +++ b/src/StreamSelectLoop.php @@ -269,10 +269,30 @@ private function waitForStreamActivity($timeout) private function streamSelect(array &$read, array &$write, $timeout) { if ($read || $write) { + // 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. + // 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. + // @link https://docs.microsoft.com/de-de/windows/win32/api/winsock2/nf-winsock2-select $except = null; + if (\DIRECTORY_SEPARATOR === '\\') { + $except = array(); + foreach ($write as $key => $socket) { + if (!isset($read[$key]) && @\ftell($socket) === 0) { + $except[$key] = $socket; + } + } + } // suppress warnings that occur, when stream_select is interrupted by a signal - return @\stream_select($read, $write, $except, $timeout === null ? null : 0, $timeout); + $ret = @\stream_select($read, $write, $except, $timeout === null ? null : 0, $timeout); + + if ($except) { + $write = \array_merge($write, $except); + } + return $ret; } if ($timeout > 0) { diff --git a/tests/AbstractLoopTest.php b/tests/AbstractLoopTest.php index 44157d2c..9b55f959 100644 --- a/tests/AbstractLoopTest.php +++ b/tests/AbstractLoopTest.php @@ -111,16 +111,11 @@ public function testAddWriteStreamTriggersWhenSocketConnectionSucceeds() public function testAddWriteStreamTriggersWhenSocketConnectionRefused() { - // @link https://github.com/reactphp/event-loop/issues/206 - if ($this->loop instanceof StreamSelectLoop && DIRECTORY_SEPARATOR === '\\') { - $this->markTestIncomplete('StreamSelectLoop does not currently support detecting connection refused errors on Windows'); - } - // first verify the operating system actually refuses the connection and no firewall is in place // use higher timeout because Windows retires multiple times and has a noticeable delay // @link https://stackoverflow.com/questions/19440364/why-do-failed-attempts-of-socket-connect-take-1-sec-on-windows $errno = $errstr = null; - if (@stream_socket_client('127.0.0.1:1', $errno, $errstr, 10.0) !== false || $errno !== SOCKET_ECONNREFUSED) { + if (@stream_socket_client('127.0.0.1:1', $errno, $errstr, 10.0) !== false || (defined('SOCKET_ECONNREFUSED') && $errno !== SOCKET_ECONNREFUSED)) { $this->markTestSkipped('Expected host to refuse connection, but got error ' . $errno . ': ' . $errstr); } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy