Quellcode durchsuchen

Merge branch '1.x'

Jordi Boggiano vor 7 Jahren
Ursprung
Commit
073c5d763a

+ 2 - 0
CHANGELOG.md

@@ -19,6 +19,8 @@
   * Fixed table row styling issues in HtmlFormatter
   * Fixed RavenHandler dropping the message when logging exception
   * Fixed WhatFailureGroupHandler skipping processors when using handleBatch
+  * Added a `ResettableInterface` in order to reset/reset/clear/flush handlers and processors
+    and implement it where possible
 
 ### 1.23.0 (2017-06-19)
 

+ 2 - 0
doc/03-utilities.md

@@ -5,6 +5,8 @@
   help in some older codebases or for ease of use.
 - _ErrorHandler_: The `Monolog\ErrorHandler` class allows you to easily register
   a Logger instance as an exception handler, error handler or fatal error handler.
+- _SignalHandler_: The `Monolog\SignalHandler` class allows you to easily register
+  a Logger instance as a POSIX signal handler.
 - _ErrorLevelActivationStrategy_: Activates a FingersCrossedHandler when a certain log
   level is reached.
 - _ChannelLevelActivationStrategy_: Activates a FingersCrossedHandler when a certain

+ 2 - 1
src/Monolog/ErrorHandler.php

@@ -13,6 +13,7 @@ namespace Monolog;
 
 use Psr\Log\LoggerInterface;
 use Psr\Log\LogLevel;
+use Monolog\Registry;
 
 /**
  * Monolog error handler
@@ -163,7 +164,7 @@ class ErrorHandler
 
         $this->logger->log(
             $level,
-            sprintf('Uncaught Exception %s: "%s" at %s line %s', get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()),
+            sprintf('Uncaught Exception %s: "%s" at %s line %s', Utils::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()),
             ['exception' => $e]
         );
 

+ 2 - 1
src/Monolog/Formatter/JsonFormatter.php

@@ -11,6 +11,7 @@
 
 namespace Monolog\Formatter;
 
+use Monolog\Utils;
 use Throwable;
 
 /**
@@ -166,7 +167,7 @@ class JsonFormatter extends NormalizerFormatter
     protected function normalizeException(Throwable $e, int $depth = 0): array
     {
         $data = [
-            'class' => get_class($e),
+            'class' => Utils::getClass($e),
             'message' => $e->getMessage(),
             'code' => $e->getCode(),
             'file' => $e->getFile().':'.$e->getLine(),

+ 3 - 1
src/Monolog/Formatter/LineFormatter.php

@@ -11,6 +11,8 @@
 
 namespace Monolog\Formatter;
 
+use Monolog\Utils;
+
 /**
  * Formats incoming records into a one-line string
  *
@@ -168,7 +170,7 @@ class LineFormatter extends NormalizerFormatter
 
     private function formatException(\Throwable $e): string
     {
-        $str = '[object] (' . get_class($e) . '(code: ' . $e->getCode() . '): ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine() . ')';
+        $str = '[object] (' . Utils::getClass($e) . '(code: ' . $e->getCode() . '): ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine() . ')';
         if ($this->includeStacktraces) {
             $str .= "\n[stacktrace]\n" . $e->getTraceAsString() . "\n";
         }

+ 3 - 2
src/Monolog/Formatter/MongoDBFormatter.php

@@ -12,6 +12,7 @@
 namespace Monolog\Formatter;
 
 use MongoDB\BSON\UTCDateTime;
+use Monolog\Utils;
 
 /**
  * Formats a record for use with the MongoDBHandler.
@@ -83,7 +84,7 @@ class MongoDBFormatter implements FormatterInterface
     protected function formatObject($value, int $nestingLevel)
     {
         $objectVars = get_object_vars($value);
-        $objectVars['class'] = get_class($value);
+        $objectVars['class'] = Utils::getClass($value);
 
         return $this->formatArray($objectVars, $nestingLevel);
     }
@@ -91,7 +92,7 @@ class MongoDBFormatter implements FormatterInterface
     protected function formatException(\Throwable $exception, int $nestingLevel)
     {
         $formattedException = [
-            'class' => get_class($exception),
+            'class' => Utils::getClass($exception),
             'message' => $exception->getMessage(),
             'code' => $exception->getCode(),
             'file' => $exception->getFile() . ':' . $exception->getLine(),

+ 4 - 3
src/Monolog/Formatter/NormalizerFormatter.php

@@ -13,6 +13,7 @@ namespace Monolog\Formatter;
 
 use Throwable;
 use Monolog\DateTimeImmutable;
+use Monolog\Utils;
 
 /**
  * Normalizes incoming records to remove objects/resources so it's easier to dump to various targets
@@ -146,7 +147,7 @@ class NormalizerFormatter implements FormatterInterface
                 }
             }
 
-            return [get_class($data) => $value];
+            return [Utils::getClass($data) => $value];
         }
 
         if (is_resource($data)) {
@@ -162,7 +163,7 @@ class NormalizerFormatter implements FormatterInterface
     protected function normalizeException(Throwable $e, int $depth = 0)
     {
         $data = [
-            'class' => get_class($e),
+            'class' => Utils::getClass($e),
             'message' => $e->getMessage(),
             'code' => $e->getCode(),
             'file' => $e->getFile().':'.$e->getLine(),
@@ -195,7 +196,7 @@ class NormalizerFormatter implements FormatterInterface
                     // as a class name to avoid any unexpected leak of sensitive information
                     $frame['args'] = array_map(function ($arg) {
                         if (is_object($arg) && !$arg instanceof \DateTimeInterface) {
-                            return sprintf("[object] (%s)", get_class($arg));
+                            return sprintf("[object] (%s)", Utils::getClass($arg));
                         }
 
                         return $arg;

+ 14 - 1
src/Monolog/Handler/AbstractProcessingHandler.php

@@ -11,6 +11,8 @@
 
 namespace Monolog\Handler;
 
+use Monolog\ResettableInterface;
+
 /**
  * Base Handler class providing the Handler structure
  *
@@ -19,7 +21,7 @@ namespace Monolog\Handler;
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Christophe Coevoet <stof@notk.org>
  */
