bug #14999 [2.6][Debug] Fix fatal-errors handling on HHVM (nicolas-grekas, digitalkaoz)
This PR was merged into the 2.6 branch. Discussion ---------- [2.6][Debug] Fix fatal-errors handling on HHVM | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #14967 | License | MIT | Doc PR | - Commits -------9f346a5
Add test for HHVM FatalErrors445de5e
[2.6][Debug] Fix fatal-errors handling on HHVM
This commit is contained in:
commit
427605a5be
@ -350,7 +350,7 @@ class ErrorHandler
|
|||||||
*
|
*
|
||||||
* @internal
|
* @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;
|
$level = error_reporting() | E_RECOVERABLE_ERROR | E_USER_ERROR;
|
||||||
$log = $this->loggedErrors & $type;
|
$log = $this->loggedErrors & $type;
|
||||||
@ -367,6 +367,15 @@ class ErrorHandler
|
|||||||
$context = $e;
|
$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 ($throw) {
|
||||||
if (($this->scopedErrors & $type) && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) {
|
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
|
// Checking for class existence is a work around for https://bugs.php.net/42098
|
||||||
@ -402,10 +411,17 @@ class ErrorHandler
|
|||||||
if ($this->scopedErrors & $type) {
|
if ($this->scopedErrors & $type) {
|
||||||
$e['scope_vars'] = $context;
|
$e['scope_vars'] = $context;
|
||||||
if ($trace) {
|
if ($trace) {
|
||||||
$e['stack'] = debug_backtrace(true); // Provide object
|
$e['stack'] = $backtrace ?: debug_backtrace(true); // Provide object
|
||||||
}
|
}
|
||||||
} elseif ($trace) {
|
} 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)
|
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 = set_error_handler('var_dump', 0);
|
||||||
$handler = is_array($handler) ? $handler[0] : null;
|
$handler = is_array($handler) ? $handler[0] : null;
|
||||||
@ -527,14 +547,15 @@ class ErrorHandler
|
|||||||
// Handled below
|
// 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
|
// Let's not throw anymore but keep logging
|
||||||
$handler->throwAt(0, true);
|
$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')) {
|
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 {
|
} 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)) {
|
} elseif (!isset($exception)) {
|
||||||
return;
|
return;
|
||||||
|
@ -35,11 +35,19 @@ use Symfony\Component\HttpKernel\Exception\FatalErrorException as LegacyFatalErr
|
|||||||
*/
|
*/
|
||||||
class FatalErrorException extends LegacyFatalErrorException
|
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);
|
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')) {
|
if (function_exists('xdebug_get_function_stack')) {
|
||||||
$trace = xdebug_get_function_stack();
|
$trace = xdebug_get_function_stack();
|
||||||
if (0 < $traceOffset) {
|
if (0 < $traceOffset) {
|
||||||
@ -48,7 +56,7 @@ class FatalErrorException extends LegacyFatalErrorException
|
|||||||
|
|
||||||
foreach ($trace as &$frame) {
|
foreach ($trace as &$frame) {
|
||||||
if (!isset($frame['type'])) {
|
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'])) {
|
if (isset($frame['class'])) {
|
||||||
$frame['type'] = '::';
|
$frame['type'] = '::';
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,9 @@ class DebugClassLoaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
if (PHP_VERSION_ID >= 70000) {
|
if (PHP_VERSION_ID >= 70000) {
|
||||||
$this->markTestSkipped('PHP7 throws exceptions, unsilencing is not required anymore.');
|
$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();
|
ob_start();
|
||||||
|
|
||||||
@ -86,6 +89,9 @@ class DebugClassLoaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
if (class_exists('Symfony\Component\Debug\Exception\ContextErrorException', false)) {
|
if (class_exists('Symfony\Component\Debug\Exception\ContextErrorException', false)) {
|
||||||
$this->markTestSkipped('The ContextErrorException class is already loaded.');
|
$this->markTestSkipped('The ContextErrorException class is already loaded.');
|
||||||
}
|
}
|
||||||
|
if (defined('HHVM_VERSION')) {
|
||||||
|
$this->markTestSkipped('HHVM is not handled in this test case.');
|
||||||
|
}
|
||||||
|
|
||||||
ErrorHandler::register();
|
ErrorHandler::register();
|
||||||
|
|
||||||
|
@ -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
|
* @group legacy
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user