Преглед изворни кода

Avoid infinite loops when no data is written on a socket for a time greater than writingTimeout settings

Danilo Silva пре 10 година
родитељ
комит
5c129a7f7f
2 измењених фајлова са 71 додато и 0 уклоњено
  1. 51 0
      src/Monolog/Handler/SocketHandler.php
  2. 20 0
      tests/Monolog/Handler/SocketHandlerTest.php

+ 51 - 0
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;
+    }
 }

+ 20 - 0
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);