-abstract class AbstractProcessingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface
+abstract class AbstractProcessingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface, ResettableInterface
 {
     use ProcessableHandlerTrait;
     use FormattableHandlerTrait;
@@ -48,4 +50,15 @@ abstract class AbstractProcessingHandler extends AbstractHandler implements Proc
      * Writes the record down to the log of the implementing handler
      */
     abstract protected function write(array $record): void;
+
+    public function reset()
+    {
+        $this->close();
+
+        foreach ($this->processors as $processor) {
+            if ($processor instanceof ResettableInterface) {
+                $processor->reset();
+            }
+        }
+    }
 }

+ 7 - 2
src/Monolog/Handler/BrowserConsoleHandler.php

@@ -70,14 +70,19 @@ class BrowserConsoleHandler extends AbstractProcessingHandler
             } elseif ($format === 'js') {
                 static::writeOutput(static::generateScript());
             }
-            static::reset();
+            static::resetStatic();
         }
     }
 
+    public function reset()
+    {
+        self::resetStatic();
+    }
+
     /**
      * Forget all logged records
      */
-    public static function reset(): void
+    public static function resetStatic(): void
     {
         static::$records = [];
     }

+ 10 - 0
src/Monolog/Handler/BufferHandler.php

@@ -12,6 +12,7 @@
 namespace Monolog\Handler;
 
 use Monolog\Logger;
+use Monolog\ResettableInterface;
 
 /**
  * Buffers all records until closing the handler and then pass them as batch.
@@ -114,4 +115,13 @@ class BufferHandler extends AbstractHandler implements ProcessableHandlerInterfa
         $this->bufferSize = 0;
         $this->buffer = [];
     }
+
+    public function reset()
+    {
+        parent::reset();
+
+        if ($this->handler instanceof ResettableInterface) {
+            $this->handler->reset();
+        }
+    }
 }

+ 8 - 0
src/Monolog/Handler/FingersCrossedHandler.php

@@ -14,6 +14,7 @@ namespace Monolog\Handler;
 use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy;
 use Monolog\Handler\FingersCrossed\ActivationStrategyInterface;
 use Monolog\Logger;
+use Monolog\ResettableInterface;
 
 /**
  * Buffers all records until a certain level is reached
@@ -147,7 +148,14 @@ class FingersCrossedHandler extends Handler implements ProcessableHandlerInterfa
      */
     public function reset()
     {
+        parent::reset();
+
+        $this->buffer = array();
         $this->buffering = true;
+
+        if ($this->handler instanceof ResettableInterface) {
+            $this->handler->reset();
+        }
     }
 
     /**

+ 12 - 0
src/Monolog/Handler/GroupHandler.php

@@ -12,6 +12,7 @@
 namespace Monolog\Handler;
 
 use Monolog\Formatter\FormatterInterface;
+use Monolog\ResettableInterface;
 
 /**
  * Forwards records to multiple handlers
@@ -90,6 +91,17 @@ class GroupHandler extends Handler implements ProcessableHandlerInterface
         }
     }
 
+    public function reset()
+    {
+        parent::reset();
+
+        foreach ($this->handlers as $handler) {
+            if ($handler instanceof ResettableInterface) {
+                $handler->reset();
+            }
+        }
+    }
+
     /**
      * {@inheritdoc}
      */

