From 5c129a7f7f50498eea9d3d59ac32216053f4cac9 Mon Sep 17 00:00:00 2001 From: Danilo Silva Date: Mon, 19 Oct 2015 12:02:32 +0200 Subject: [PATCH 1/4] Avoid infinite loops when no data is written on a socket for a time greater than writingTimeout settings --- src/Monolog/Handler/SocketHandler.php | 51 +++++++++++++++++++++ tests/Monolog/Handler/SocketHandlerTest.php | 20 ++++++++ 2 files changed, 71 insertions(+) diff --git a/src/Monolog/Handler/SocketHandler.php b/src/Monolog/Handler/SocketHandler.php index a3e7252e..2712c8e6 100644 --- a/src/Monolog/Handler/SocketHandler.php +++ b/src/Monolog/Handler/SocketHandler.php @@ -25,6 +25,8 @@ class SocketHandler extends AbstractProcessingHandler private $connectionTimeout; private $resource; private $timeout = 0; + private $writingTimeout = 0; + private $lastSentBytes = null; private $persistent = false; private $errno; private $errstr; @@ -113,6 +115,18 @@ class SocketHandler extends AbstractProcessingHandler $this->timeout = (float) $seconds; } + /** + * Set writing timeout. Only has effect during connection in the writing cycle. + * + * @param float $seconds 0 for no timeout + * + */ + public function setWritingTimeout($seconds) + { + $this->validateTimeout($seconds); + $this->writingTimeout = (float) $seconds; + } + /** * Get current connection string * @@ -153,6 +167,16 @@ class SocketHandler extends AbstractProcessingHandler return $this->timeout; } + /** + * Get current local writing timeout + * + * @return float + */ + public function getWritingTimeout() + { + return $this->writingTimeout; + } + /** * Check to see if the socket is currently available. * @@ -262,6 +286,7 @@ class SocketHandler extends AbstractProcessingHandler { $length = strlen($data); $sent = 0; + $this->lastSentBytes = $sent; while ($this->isConnected() && $sent < $length) { if (0 == $sent) { $chunk = $this->fwrite($data); @@ -276,9 +301,35 @@ class SocketHandler extends AbstractProcessingHandler if ($socketInfo['timed_out']) { throw new \RuntimeException("Write timed-out"); } + + if ($this->writingIsTimedOut($sent)) { + throw new \RuntimeException("Write timed-out, no data sent for `{$this->writingTimeout}` seconds, probably we got disconnected (sent $sent of $length)"); + } } if (!$this->isConnected() && $sent < $length) { throw new \RuntimeException("End-of-file reached, probably we got disconnected (sent $sent of $length)"); } } + + private function writingIsTimedOut($sent) + { + $writingTimeout = (int) floor($this->writingTimeout); + if (0 === $writingTimeout) { + return false; + } + + if ($sent !== $this->lastSentBytes) { + $this->lastWritingAt = time(); + $this->lastSentBytes = $sent; + } else { + usleep(100); + } + + if ((time() - $this->lastWritingAt) >= $writingTimeout) { + $this->closeSocket(); + return true; + } + + return false; + } } diff --git a/tests/Monolog/Handler/SocketHandlerTest.php b/tests/Monolog/Handler/SocketHandlerTest.php index 2e3d504a..69f08aa2 100644 --- a/tests/Monolog/Handler/SocketHandlerTest.php +++ b/tests/Monolog/Handler/SocketHandlerTest.php @@ -235,6 +235,26 @@ class SocketHandlerTest extends TestCase $this->assertTrue(is_resource($this->res)); } + /** + * @expectedException \RuntimeException + */ + public function testAvoidInfiniteLoopWhenNoDataIsWrittenForAWritingTimeoutSeconds() + { + $this->setMockHandler(array('fwrite', 'streamGetMetadata')); + + $this->handler->expects($this->any()) + ->method('fwrite') + ->will($this->returnValue(0)); + + $this->handler->expects($this->any()) + ->method('streamGetMetadata') + ->will($this->returnValue(array('timed_out' => false))); + + $this->handler->setWritingTimeout(1); + + $this->writeRecord('Hello world'); + } + private function createHandler($connectionString) { $this->handler = new SocketHandler($connectionString); From 6f19ba38adb691574f077b13961ad408b9d05c47 Mon Sep 17 00:00:00 2001 From: Danilo Silva Date: Mon, 19 Oct 2015 12:21:41 +0200 Subject: [PATCH 2/4] added a test on writingTimeout setter and getter methods --- tests/Monolog/Handler/SocketHandlerTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Monolog/Handler/SocketHandlerTest.php b/tests/Monolog/Handler/SocketHandlerTest.php index 69f08aa2..1f9c1f28 100644 --- a/tests/Monolog/Handler/SocketHandlerTest.php +++ b/tests/Monolog/Handler/SocketHandlerTest.php @@ -70,6 +70,13 @@ class SocketHandlerTest extends TestCase $this->assertEquals(10.25, $this->handler->getTimeout()); } + public function testSetWritingTimeout() + { + $this->createHandler('localhost:1234'); + $this->handler->setWritingTimeout(10.25); + $this->assertEquals(10.25, $this->handler->getWritingTimeout()); + } + public function testSetConnectionString() { $this->createHandler('tcp://localhost:9090'); From 65cc3083b60cb63645944304c4f055836b9de4c2 Mon Sep 17 00:00:00 2001 From: Danilo Silva Date: Wed, 28 Oct 2015 16:11:06 +0100 Subject: [PATCH 3/4] exit prematurely when data has been written to avoid unnecessary time check @Seldaek --- src/Monolog/Handler/SocketHandler.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Monolog/Handler/SocketHandler.php b/src/Monolog/Handler/SocketHandler.php index 2712c8e6..1ca80afa 100644 --- a/src/Monolog/Handler/SocketHandler.php +++ b/src/Monolog/Handler/SocketHandler.php @@ -321,6 +321,8 @@ class SocketHandler extends AbstractProcessingHandler if ($sent !== $this->lastSentBytes) { $this->lastWritingAt = time(); $this->lastSentBytes = $sent; + + return false; } else { usleep(100); } From 988d595351ca166934f7318d9c368a92ff1f783d Mon Sep 17 00:00:00 2001 From: Danilo Silva Date: Wed, 28 Oct 2015 16:13:45 +0100 Subject: [PATCH 4/4] set to 10 seconds the default timeout during writing data on a socket, to aovid possible issue and with a very low risk to break backward compatibility @Seldaek --- src/Monolog/Handler/SocketHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Monolog/Handler/SocketHandler.php b/src/Monolog/Handler/SocketHandler.php index 1ca80afa..5d807cc0 100644 --- a/src/Monolog/Handler/SocketHandler.php +++ b/src/Monolog/Handler/SocketHandler.php @@ -25,7 +25,7 @@ class SocketHandler extends AbstractProcessingHandler private $connectionTimeout; private $resource; private $timeout = 0; - private $writingTimeout = 0; + private $writingTimeout = 10; private $lastSentBytes = null; private $persistent = false; private $errno;