diff --git a/src/Transport/Transport.php b/src/Transport/Transport.php index 48949a6..6c4fff6 100644 --- a/src/Transport/Transport.php +++ b/src/Transport/Transport.php @@ -244,20 +244,23 @@ protected function waitForReadyRead(int $time = 0): void return; } - $read = [$this->stream]; - $null = null; - $result = @stream_select($read, $null, $null, $this->config["timeout"]); - - if ($result === false) { - throw new TransportException("unable to wait for data from server '" . $this->config["host"] . ":" . $this->config["port"] . "'"); - } - - if ($result === 0) { - $time = $time ?: $this->config["timeout"]; - Signal::getInstance() - ->emit(strtolower($this->getAdapterType()) . "WaitTimeout", $time, $this->getAdapter()); - - throw new TransportException("timed out waiting for data from server '" . $this->config["host"] . ":" . $this->config["port"] . "'"); - } + do { + $read = [$this->stream]; + $null = null; + + if ($time) { + Signal::getInstance() + ->emit(strtolower($this->getAdapterType()) . "WaitTimeout", $time, $this->getAdapter()); + } + + // A zero timeout is valid for polling, but must still advance the + // reported idle time to avoid a busy loop without timeout signals. + $time += max(1, (int) $this->config["timeout"]); + $result = @stream_select($read, $null, $null, $this->config["timeout"]); + + if ($result === false) { + throw new TransportException("unable to wait for data from server '" . $this->config["host"] . ":" . $this->config["port"] . "'"); + } + } while ($result === 0); } } diff --git a/tests/Transport/TCPTest.php b/tests/Transport/TCPTest.php index c931091..14f932f 100644 --- a/tests/Transport/TCPTest.php +++ b/tests/Transport/TCPTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use PlanetTeamSpeak\TeamSpeak3Framework\Adapter\MockServerQuery; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\ServerQueryException; +use PlanetTeamSpeak\TeamSpeak3Framework\Helper\Signal; use PlanetTeamSpeak\TeamSpeak3Framework\Transport\TCP; use PlanetTeamSpeak\TeamSpeak3Framework\Exception\TransportException; @@ -349,7 +350,7 @@ public function testSendLineNoConnection() $transport->sendLine('test.sendLine'); } - public function testNonBlockingReadTimesOut(): void + public function testNonBlockingReadContinuesWaitingAfterTimeout(): void { $transport = new class (['host' => 'test', 'port' => 12345, 'blocking' => 0, 'timeout' => 0]) extends TCP { private $peer; @@ -363,13 +364,29 @@ public function waitForReadForTest(): void { $this->waitForReadyRead(); } + + public function makeStreamReadableForTest(): void + { + fwrite($this->peer, "event\n"); + } }; $transport->connectForTest(); + $idleTimes = []; + $callback = static function (int $idleTime) use ($transport, &$idleTimes): void { + $idleTimes[] = $idleTime; + $transport->makeStreamReadableForTest(); + }; - $this->expectException(TransportException::class); - $this->expectExceptionMessage("timed out waiting for data from server 'test:12345'"); - $transport->waitForReadForTest(); + Signal::getInstance()->subscribe('unknownWaitTimeout', $callback); + + try { + $transport->waitForReadForTest(); + } finally { + Signal::getInstance()->unsubscribe('unknownWaitTimeout', $callback); + } + + $this->assertSame([1], $idleTimes); } public function testTlsVerificationIsDisabledByDefault(): void