+ 9 - 1
src/Monolog/Handler/HandlerWrapper.php

@@ -11,6 +11,7 @@
 
 namespace Monolog\Handler;
 
+use Monolog\ResettableInterface;
 use Monolog\Formatter\FormatterInterface;
 
 /**
@@ -30,7 +31,7 @@ use Monolog\Formatter\FormatterInterface;
  *
  * @author Alexey Karapetov <alexey@karapetov.com>
  */
-class HandlerWrapper implements HandlerInterface, ProcessableHandlerInterface, FormattableHandlerInterface
+class HandlerWrapper implements HandlerInterface, ProcessableHandlerInterface, FormattableHandlerInterface, ResettableInterface
 {
     /**
      * @var HandlerInterface
@@ -127,4 +128,11 @@ class HandlerWrapper implements HandlerInterface, ProcessableHandlerInterface, F
 
         throw new \LogicException('The wrapped handler does not implement ' . FormattableHandlerInterface::class);
     }
+
+    public function reset()
+    {
+        if ($this->handler instanceof ResettableInterface) {
+            return $this->handler->reset();
+        }
+    }
 }

+ 1 - 1
src/Monolog/Handler/RavenHandler.php

@@ -18,7 +18,7 @@ use Raven_Client;
 
 /**
  * Handler to send messages to a Sentry (https://github.com/getsentry/sentry) server
- * using raven-php (https://github.com/getsentry/raven-php)
+ * using sentry-php (https://github.com/getsentry/sentry-php)
  *
  * @author Marc Abramowitz <marc@marc-abramowitz.com>
  */

+ 16 - 1
src/Monolog/Logger.php

@@ -25,7 +25,7 @@ use Throwable;
  *
  * @author Jordi Boggiano <j.boggiano@seld.be>
  */
-class Logger implements LoggerInterface
+class Logger implements LoggerInterface, ResettableInterface
 {
     /**
      * Detailed debug information
@@ -333,6 +333,21 @@ class Logger implements LoggerInterface
         return true;
     }
 
+    public function reset()
+    {
+        foreach ($this->handlers as $handler) {
+            if ($handler instanceof ResettableInterface) {
+                $handler->reset();
+            }
+        }
+
+        foreach ($this->processors as $processor) {
+            if ($processor instanceof ResettableInterface) {
+                $processor->reset();
+            }
+        }
+    }
+
     /**
      * Gets all supported logging levels.
      *

+ 3 - 1
src/Monolog/Processor/PsrLogMessageProcessor.php

@@ -11,6 +11,8 @@
 
 namespace Monolog\Processor;
 
+use Monolog\Utils;
+
 /**
  * Processes a record's message according to PSR-3 rules
  *
@@ -66,7 +68,7 @@ class PsrLogMessageProcessor implements ProcessorInterface
                     $replacements[$placeholder] = $val->format($this->dateFormat ?: static::SIMPLE_DATE);
                 }
             } elseif (is_object($val)) {
-                $replacements[$placeholder] = '[object '.get_class($val).']';
+                $replacements[$placeholder] = '[object '.Utils::getClass($val).']';
             } elseif (is_array($val)) {
                 $replacements[$placeholder] = 'array'.@json_encode($val);
             } else {

+ 14 - 2
src/Monolog/Processor/UidProcessor.php

@@ -11,12 +11,14 @@
 
 namespace Monolog\Processor;
 
+use Monolog\ResettableInterface;
+
 /**
  * Adds a unique identifier into records
  *
  * @author Simon Mönch <sm@webfactory.de>
  */
-class UidProcessor implements ProcessorInterface
+class UidProcessor implements ProcessorInterface, ResettableInterface
 {
     private $uid;
 
@@ -26,7 +28,7 @@ class UidProcessor implements ProcessorInterface
             throw new \InvalidArgumentException('The uid length must be an integer between 1 and 32');
         }
 
-        $this->uid = substr(bin2hex(random_bytes((int) ceil($length / 2))), 0, $length);
+        $this->uid = $this->generateUid($length);
     }
 
     public function __invoke(array $record): array
@@ -43,4 +45,14 @@ class UidProcessor implements ProcessorInterface
     {
         return $this->uid;
     }
+
+    public function reset()
+    {
+        $this->uid = $this->generateUid(strlen($this->uid));
+    }
+
+    private function generateUid($length)
+    {
+        return substr(bin2hex(random_bytes((int) ceil($length / 2))), 0, $length);
+    }
 }

