2020-02-12 14:24:11 -08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Improve PHP error logs and optionally route errors, exceptions and debugging
|
|
|
|
* information to a central listener.
|
|
|
|
*
|
|
|
|
* This class takes over the PHP error and exception handlers when you call
|
|
|
|
* ##PhutilErrorHandler::initialize()## and forwards all debugging information
|
2023-11-24 18:58:36 +02:00
|
|
|
* to a listener you install with ##PhutilErrorHandler::addErrorListener()##.
|
2020-02-12 14:24:11 -08:00
|
|
|
*
|
|
|
|
* To use PhutilErrorHandler, which will enhance the messages printed to the
|
|
|
|
* PHP error log, just initialize it:
|
|
|
|
*
|
|
|
|
* PhutilErrorHandler::initialize();
|
|
|
|
*
|
|
|
|
* To additionally install a custom listener which can print error information
|
|
|
|
* to some other file or console, register a listener:
|
|
|
|
*
|
2023-11-24 18:58:36 +02:00
|
|
|
* PhutilErrorHandler::addErrorListener($some_callback);
|
2020-02-12 14:24:11 -08:00
|
|
|
*
|
|
|
|
* For information on writing an error listener, see
|
|
|
|
* @{function:phutil_error_listener_example}. Providing a listener is optional,
|
|
|
|
* you will benefit from improved error logs even without one.
|
|
|
|
*
|
|
|
|
* Phabricator uses this class to drive the DarkConsole "Error Log" plugin.
|
|
|
|
*
|
|
|
|
* @task config Configuring Error Dispatch
|
|
|
|
* @task exutil Exception Utilities
|
|
|
|
* @task trap Error Traps
|
|
|
|
* @task internal Internals
|
|
|
|
*/
|
|
|
|
final class PhutilErrorHandler extends Phobject {
|
|
|
|
|
2023-11-24 18:58:36 +02:00
|
|
|
private static $errorListeners = array();
|
2020-02-12 14:24:11 -08:00
|
|
|
private static $initialized = false;
|
|
|
|
private static $traps = array();
|
|
|
|
|
|
|
|
const EXCEPTION = 'exception';
|
|
|
|
const ERROR = 'error';
|
|
|
|
const PHLOG = 'phlog';
|
|
|
|
const DEPRECATED = 'deprecated';
|
|
|
|
|
|
|
|
|
|
|
|
/* -( Configuring Error Dispatch )----------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Registers this class as the PHP error and exception handler. This will
|
|
|
|
* overwrite any previous handlers!
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
* @task config
|
|
|
|
*/
|
|
|
|
public static function initialize() {
|
|
|
|
self::$initialized = true;
|
|
|
|
set_error_handler(array(__CLASS__, 'handleError'));
|
|
|
|
set_exception_handler(array(__CLASS__, 'handleException'));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Provide an optional listener callback which will receive all errors,
|
|
|
|
* exceptions and debugging messages. It can then print them to a web console,
|
|
|
|
* for example.
|
|
|
|
*
|
|
|
|
* See @{function:phutil_error_listener_example} for details about the
|
|
|
|
* callback parameters and operation.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
* @task config
|
|
|
|
*/
|
2023-11-24 18:58:36 +02:00
|
|
|
public static function addErrorListener($listener) {
|
|
|
|
self::$errorListeners[] = $listener;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deprecated - use `addErrorListener`.
|
|
|
|
*/
|
2020-02-12 14:24:11 -08:00
|
|
|
public static function setErrorListener($listener) {
|
2023-11-24 18:58:36 +02:00
|
|
|
self::addErrorListener($listener);
|
2020-02-12 14:24:11 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* -( Exception Utilities )------------------------------------------------ */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the previous exception of a nested exception. Prior to PHP 5.3 you
|
|
|
|
* can use @{class:PhutilProxyException} to nest exceptions; after PHP 5.3
|
|
|
|
* all exceptions are nestable.
|
|
|
|
*
|
2024-08-23 12:55:36 +02:00
|
|
|
* @param Exception|Throwable $ex Exception to unnest.
|
2020-02-12 14:24:11 -08:00
|
|
|
* @return Exception|Throwable|null Previous exception, if one exists.
|
|
|
|
* @task exutil
|
|
|
|
*/
|
|
|
|
public static function getPreviousException($ex) {
|
|
|
|
if (method_exists($ex, 'getPrevious')) {
|
|
|
|
return $ex->getPrevious();
|
|
|
|
}
|
|
|
|
if (method_exists($ex, 'getPreviousException')) {
|
|
|
|
return $ex->getPreviousException();
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the most deeply nested exception from a possibly-nested exception.
|
|
|
|
*
|
2024-08-23 12:55:36 +02:00
|
|
|
* @param Exception|Throwable $ex A possibly-nested exception.
|
2020-02-12 14:24:11 -08:00
|
|
|
* @return Exception|Throwable Deepest exception in the nest.
|
|
|
|
* @task exutil
|
|
|
|
*/
|
|
|
|
public static function getRootException($ex) {
|
|
|
|
$root = $ex;
|
|
|
|
while (self::getPreviousException($root)) {
|
|
|
|
$root = self::getPreviousException($root);
|
|
|
|
}
|
|
|
|
return $root;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* -( Trapping Errors )---------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds an error trap. Normally you should not invoke this directly;
|
|
|
|
* @{class:PhutilErrorTrap} registers itself on construction.
|
|
|
|
*
|
2024-08-23 12:55:36 +02:00
|
|
|
* @param PhutilErrorTrap $trap Trap to add.
|
2020-02-12 14:24:11 -08:00
|
|
|
* @return void
|
|
|
|
* @task trap
|
|
|
|
*/
|
|
|
|
public static function addErrorTrap(PhutilErrorTrap $trap) {
|
|
|
|
$key = $trap->getTrapKey();
|
|
|
|
self::$traps[$key] = $trap;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes an error trap. Normally you should not invoke this directly;
|
|
|
|
* @{class:PhutilErrorTrap} deregisters itself on destruction.
|
|
|
|
*
|
2024-08-23 12:55:36 +02:00
|
|
|
* @param PhutilErrorTrap $trap Trap to remove.
|
2020-02-12 14:24:11 -08:00
|
|
|
* @return void
|
|
|
|
* @task trap
|
|
|
|
*/
|
|
|
|
public static function removeErrorTrap(PhutilErrorTrap $trap) {
|
|
|
|
$key = $trap->getTrapKey();
|
|
|
|
unset(self::$traps[$key]);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* -( Internals )---------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine if PhutilErrorHandler has been initialized.
|
|
|
|
*
|
|
|
|
* @return bool True if initialized.
|
|
|
|
* @task internal
|
|
|
|
*/
|
|
|
|
public static function hasInitialized() {
|
|
|
|
return self::$initialized;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles PHP errors and dispatches them forward. This is a callback for
|
|
|
|
* ##set_error_handler()##. You should not call this function directly; use
|
|
|
|
* @{function:phlog} to print debugging messages or ##trigger_error()## to
|
|
|
|
* trigger PHP errors.
|
|
|
|
*
|
|
|
|
* This handler converts E_RECOVERABLE_ERROR messages from violated typehints
|
|
|
|
* into @{class:InvalidArgumentException}s.
|
|
|
|
*
|
|
|
|
* This handler converts other E_RECOVERABLE_ERRORs into
|
|
|
|
* @{class:RuntimeException}s.
|
|
|
|
*
|
|
|
|
* This handler converts E_NOTICE messages from uses of undefined variables
|
|
|
|
* into @{class:RuntimeException}s.
|
|
|
|
*
|
2024-08-23 12:55:36 +02:00
|
|
|
* @param int $num Error code.
|
|
|
|
* @param string $str Error message.
|
|
|
|
* @param string $file File where the error occurred.
|
|
|
|
* @param int $line Line on which the error occurred.
|
|
|
|
* @param wild $ctx (optional) Error context information.
|
2020-02-12 14:24:11 -08:00
|
|
|
* @return void
|
|
|
|
* @task internal
|
|
|
|
*/
|
2021-01-11 02:02:16 +00:00
|
|
|
public static function handleError($num, $str, $file, $line, $ctx = null) {
|
2020-02-12 14:24:11 -08:00
|
|
|
foreach (self::$traps as $trap) {
|
2021-01-11 02:02:16 +00:00
|
|
|
$trap->addError($num, $str, $file, $line);
|
2020-02-12 14:24:11 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
if ((error_reporting() & $num) == 0) {
|
|
|
|
// Respect the use of "@" to silence warnings: if this error was
|
|
|
|
// emitted from a context where "@" was in effect, the
|
|
|
|
// value returned by error_reporting() will be 0. This is the
|
|
|
|
// recommended way to check for this, see set_error_handler() docs
|
|
|
|
// on php.net.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-03-20 12:33:25 -07:00
|
|
|
// See T13499. If this is a user error arising from "trigger_error()" or
|
|
|
|
// similar, route it through normal error handling: this is probably the
|
|
|
|
// best match to authorial intent, since the code could choose to throw
|
|
|
|
// an exception instead if it wanted that behavior. Phabricator does not
|
|
|
|
// use "trigger_error()" so we never normally expect to reach this
|
|
|
|
// block in first-party code.
|
|
|
|
|
|
|
|
if (($num === E_USER_ERROR) ||
|
|
|
|
($num === E_USER_WARNING) ||
|
Error handling: send Deprecation messages as explicit Event
Summary:
Ref T15554. When a deprecation warning is captured here, mark it as such and send using the same channel as error messages.
Error Handlers will generally ignore it now, so they'll need to be updated, e.g. D25386
Test Plan:
Hitting a `strlen(null)` before This Change:
- Web:
- PhutilAggregateException - white boxes with red border.
- Daemons:
- trace in daemon log, task fails. Daemon sleeps for 5 seconds.
- Arcanist and Scripts in phorge/bin/ and phorge/scripts:
- execution blows up with error trace.
- SSH server-side scripts (ssh-auth and ssh-exec):
- trace in configured log, execution fails
- SSH client-side scripts (ssh-connect):
- execution blows up with error trace.
After this change:
- Web:
- Before `D25386`: Nothing on screen, errors show in log.
- With `D25386`: logs + dark console.
- Daemons:
- trace in daemon log, task completes successfully.
- Arcanist and Scripts in phorge/bin/ and phorge/scripts/ :
- Error trace printed to STDERR, execution continues.
- SSH server-side scripts (ssh-auth and ssh-exec):
- trace in configured log, execution continues.
- SSH client-side scripts (ssh-connect):
- Error trace printed to STDERR, execution continues.
Reviewers: O1 Blessed Committers, valerio.bozzolan
Reviewed By: O1 Blessed Committers, valerio.bozzolan
Subscribers: speck, tobiaswiese, valerio.bozzolan, Matthew, Cigaryno
Maniphest Tasks: T15554
Differential Revision: https://we.phorge.it/D25387
2023-08-31 08:12:23 -07:00
|
|
|
($num === E_USER_NOTICE) ||
|
|
|
|
($num === E_DEPRECATED)) {
|
|
|
|
|
|
|
|
// See T15554 - we special-case E_DEPRECATED because we don't want them
|
|
|
|
// to kill the process.
|
|
|
|
$level = ($num === E_DEPRECATED) ? self::DEPRECATED : self::ERROR;
|
2020-03-20 12:33:25 -07:00
|
|
|
|
|
|
|
$trace = debug_backtrace();
|
|
|
|
array_shift($trace);
|
|
|
|
self::dispatchErrorMessage(
|
Error handling: send Deprecation messages as explicit Event
Summary:
Ref T15554. When a deprecation warning is captured here, mark it as such and send using the same channel as error messages.
Error Handlers will generally ignore it now, so they'll need to be updated, e.g. D25386
Test Plan:
Hitting a `strlen(null)` before This Change:
- Web:
- PhutilAggregateException - white boxes with red border.
- Daemons:
- trace in daemon log, task fails. Daemon sleeps for 5 seconds.
- Arcanist and Scripts in phorge/bin/ and phorge/scripts:
- execution blows up with error trace.
- SSH server-side scripts (ssh-auth and ssh-exec):
- trace in configured log, execution fails
- SSH client-side scripts (ssh-connect):
- execution blows up with error trace.
After this change:
- Web:
- Before `D25386`: Nothing on screen, errors show in log.
- With `D25386`: logs + dark console.
- Daemons:
- trace in daemon log, task completes successfully.
- Arcanist and Scripts in phorge/bin/ and phorge/scripts/ :
- Error trace printed to STDERR, execution continues.
- SSH server-side scripts (ssh-auth and ssh-exec):
- trace in configured log, execution continues.
- SSH client-side scripts (ssh-connect):
- Error trace printed to STDERR, execution continues.
Reviewers: O1 Blessed Committers, valerio.bozzolan
Reviewed By: O1 Blessed Committers, valerio.bozzolan
Subscribers: speck, tobiaswiese, valerio.bozzolan, Matthew, Cigaryno
Maniphest Tasks: T15554
Differential Revision: https://we.phorge.it/D25387
2023-08-31 08:12:23 -07:00
|
|
|
$level,
|
2020-03-20 12:33:25 -07:00
|
|
|
$str,
|
|
|
|
array(
|
|
|
|
'file' => $file,
|
|
|
|
'line' => $line,
|
|
|
|
'error_code' => $num,
|
|
|
|
'trace' => $trace,
|
|
|
|
));
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-02-12 14:24:11 -08:00
|
|
|
// Convert typehint failures into exceptions.
|
|
|
|
if (preg_match('/^Argument (\d+) passed to (\S+) must be/', $str)) {
|
|
|
|
throw new InvalidArgumentException($str);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert other E_RECOVERABLE_ERRORs into generic runtime exceptions.
|
|
|
|
if ($num == E_RECOVERABLE_ERROR) {
|
|
|
|
throw new RuntimeException($str);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert uses of undefined variables into exceptions.
|
|
|
|
if (preg_match('/^Undefined variable: /', $str)) {
|
|
|
|
throw new RuntimeException($str);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert uses of undefined properties into exceptions.
|
|
|
|
if (preg_match('/^Undefined property: /', $str)) {
|
|
|
|
throw new RuntimeException($str);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert undefined constants into exceptions. Usually this means there
|
|
|
|
// is a missing `$` and the program is horribly broken.
|
|
|
|
if (preg_match('/^Use of undefined constant /', $str)) {
|
|
|
|
throw new RuntimeException($str);
|
|
|
|
}
|
|
|
|
|
2020-03-20 12:33:25 -07:00
|
|
|
// Convert undefined indexes into exceptions.
|
|
|
|
if (preg_match('/^Undefined index: /', $str)) {
|
|
|
|
throw new RuntimeException($str);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert undefined offsets into exceptions.
|
|
|
|
if (preg_match('/^Undefined offset: /', $str)) {
|
|
|
|
throw new RuntimeException($str);
|
|
|
|
}
|
|
|
|
|
|
|
|
// See T13499. Convert all other runtime errors not handled in a more
|
|
|
|
// specific way into runtime exceptions.
|
|
|
|
throw new RuntimeException($str);
|
2020-02-12 14:24:11 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles PHP exceptions and dispatches them forward. This is a callback for
|
|
|
|
* ##set_exception_handler()##. You should not call this function directly;
|
|
|
|
* to print exceptions, pass the exception object to @{function:phlog}.
|
|
|
|
*
|
2024-08-23 12:55:36 +02:00
|
|
|
* @param Exception|Throwable $ex Uncaught exception object.
|
2020-02-12 14:24:11 -08:00
|
|
|
* @return void
|
|
|
|
* @task internal
|
|
|
|
*/
|
|
|
|
public static function handleException($ex) {
|
|
|
|
self::dispatchErrorMessage(
|
|
|
|
self::EXCEPTION,
|
|
|
|
$ex,
|
|
|
|
array(
|
|
|
|
'file' => $ex->getFile(),
|
|
|
|
'line' => $ex->getLine(),
|
|
|
|
'trace' => self::getExceptionTrace($ex),
|
|
|
|
'catch_trace' => debug_backtrace(),
|
|
|
|
));
|
|
|
|
|
|
|
|
// Normally, PHP exits with code 255 after an uncaught exception is thrown.
|
|
|
|
// However, if we install an exception handler (as we have here), it exits
|
|
|
|
// with code 0 instead. Script execution terminates after this function
|
|
|
|
// exits in either case, so exit explicitly with the correct exit code.
|
|
|
|
exit(255);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Output a stacktrace to the PHP error log.
|
|
|
|
*
|
2024-08-23 12:55:36 +02:00
|
|
|
* @param trace $trace A stacktrace, e.g. from debug_backtrace();
|
2020-02-12 14:24:11 -08:00
|
|
|
* @return void
|
|
|
|
* @task internal
|
|
|
|
*/
|
|
|
|
public static function outputStacktrace($trace) {
|
|
|
|
$lines = explode("\n", self::formatStacktrace($trace));
|
|
|
|
foreach ($lines as $line) {
|
|
|
|
error_log($line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Format a stacktrace for output.
|
|
|
|
*
|
2024-08-23 12:55:36 +02:00
|
|
|
* @param trace $trace A stacktrace, e.g. from debug_backtrace();
|
2020-02-12 14:24:11 -08:00
|
|
|
* @return string Human-readable trace.
|
|
|
|
* @task internal
|
|
|
|
*/
|
|
|
|
public static function formatStacktrace($trace) {
|
|
|
|
$result = array();
|
|
|
|
|
|
|
|
$libinfo = self::getLibraryVersions();
|
|
|
|
if ($libinfo) {
|
|
|
|
foreach ($libinfo as $key => $dict) {
|
|
|
|
$info = array();
|
|
|
|
foreach ($dict as $dkey => $dval) {
|
|
|
|
$info[] = $dkey.'='.$dval;
|
|
|
|
}
|
|
|
|
$libinfo[$key] = $key.'('.implode(', ', $info).')';
|
|
|
|
}
|
|
|
|
$result[] = implode(', ', $libinfo);
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($trace as $key => $entry) {
|
|
|
|
$line = ' #'.$key.' ';
|
|
|
|
if (!empty($entry['xid'])) {
|
|
|
|
if ($entry['xid'] != 1) {
|
|
|
|
$line .= '<#'.$entry['xid'].'> ';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (isset($entry['class'])) {
|
|
|
|
$line .= $entry['class'].'::';
|
|
|
|
}
|
|
|
|
$line .= idx($entry, 'function', '');
|
|
|
|
|
|
|
|
if (isset($entry['args'])) {
|
|
|
|
$args = array();
|
|
|
|
foreach ($entry['args'] as $arg) {
|
|
|
|
|
|
|
|
// NOTE: Print out object types, not values. Values sometimes contain
|
|
|
|
// sensitive information and are usually not particularly helpful
|
|
|
|
// for debugging.
|
|
|
|
|
|
|
|
$type = (gettype($arg) == 'object')
|
|
|
|
? get_class($arg)
|
|
|
|
: gettype($arg);
|
|
|
|
$args[] = $type;
|
|
|
|
}
|
|
|
|
$line .= '('.implode(', ', $args).')';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($entry['file'])) {
|
|
|
|
$file = self::adjustFilePath($entry['file']);
|
|
|
|
$line .= ' called at ['.$file.':'.$entry['line'].']';
|
|
|
|
}
|
|
|
|
|
|
|
|
$result[] = $line;
|
|
|
|
}
|
|
|
|
return implode("\n", $result);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* All different types of error messages come here before they are
|
|
|
|
* dispatched to the listener; this method also prints them to the PHP error
|
|
|
|
* log.
|
|
|
|
*
|
2024-08-23 12:55:36 +02:00
|
|
|
* @param const $event Event type constant.
|
|
|
|
* @param wild $value Event value.
|
|
|
|
* @param dict $metadata Event metadata.
|
2020-02-12 14:24:11 -08:00
|
|
|
* @return void
|
|
|
|
* @task internal
|
|
|
|
*/
|
|
|
|
public static function dispatchErrorMessage($event, $value, $metadata) {
|
2021-12-09 12:40:00 -08:00
|
|
|
$timestamp = date('Y-m-d H:i:s');
|
2020-02-12 14:24:11 -08:00
|
|
|
|
|
|
|
switch ($event) {
|
Error handling: send Deprecation messages as explicit Event
Summary:
Ref T15554. When a deprecation warning is captured here, mark it as such and send using the same channel as error messages.
Error Handlers will generally ignore it now, so they'll need to be updated, e.g. D25386
Test Plan:
Hitting a `strlen(null)` before This Change:
- Web:
- PhutilAggregateException - white boxes with red border.
- Daemons:
- trace in daemon log, task fails. Daemon sleeps for 5 seconds.
- Arcanist and Scripts in phorge/bin/ and phorge/scripts:
- execution blows up with error trace.
- SSH server-side scripts (ssh-auth and ssh-exec):
- trace in configured log, execution fails
- SSH client-side scripts (ssh-connect):
- execution blows up with error trace.
After this change:
- Web:
- Before `D25386`: Nothing on screen, errors show in log.
- With `D25386`: logs + dark console.
- Daemons:
- trace in daemon log, task completes successfully.
- Arcanist and Scripts in phorge/bin/ and phorge/scripts/ :
- Error trace printed to STDERR, execution continues.
- SSH server-side scripts (ssh-auth and ssh-exec):
- trace in configured log, execution continues.
- SSH client-side scripts (ssh-connect):
- Error trace printed to STDERR, execution continues.
Reviewers: O1 Blessed Committers, valerio.bozzolan
Reviewed By: O1 Blessed Committers, valerio.bozzolan
Subscribers: speck, tobiaswiese, valerio.bozzolan, Matthew, Cigaryno
Maniphest Tasks: T15554
Differential Revision: https://we.phorge.it/D25387
2023-08-31 08:12:23 -07:00
|
|
|
case self::DEPRECATED:
|
2020-02-12 14:24:11 -08:00
|
|
|
case self::ERROR:
|
|
|
|
$default_message = sprintf(
|
|
|
|
'[%s] ERROR %d: %s at [%s:%d]',
|
|
|
|
$timestamp,
|
|
|
|
$metadata['error_code'],
|
|
|
|
$value,
|
|
|
|
$metadata['file'],
|
|
|
|
$metadata['line']);
|
|
|
|
|
|
|
|
$metadata['default_message'] = $default_message;
|
|
|
|
error_log($default_message);
|
|
|
|
self::outputStacktrace($metadata['trace']);
|
|
|
|
break;
|
|
|
|
case self::EXCEPTION:
|
|
|
|
$messages = array();
|
|
|
|
$current = $value;
|
|
|
|
do {
|
|
|
|
$messages[] = '('.get_class($current).') '.$current->getMessage();
|
|
|
|
} while ($current = self::getPreviousException($current));
|
|
|
|
$messages = implode(' {>} ', $messages);
|
|
|
|
|
|
|
|
if (strlen($messages) > 4096) {
|
|
|
|
$messages = substr($messages, 0, 4096).'...';
|
|
|
|
}
|
|
|
|
|
|
|
|
$default_message = sprintf(
|
|
|
|
'[%s] EXCEPTION: %s at [%s:%d]',
|
|
|
|
$timestamp,
|
|
|
|
$messages,
|
|
|
|
self::adjustFilePath(self::getRootException($value)->getFile()),
|
|
|
|
self::getRootException($value)->getLine());
|
|
|
|
|
|
|
|
$metadata['default_message'] = $default_message;
|
|
|
|
error_log($default_message);
|
|
|
|
self::outputStacktrace($metadata['trace']);
|
|
|
|
break;
|
|
|
|
case self::PHLOG:
|
|
|
|
$default_message = sprintf(
|
|
|
|
'[%s] PHLOG: %s at [%s:%d]',
|
|
|
|
$timestamp,
|
|
|
|
PhutilReadableSerializer::printShort($value),
|
|
|
|
$metadata['file'],
|
|
|
|
$metadata['line']);
|
|
|
|
|
|
|
|
$metadata['default_message'] = $default_message;
|
|
|
|
error_log($default_message);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
error_log(pht('Unknown event %s', $event));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2023-11-24 18:58:36 +02:00
|
|
|
if (self::$errorListeners) {
|
2020-02-12 14:24:11 -08:00
|
|
|
static $handling_error;
|
|
|
|
if ($handling_error) {
|
|
|
|
error_log(
|
|
|
|
'Error handler was reentered, some errors were not passed to the '.
|
|
|
|
'listener.');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$handling_error = true;
|
2023-11-24 18:58:36 +02:00
|
|
|
foreach (self::$errorListeners as $error_listener) {
|
|
|
|
call_user_func($error_listener, $event, $value, $metadata);
|
|
|
|
}
|
2020-02-12 14:24:11 -08:00
|
|
|
$handling_error = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function adjustFilePath($path) {
|
|
|
|
// Compute known library locations so we can emit relative paths if the
|
|
|
|
// file resides inside a known library. This is a little cleaner to read,
|
|
|
|
// and limits the number of false positives we get about full path
|
|
|
|
// disclosure via HackerOne.
|
|
|
|
|
|
|
|
$bootloader = PhutilBootloader::getInstance();
|
|
|
|
$libraries = $bootloader->getAllLibraries();
|
|
|
|
$roots = array();
|
|
|
|
foreach ($libraries as $library) {
|
|
|
|
$root = $bootloader->getLibraryRoot($library);
|
|
|
|
// For these libraries, the effective root is one level up.
|
|
|
|
switch ($library) {
|
|
|
|
case 'arcanist':
|
2022-08-25 01:20:55 -07:00
|
|
|
case 'phorge':
|
2020-02-12 14:24:11 -08:00
|
|
|
case 'phabricator':
|
|
|
|
$root = dirname($root);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!strncmp($root, $path, strlen($root))) {
|
|
|
|
return '<'.$library.'>'.substr($path, strlen($root));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $path;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function getLibraryVersions() {
|
|
|
|
$libinfo = array();
|
|
|
|
|
|
|
|
$bootloader = PhutilBootloader::getInstance();
|
|
|
|
foreach ($bootloader->getAllLibraries() as $library) {
|
|
|
|
$root = phutil_get_library_root($library);
|
|
|
|
$try_paths = array(
|
|
|
|
$root,
|
|
|
|
dirname($root),
|
|
|
|
);
|
|
|
|
$libinfo[$library] = array();
|
|
|
|
|
|
|
|
$get_refs = array('master');
|
|
|
|
foreach ($try_paths as $try_path) {
|
|
|
|
// Try to read what the HEAD of the repository is pointed at. This is
|
|
|
|
// normally the name of a branch ("ref").
|
|
|
|
$try_file = $try_path.'/.git/HEAD';
|
|
|
|
if (@file_exists($try_file)) {
|
|
|
|
$head = @file_get_contents($try_file);
|
|
|
|
$matches = null;
|
|
|
|
if (preg_match('(^ref: refs/heads/(.*)$)', trim($head), $matches)) {
|
|
|
|
$libinfo[$library]['head'] = trim($matches[1]);
|
|
|
|
$get_refs[] = trim($matches[1]);
|
|
|
|
} else {
|
|
|
|
$libinfo[$library]['head'] = trim($head);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try to read which commit relevant branch heads are at.
|
|
|
|
foreach (array_unique($get_refs) as $ref) {
|
|
|
|
foreach ($try_paths as $try_path) {
|
|
|
|
$try_file = $try_path.'/.git/refs/heads/'.$ref;
|
|
|
|
if (@file_exists($try_file)) {
|
|
|
|
$hash = @file_get_contents($try_file);
|
|
|
|
if ($hash) {
|
|
|
|
$libinfo[$library]['ref.'.$ref] = substr(trim($hash), 0, 12);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Look for extension files.
|
|
|
|
$custom = @scandir($root.'/extensions/');
|
|
|
|
if ($custom) {
|
|
|
|
$count = 0;
|
|
|
|
foreach ($custom as $custom_path) {
|
|
|
|
if (preg_match('/\.php$/', $custom_path)) {
|
|
|
|
$count++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($count) {
|
|
|
|
$libinfo[$library]['custom'] = $count;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ksort($libinfo);
|
|
|
|
|
|
|
|
return $libinfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a full trace across all proxied and aggregated exceptions.
|
|
|
|
*
|
|
|
|
* This attempts to build a set of stack frames which completely represent
|
|
|
|
* all of the places an exception came from, even if it came from multiple
|
|
|
|
* origins and has been aggregated or proxied.
|
|
|
|
*
|
2024-08-23 12:55:36 +02:00
|
|
|
* @param Exception|Throwable $ex Exception to retrieve a trace for.
|
2020-02-12 14:24:11 -08:00
|
|
|
* @return list<wild> List of stack frames.
|
|
|
|
*/
|
|
|
|
public static function getExceptionTrace($ex) {
|
|
|
|
$id = 1;
|
|
|
|
|
|
|
|
// Keep track of discovered exceptions which we need to build traces for.
|
|
|
|
$stack = array(
|
|
|
|
array($id, $ex),
|
|
|
|
);
|
|
|
|
|
|
|
|
$frames = array();
|
|
|
|
while ($info = array_shift($stack)) {
|
|
|
|
list($xid, $ex) = $info;
|
|
|
|
|
|
|
|
// We're going from top-level exception down in bredth-first order, but
|
|
|
|
// want to build a trace in approximately standard order (deepest part of
|
|
|
|
// the call stack to most shallow) so we need to reverse each list of
|
|
|
|
// frames and then reverse everything at the end.
|
|
|
|
|
|
|
|
$ex_frames = array_reverse($ex->getTrace());
|
|
|
|
$ex_frames = array_values($ex_frames);
|
|
|
|
$last_key = (count($ex_frames) - 1);
|
|
|
|
foreach ($ex_frames as $frame_key => $frame) {
|
|
|
|
$frame['xid'] = $xid;
|
|
|
|
|
|
|
|
// If this is a child/previous exception and we're on the deepest frame
|
|
|
|
// and missing file/line data, fill it in from the exception itself.
|
|
|
|
if ($xid > 1 && ($frame_key == $last_key)) {
|
|
|
|
if (empty($frame['file'])) {
|
|
|
|
$frame['file'] = $ex->getFile();
|
|
|
|
$frame['line'] = $ex->getLine();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Since the exceptions are likely to share the most shallow frames,
|
|
|
|
// try to add those to the trace only once.
|
|
|
|
if (isset($frame['file']) && isset($frame['line'])) {
|
|
|
|
$signature = $frame['file'].':'.$frame['line'];
|
|
|
|
if (empty($frames[$signature])) {
|
|
|
|
$frames[$signature] = $frame;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$frames[] = $frame;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If this is a proxy exception, add the proxied exception.
|
|
|
|
$prev = self::getPreviousException($ex);
|
|
|
|
if ($prev) {
|
|
|
|
$stack[] = array(++$id, $prev);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If this is an aggregate exception, add the child exceptions.
|
|
|
|
if ($ex instanceof PhutilAggregateException) {
|
|
|
|
foreach ($ex->getExceptions() as $child) {
|
|
|
|
$stack[] = array(++$id, $child);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return array_values(array_reverse($frames));
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|