diff --git a/src/StreamSelectLoop.php b/src/StreamSelectLoop.php index 4b1f81c1..9e78dc03 100644 --- a/src/StreamSelectLoop.php +++ b/src/StreamSelectLoop.php @@ -287,8 +287,28 @@ private function streamSelect(array &$read, array &$write, $timeout) } } - // suppress warnings that occur, when stream_select is interrupted by a signal - $ret = @\stream_select($read, $write, $except, $timeout === null ? null : 0, $timeout); + /** @var ?callable $previous */ + $previous = \set_error_handler(function ($errno, $errstr) use (&$previous) { + // suppress warnings that occur when `stream_select()` is interrupted by a signal + $eintr = \defined('SOCKET_EINTR') ? \SOCKET_EINTR : 4; + if ($errno === \E_WARNING && \strpos($errstr, '[' . $eintr .']: ') !== false) { + return; + } + + // forward any other error to registered error handler or print warning + return ($previous !== null) ? \call_user_func_array($previous, \func_get_args()) : false; + }); + + try { + $ret = \stream_select($read, $write, $except, $timeout === null ? null : 0, $timeout); + \restore_error_handler(); + } catch (\Throwable $e) { // @codeCoverageIgnoreStart + \restore_error_handler(); + throw $e; + } catch (\Exception $e) { + \restore_error_handler(); + throw $e; + } // @codeCoverageIgnoreEnd if ($except) { $write = \array_merge($write, $except); diff --git a/tests/StreamSelectLoopTest.php b/tests/StreamSelectLoopTest.php index d6800c5e..80197468 100644 --- a/tests/StreamSelectLoopTest.php +++ b/tests/StreamSelectLoopTest.php @@ -40,6 +40,85 @@ public function testStreamSelectTimeoutEmulation() $this->assertGreaterThan(0.04, $interval); } + public function testStreamSelectReportsWarningForStreamWithFilter() + { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on legacy HHVM'); + } + + $stream = tmpfile(); + stream_filter_append($stream, 'string.rot13'); + + $this->loop->addReadStream($stream, $this->expectCallableNever()); + + $loop = $this->loop; + $this->loop->futureTick(function () use ($loop, $stream) { + $loop->futureTick(function () use ($loop, $stream) { + $loop->removeReadStream($stream); + }); + }); + + $error = null; + $previous = set_error_handler(function ($_, $errstr) use (&$error) { + $error = $errstr; + }); + + try { + $this->loop->run(); + } catch (\ValueError $e) { + // ignore ValueError for PHP 8+ due to empty stream array + } + + restore_error_handler(); + + $this->assertNotNull($error); + + $now = set_error_handler(function () { }); + restore_error_handler(); + $this->assertEquals($previous, $now); + } + + public function testStreamSelectThrowsWhenCustomErrorHandlerThrowsForStreamWithFilter() + { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on legacy HHVM'); + } + + $stream = tmpfile(); + stream_filter_append($stream, 'string.rot13'); + + $this->loop->addReadStream($stream, $this->expectCallableNever()); + + $loop = $this->loop; + $this->loop->futureTick(function () use ($loop, $stream) { + $loop->futureTick(function () use ($loop, $stream) { + $loop->removeReadStream($stream); + }); + }); + + $previous = set_error_handler(function ($_, $errstr) { + throw new \RuntimeException($errstr); + }); + + $e = null; + try { + $this->loop->run(); + restore_error_handler(); + $this->fail(); + } catch (\RuntimeException $e) { + restore_error_handler(); + } catch (\ValueError $e) { + restore_error_handler(); // PHP 8+ + $e = $e->getPrevious(); + } + + $this->assertInstanceOf('RuntimeException', $e); + + $now = set_error_handler(function () { }); + restore_error_handler(); + $this->assertEquals($previous, $now); + } + public function signalProvider() { return array(