+ 10 - 0
src/Monolog/Registry.php

@@ -130,4 +130,14 @@ class Registry
     {
         return self::getInstance($name);
     }
+
+    /**
+     * @internal
+     */
+    public function getClass($object)
+    {
+        $class = \get_class($object);
+
+        return 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
+    }
 }

+ 25 - 0
src/Monolog/ResettableInterface.php

@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Monolog;
+
+/**
+ * Handler or Processor implementing this interface will be reset when Logger::reset() is called.
+ *
+ * Resetting an Handler or a Processor usually means cleaning all buffers or
+ * resetting in its internal state. This should also generally close() the handler.
+ *
+ * @author Grégoire Pineau <lyrixx@lyrixx.info>
+ */
+interface ResettableInterface
+{
+    public function reset();
+}

+ 115 - 0
src/Monolog/SignalHandler.php

@@ -0,0 +1,115 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Monolog;
+
+use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
+use ReflectionExtension;
+
+/**
+ * Monolog POSIX signal handler
+ *
+ * @author Robert Gust-Bardon <robert@gust-bardon.org>
+ */
+class SignalHandler
+{
+    private $logger;
+
+    private $previousSignalHandler = array();
+    private $signalLevelMap = array();
+    private $signalRestartSyscalls = array();
+
+    public function __construct(LoggerInterface $logger)
+    {
+        $this->logger = $logger;
+    }
+
+    public function registerSignalHandler($signo, $level = LogLevel::CRITICAL, $callPrevious = true, $restartSyscalls = true, $async = true)
+    {
+        if (!extension_loaded('pcntl') || !function_exists('pcntl_signal')) {
+            return $this;
+        }
+
+        if ($callPrevious) {
+            if (function_exists('pcntl_signal_get_handler')) {
+                $handler = pcntl_signal_get_handler($signo);
+                if ($handler === false) {
+                    return $this;
+                }
+                $this->previousSignalHandler[$signo] = $handler;
+            } else {
+                $this->previousSignalHandler[$signo] = true;
+            }
+        } else {
+            unset($this->previousSignalHandler[$signo]);
+        }
+        $this->signalLevelMap[$signo] = $level;
+        $this->signalRestartSyscalls[$signo] = $restartSyscalls;
+
+        if (function_exists('pcntl_async_signals') && $async !== null) {
+            pcntl_async_signals($async);
+        }
+
+        pcntl_signal($signo, array($this, 'handleSignal'), $restartSyscalls);
+
+        return $this;
+    }
+
+    public function handleSignal($signo, array $siginfo = null)
+    {
+        static $signals = array();
+
+        if (!$signals && extension_loaded('pcntl')) {
+            $pcntl = new ReflectionExtension('pcntl');
+            $constants = $pcntl->getConstants();
+            if (!$constants) {
+                // HHVM 3.24.2 returns an empty array.
+                $constants = get_defined_constants(true);
+                $constants = $constants['Core'];
+            }
+            foreach ($constants as $name => $value) {
+                if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_' && is_int($value)) {
+                    $signals[$value] = $name;
+                }
+            }
+            unset($constants);
+        }
+
+        $level = isset($this->signalLevelMap[$signo]) ? $this->signalLevelMap[$signo] : LogLevel::CRITICAL;
+        $signal = isset($signals[$signo]) ? $signals[$signo] : $signo;
+        $context = isset($siginfo) ? $siginfo : array();
+        $this->logger->log($level, sprintf('Program received signal %s', $signal), $context);
+
+        if (!isset($this->previousSignalHandler[$signo])) {
+            return;
+        }
+
+        if ($this->previousSignalHandler[$signo] === true || $this->previousSignalHandler[$signo] === SIG_DFL) {
+            if (extension_loaded('pcntl') && function_exists('pcntl_signal') && function_exists('pcntl_sigprocmask') && function_exists('pcntl_signal_dispatch')
+                && extension_loaded('posix') && function_exists('posix_getpid') && function_exists('posix_kill')) {
+                    $restartSyscalls = isset($this->restartSyscalls[$signo]) ? $this->restartSyscalls[$signo] : true;
+                    pcntl_signal($signo, SIG_DFL, $restartSyscalls);
+                    pcntl_sigprocmask(SIG_UNBLOCK, array($signo), $oldset);
+                    posix_kill(posix_getpid(), $signo);
+                    pcntl_signal_dispatch();
+                    pcntl_sigprocmask(SIG_SETMASK, $oldset);
+                    pcntl_signal($signo, array($this, 'handleSignal'), $restartSyscalls);
+                }
+        } elseif (is_callable($this->previousSignalHandler[$signo])) {
+            if (PHP_VERSION_ID >= 70100) {
+                $this->previousSignalHandler[$signo]($signo, $siginfo);
+            } else {
+                $this->previousSignalHandler[$signo]($signo);
+            }
+        }
+    }
+}

