From 445de5e828cec8fdd222349839fd39bc20241cba Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 16 Jun 2015 14:15:02 +0200 Subject: [PATCH 1/2] [2.6][Debug] Fix fatal-errors handling on HHVM --- src/Symfony/Component/Debug/ErrorHandler.php | 35 +++++++++++++++---- .../Debug/Exception/FatalErrorException.php | 14 ++++++-- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 91a59058dc..f315a536ea 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -350,7 +350,7 @@ class ErrorHandler * * @internal */ - public function handleError($type, $message, $file, $line, array $context) + public function handleError($type, $message, $file, $line, array $context, array $backtrace = null) { $level = error_reporting() | E_RECOVERABLE_ERROR | E_USER_ERROR; $log = $this->loggedErrors & $type; @@ -367,6 +367,15 @@ class ErrorHandler $context = $e; } + if (null !== $backtrace && $type & E_ERROR) { + // E_ERROR fatal errors are triggered on HHVM when + // hhvm.error_handling.call_user_handler_on_fatals=1 + // which is the way to get their backtrace. + $this->handleFatalError(compact('type', 'message', 'file', 'line', 'backtrace')); + + return true; + } + if ($throw) { if (($this->scopedErrors & $type) && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) { // Checking for class existence is a work around for https://bugs.php.net/42098 @@ -402,10 +411,17 @@ class ErrorHandler if ($this->scopedErrors & $type) { $e['scope_vars'] = $context; if ($trace) { - $e['stack'] = debug_backtrace(true); // Provide object + $e['stack'] = $backtrace ?: debug_backtrace(true); // Provide object } } elseif ($trace) { - $e['stack'] = debug_backtrace(PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_IGNORE_ARGS : false); + if (null === $backtrace) { + $e['stack'] = debug_backtrace(PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_IGNORE_ARGS : false); + } else { + foreach ($backtrace as &$frame) { + unset($frame['args'], $frame); + } + $e['stack'] = $backtrace; + } } } @@ -505,7 +521,11 @@ class ErrorHandler */ public static function handleFatalError(array $error = null) { - self::$reservedMemory = ''; + if (null === self::$reservedMemory) { + return; + } + + self::$reservedMemory = null; $handler = set_error_handler('var_dump', 0); $handler = is_array($handler) ? $handler[0] : null; @@ -527,14 +547,15 @@ class ErrorHandler // Handled below } - if ($error && ($error['type'] & (E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR))) { + if ($error && $error['type'] &= E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR) { // Let's not throw anymore but keep logging $handler->throwAt(0, true); + $trace = isset($error['backtrace']) ? $error['backtrace'] : null; if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) { - $exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false); + $exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false, $trace); } else { - $exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true); + $exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true, $trace); } } elseif (!isset($exception)) { return; diff --git a/src/Symfony/Component/Debug/Exception/FatalErrorException.php b/src/Symfony/Component/Debug/Exception/FatalErrorException.php index d142051ba9..f46e20875a 100644 --- a/src/Symfony/Component/Debug/Exception/FatalErrorException.php +++ b/src/Symfony/Component/Debug/Exception/FatalErrorException.php @@ -35,11 +35,19 @@ use Symfony\Component\HttpKernel\Exception\FatalErrorException as LegacyFatalErr */ class FatalErrorException extends LegacyFatalErrorException { - public function __construct($message, $code, $severity, $filename, $lineno, $traceOffset = null, $traceArgs = true) + public function __construct($message, $code, $severity, $filename, $lineno, $traceOffset = null, $traceArgs = true, array $trace = null) { parent::__construct($message, $code, $severity, $filename, $lineno); - if (null !== $traceOffset) { + if (null !== $trace) { + if (!$traceArgs) { + foreach ($trace as &$frame) { + unset($frame['args'], $frame['this'], $frame); + } + } + + $this->setTrace($trace); + } elseif (null !== $traceOffset) { if (function_exists('xdebug_get_function_stack')) { $trace = xdebug_get_function_stack(); if (0 < $traceOffset) { @@ -48,7 +56,7 @@ class FatalErrorException extends LegacyFatalErrorException foreach ($trace as &$frame) { if (!isset($frame['type'])) { - // XDebug pre 2.1.1 doesn't currently set the call type key http://bugs.xdebug.org/view.php?id=695 + // XDebug pre 2.1.1 doesn't currently set the call type key http://bugs.xdebug.org/view.php?id=695 if (isset($frame['class'])) { $frame['type'] = '::'; } From 9f346a5a8d35f2176a04f5b2bf302ff0f5f3cbde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=B6nthal?= Date: Fri, 12 Jun 2015 23:50:21 +0200 Subject: [PATCH 2/2] Add test for HHVM FatalErrors --- .../Debug/Tests/DebugClassLoaderTest.php | 6 +++ .../Debug/Tests/ErrorHandlerTest.php | 46 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index f720b35958..8435397f66 100644 --- a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -64,6 +64,9 @@ class DebugClassLoaderTest extends \PHPUnit_Framework_TestCase if (PHP_VERSION_ID >= 70000) { $this->markTestSkipped('PHP7 throws exceptions, unsilencing is not required anymore.'); } + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('HHVM is not handled in this test case.'); + } ob_start(); @@ -86,6 +89,9 @@ class DebugClassLoaderTest extends \PHPUnit_Framework_TestCase if (class_exists('Symfony\Component\Debug\Exception\ContextErrorException', false)) { $this->markTestSkipped('The ContextErrorException class is already loaded.'); } + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('HHVM is not handled in this test case.'); + } ErrorHandler::register(); diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index 19e7a7def7..033b1c7f72 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -393,6 +393,52 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase } } + public function testHandleFatalErrorOnHHVM() + { + try { + $handler = ErrorHandler::register(); + + $logger = $this->getMock('Psr\Log\LoggerInterface'); + $logger + ->expects($this->once()) + ->method('log') + ->with( + $this->equalTo(LogLevel::EMERGENCY), + $this->equalTo('Fatal Error: foo'), + $this->equalTo(array( + 'type' => 1, + 'file' => 'bar', + 'line' => 123, + 'level' => -1, + 'stack' => array(456), + )) + ) + ; + + $handler->setDefaultLogger($logger, E_ERROR); + + $error = array( + 'type' => E_ERROR + 0x1000000, // This error level is used by HHVM for fatal errors + 'message' => 'foo', + 'file' => 'bar', + 'line' => 123, + 'context' => array(123), + 'backtrace' => array(456), + ); + + call_user_func_array(array($handler, 'handleError'), $error); + $handler->handleFatalError($error); + + restore_error_handler(); + restore_exception_handler(); + } catch (\Exception $e) { + restore_error_handler(); + restore_exception_handler(); + + throw $e; + } + } + /** * @group legacy */