+ 25 - 0
src/Monolog/Utils.php

@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Monolog;
+
+class Utils
+{
+    /**
+     * @internal
+     */
+    public function getClass($object)
+    {
+        $class = \get_class($object);
+
+        return 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
+    }
+}

+ 1 - 1
tests/Monolog/Handler/BrowserConsoleHandlerTest.php

@@ -21,7 +21,7 @@ class BrowserConsoleHandlerTest extends TestCase
 {
     protected function setUp()
     {
-        BrowserConsoleHandler::reset();
+        BrowserConsoleHandler::resetStatic();
     }
 
     protected function generateScript()

+ 2 - 2
tests/Monolog/Handler/ChromePHPHandlerTest.php

@@ -21,7 +21,7 @@ class ChromePHPHandlerTest extends TestCase
 {
     protected function setUp()
     {
-        TestChromePHPHandler::reset();
+        TestChromePHPHandler::resetStatic();
         $_SERVER['HTTP_USER_AGENT'] = 'Monolog Test; Chrome/1.0';
     }
 
@@ -136,7 +136,7 @@ class TestChromePHPHandler extends ChromePHPHandler
 {
     protected $headers = [];
 
-    public static function reset()
+    public static function resetStatic()
     {
         self::$initialized = false;
         self::$overflowed = false;

+ 2 - 2
tests/Monolog/Handler/FingersCrossedHandlerTest.php

@@ -58,7 +58,7 @@ class FingersCrossedHandlerTest extends TestCase
      * @covers Monolog\Handler\FingersCrossedHandler::activate
      * @covers Monolog\Handler\FingersCrossedHandler::reset
      */
-    public function testHandleRestartBufferingAfterReset()
+    public function testHandleResetBufferingAfterReset()
     {
         $test = new TestHandler();
         $handler = new FingersCrossedHandler($test);
@@ -76,7 +76,7 @@ class FingersCrossedHandlerTest extends TestCase
      * @covers Monolog\Handler\FingersCrossedHandler::handle
      * @covers Monolog\Handler\FingersCrossedHandler::activate
      */
-    public function testHandleRestartBufferingAfterBeingTriggeredWhenStopBufferingIsDisabled()
+    public function testHandleResetBufferingAfterBeingTriggeredWhenStopBufferingIsDisabled()
     {
         $test = new TestHandler();
         $handler = new FingersCrossedHandler($test, Logger::WARNING, 0, false, false);

+ 2 - 2
tests/Monolog/Handler/FirePHPHandlerTest.php

@@ -21,7 +21,7 @@ class FirePHPHandlerTest extends TestCase
 {
     public function setUp()
     {
-        TestFirePHPHandler::reset();
+        TestFirePHPHandler::resetStatic();
         $_SERVER['HTTP_USER_AGENT'] = 'Monolog Test; FirePHP/1.0';
     }
 
@@ -77,7 +77,7 @@ class TestFirePHPHandler extends FirePHPHandler
 {
     protected $headers = [];
 
-    public static function reset()
+    public static function resetStatic()
     {
         self::$initialized = false;
         self::$sendHeaders = true;

+ 73 - 0
tests/Monolog/LoggerTest.php

@@ -635,4 +635,77 @@ class LoggerTest extends \PHPUnit\Framework\TestCase
         $logger->pushHandler($handler);
         $logger->info('test');
     }
+
+    public function testReset()
+    {
+        $logger = new Logger('app');
+
+        $testHandler = new Handler\TestHandler();
+        $bufferHandler = new Handler\BufferHandler($testHandler);
+        $groupHandler = new Handler\GroupHandler(array($bufferHandler));
+        $fingersCrossedHandler = new Handler\FingersCrossedHandler($groupHandler);
+
+        $logger->pushHandler($fingersCrossedHandler);
+
+        $processorUid1 = new Processor\UidProcessor(10);
+        $uid1 = $processorUid1->getUid();
+        $groupHandler->pushProcessor($processorUid1);
+
+        $processorUid2 = new Processor\UidProcessor(5);
+        $uid2 = $processorUid2->getUid();
+        $logger->pushProcessor($processorUid2);
+
+        $getProperty = function ($object, $property) {
+            $reflectionProperty = new \ReflectionProperty(get_class($object), $property);
+            $reflectionProperty->setAccessible(true);
+
+            return $reflectionProperty->getValue($object);
+        };
+        $that = $this;
+        $assertBufferOfBufferHandlerEmpty = function () use ($getProperty, $bufferHandler, $that) {
+            $that->assertEmpty($getProperty($bufferHandler, 'buffer'));
+        };
+        $assertBuffersEmpty = function() use ($assertBufferOfBufferHandlerEmpty, $getProperty, $fingersCrossedHandler, $that) {
+            $assertBufferOfBufferHandlerEmpty();
+            $that->assertEmpty($getProperty($fingersCrossedHandler, 'buffer'));
+        };
+
+        $logger->debug('debug');
+        $logger->reset();
+        $assertBuffersEmpty();
+        $this->assertFalse($testHandler->hasDebugRecords());
+        $this->assertFalse($testHandler->hasErrorRecords());
+        $this->assertNotSame($uid1, $uid1 = $processorUid1->getUid());
+        $this->assertNotSame($uid2, $uid2 = $processorUid2->getUid());
+
+        $logger->debug('debug');
+        $logger->error('error');
+        $logger->reset();
+        $assertBuffersEmpty();
+        $this->assertTrue($testHandler->hasDebugRecords());
+        $this->assertTrue($testHandler->hasErrorRecords());
+        $this->assertNotSame($uid1, $uid1 = $processorUid1->getUid());
+        $this->assertNotSame($uid2, $uid2 = $processorUid2->getUid());
+
+        $logger->info('info');
+        $this->assertNotEmpty($getProperty($fingersCrossedHandler, 'buffer'));
+        $assertBufferOfBufferHandlerEmpty();
+        $this->assertFalse($testHandler->hasInfoRecords());
+
+        $logger->reset();
+        $assertBuffersEmpty();
+        $this->assertFalse($testHandler->hasInfoRecords());
+        $this->assertNotSame($uid1, $uid1 = $processorUid1->getUid());
+        $this->assertNotSame($uid2, $uid2 = $processorUid2->getUid());
+
+        $logger->notice('notice');
+        $logger->emergency('emergency');
+        $logger->reset();
+        $assertBuffersEmpty();
+        $this->assertFalse($testHandler->hasInfoRecords());
+        $this->assertTrue($testHandler->hasNoticeRecords());
+        $this->assertTrue($testHandler->hasEmergencyRecords());
+        $this->assertNotSame($uid1, $processorUid1->getUid());
+        $this->assertNotSame($uid2, $processorUid2->getUid());
+    }
 }

+ 287 - 0
tests/Monolog/SignalHandlerTest.php

@@ -0,0 +1,287 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Monolog;
+
+use Monolog\Handler\StreamHandler;
+use Monolog\Handler\TestHandler;
+use Psr\Log\LogLevel;
+
+/**
+ * @author Robert Gust-Bardon <robert@gust-bardon.org>
+ * @covers Monolog\SignalHandler
+ */
+class SignalHandlerTest extends TestCase
+{
+
+    private $asyncSignalHandling;
+    private $blockedSignals;
+    private $signalHandlers;
+
+    protected function setUp()
+    {
+        $this->signalHandlers = array();
+        if (extension_loaded('pcntl')) {
+            if (function_exists('pcntl_async_signals')) {
+                $this->asyncSignalHandling = pcntl_async_signals();
+            }
+            if (function_exists('pcntl_sigprocmask')) {
+                pcntl_sigprocmask(SIG_BLOCK, array(), $this->blockedSignals);
+            }
+        }
+    }
+
+    protected function tearDown()
+    {
+        if ($this->asyncSignalHandling !== null) {
+            pcntl_async_signals($this->asyncSignalHandling);
+        }
+        if ($this->blockedSignals !== null) {
+            pcntl_sigprocmask(SIG_SETMASK, $this->blockedSignals);
+        }
+        if ($this->signalHandlers) {
+            pcntl_signal_dispatch();
+            foreach ($this->signalHandlers as $signo => $handler) {
+                pcntl_signal($signo, $handler);
+            }
+        }
+    }
+
+    private function setSignalHandler($signo, $handler = SIG_DFL) {
+        if (function_exists('pcntl_signal_get_handler')) {
+            $this->signalHandlers[$signo] = pcntl_signal_get_handler($signo);
+        } else {
+            $this->signalHandlers[$signo] = SIG_DFL;
+        }
+        $this->assertTrue(pcntl_signal($signo, $handler));
+    }
+
+    public function testHandleSignal()
+    {
+        $logger = new Logger('test', array($handler = new TestHandler));
+        $errHandler = new SignalHandler($logger);
+        $signo = 2;  // SIGINT.
+        $siginfo = array('signo' => $signo, 'errno' => 0, 'code' => 0);
+        $errHandler->handleSignal($signo, $siginfo);
+        $this->assertCount(1, $handler->getRecords());
+        $this->assertTrue($handler->hasCriticalRecords());
+        $records = $handler->getRecords();
+        $this->assertSame($siginfo, $records[0]['context']);
+    }
+
+    /**
+     * @depends testHandleSignal
+     * @requires extension pcntl
+     * @requires extension posix
+     * @requires function pcntl_signal
+     * @requires function pcntl_signal_dispatch
+     * @requires function posix_getpid
+     * @requires function posix_kill
+     */
+    public function testRegisterSignalHandler()
+    {
+        // SIGCONT and SIGURG should be ignored by default.
+        if (!defined('SIGCONT') || !defined('SIGURG')) {
+            $this->markTestSkipped('This test requires the SIGCONT and SIGURG pcntl constants.');
+        }
+
+        $this->setSignalHandler(SIGCONT, SIG_IGN);
+        $this->setSignalHandler(SIGURG, SIG_IGN);
+
+        $logger = new Logger('test', array($handler = new TestHandler));
+        $errHandler = new SignalHandler($logger);
+        $pid = posix_getpid();
+
+        $this->assertTrue(posix_kill($pid, SIGURG));
+        $this->assertTrue(pcntl_signal_dispatch());
+        $this->assertCount(0, $handler->getRecords());
+
+        $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, false, false, false);
+
+        $this->assertTrue(posix_kill($pid, SIGCONT));
+        $this->assertTrue(pcntl_signal_dispatch());
+        $this->assertCount(0, $handler->getRecords());
+
+        $this->assertTrue(posix_kill($pid, SIGURG));
+        $this->assertTrue(pcntl_signal_dispatch());
+        $this->assertCount(1, $handler->getRecords());
+        $this->assertTrue($handler->hasInfoThatContains('SIGURG'));
+    }
+
+    /**
+     * @dataProvider defaultPreviousProvider
+     * @depends testRegisterSignalHandler
+     * @requires function pcntl_fork
+     * @requires function pcntl_sigprocmask
+     * @requires function pcntl_waitpid
+     */
+    public function testRegisterDefaultPreviousSignalHandler($signo, $callPrevious, $expected)
+    {
+        $this->setSignalHandler($signo, SIG_DFL);
+
+        $path = tempnam(sys_get_temp_dir(), 'monolog-');
+        $this->assertNotFalse($path);
+
+        $pid = pcntl_fork();
+        if ($pid === 0) {  // Child.
+            $streamHandler = new StreamHandler($path);
+            $streamHandler->setFormatter($this->getIdentityFormatter());
+            $logger = new Logger('test', array($streamHandler));
+            $errHandler = new SignalHandler($logger);
+            $errHandler->registerSignalHandler($signo, LogLevel::INFO, $callPrevious, false, false);
+            pcntl_sigprocmask(SIG_SETMASK, array(SIGCONT));
+            posix_kill(posix_getpid(), $signo);
+            pcntl_signal_dispatch();
+            // If $callPrevious is true, SIGINT should terminate by this line.
+            pcntl_sigprocmask(SIG_BLOCK, array(), $oldset);
+            file_put_contents($path, implode(' ', $oldset), FILE_APPEND);
+            posix_kill(posix_getpid(), $signo);
+            pcntl_signal_dispatch();
+            exit();
+        }
+
+        $this->assertNotSame(-1, $pid);
+        $this->assertNotSame(-1, pcntl_waitpid($pid, $status));
+        $this->assertNotSame(-1, $status);
+        $this->assertSame($expected, file_get_contents($path));
+    }
+
+    public function defaultPreviousProvider()
+    {
+        if (!defined('SIGCONT') || !defined('SIGINT') || !defined('SIGURG')) {
+            return array();
+        }
+
+        return array(
+            array(SIGINT, false, 'Program received signal SIGINT'.SIGCONT.'Program received signal SIGINT'),
+            array(SIGINT, true, 'Program received signal SIGINT'),
+            array(SIGURG, false, 'Program received signal SIGURG'.SIGCONT.'Program received signal SIGURG'),
+            array(SIGURG, true, 'Program received signal SIGURG'.SIGCONT.'Program received signal SIGURG'),
+        );
+    }
+
+    /**
+     * @dataProvider callablePreviousProvider
+     * @depends testRegisterSignalHandler
+     * @requires function pcntl_signal_get_handler
+     */
+    public function testRegisterCallablePreviousSignalHandler($callPrevious)
+    {
+        $this->setSignalHandler(SIGURG, SIG_IGN);
+
+        $logger = new Logger('test', array($handler = new TestHandler));
+        $errHandler = new SignalHandler($logger);
+        $previousCalled = 0;
+        pcntl_signal(SIGURG, function ($signo, array $siginfo = null) use (&$previousCalled) {
+            ++$previousCalled;
+        });
+        $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, $callPrevious, false, false);
+        $this->assertTrue(posix_kill(posix_getpid(), SIGURG));
+        $this->assertTrue(pcntl_signal_dispatch());
+        $this->assertCount(1, $handler->getRecords());
+        $this->assertTrue($handler->hasInfoThatContains('SIGURG'));
+        $this->assertSame($callPrevious ? 1 : 0, $previousCalled);
+    }
+
+    public function callablePreviousProvider()
+    {
+        return array(
+            array(false),
+            array(true),
+        );
+    }
+
+    /**
+     * @dataProvider restartSyscallsProvider
+     * @depends testRegisterDefaultPreviousSignalHandler
+     * @requires function pcntl_fork
+     * @requires function pcntl_waitpid
+     */
+    public function testRegisterSyscallRestartingSignalHandler($restartSyscalls)
+    {
+        $this->setSignalHandler(SIGURG, SIG_IGN);
+
+        $parentPid = posix_getpid();
+        $microtime = microtime(true);
+
+        $pid = pcntl_fork();
+        if ($pid === 0) {  // Child.
+            usleep(100000);
+            posix_kill($parentPid, SIGURG);
+            usleep(100000);
+            exit();
+        }
+
+        $this->assertNotSame(-1, $pid);
+        $logger = new Logger('test', array($handler = new TestHandler));
+        $errHandler = new SignalHandler($logger);
+        $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, false, $restartSyscalls, false);
+        if ($restartSyscalls) {
+            // pcntl_wait is expected to be restarted after the signal handler.
+            $this->assertNotSame(-1, pcntl_waitpid($pid, $status));
+        } else {
+            // pcntl_wait is expected to be interrupted when the signal handler is invoked.
+            $this->assertSame(-1, pcntl_waitpid($pid, $status));
+        }
+        $this->assertSame($restartSyscalls, microtime(true) - $microtime > 0.15);
+        $this->assertTrue(pcntl_signal_dispatch());
+        $this->assertCount(1, $handler->getRecords());
+        if ($restartSyscalls) {
+            // The child has already exited.
+            $this->assertSame(-1, pcntl_waitpid($pid, $status));
+        } else {
+            // The child has not exited yet.
+            $this->assertNotSame(-1, pcntl_waitpid($pid, $status));
+        }
+    }
+
+    public function restartSyscallsProvider()
+    {
+        return array(
+            array(false),
+            array(true),
+            array(false),
+            array(true),
+        );
+    }
+
+    /**
+     * @dataProvider asyncProvider
+     * @depends testRegisterDefaultPreviousSignalHandler
+     * @requires function pcntl_async_signals
+     */
+    public function testRegisterAsyncSignalHandler($initialAsync, $desiredAsync, $expectedBefore, $expectedAfter)
+    {
+        $this->setSignalHandler(SIGURG, SIG_IGN);
+        pcntl_async_signals($initialAsync);
+
+        $logger = new Logger('test', array($handler = new TestHandler));
+        $errHandler = new SignalHandler($logger);
+        $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, false, false, $desiredAsync);
+        $this->assertTrue(posix_kill(posix_getpid(), SIGURG));
+        $this->assertCount($expectedBefore, $handler->getRecords());
+        $this->assertTrue(pcntl_signal_dispatch());
+        $this->assertCount($expectedAfter, $handler->getRecords());
+    }
+
+    public function asyncProvider()
+    {
+        return array(
+            array(false, false, 0, 1),
+            array(false, null, 0, 1),
+            array(false, true, 1, 1),
+            array(true, false, 0, 1),
+            array(true, null, 1, 1),
+            array(true, true, 1, 1),
+        );
+    }
+
+}