Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F1389936
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
22 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/error/PhutilErrorHandler.php b/src/error/PhutilErrorHandler.php
index deb6ee16..95af88b6 100644
--- a/src/error/PhutilErrorHandler.php
+++ b/src/error/PhutilErrorHandler.php
@@ -1,623 +1,622 @@
<?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
* to a listener you install with ##PhutilErrorHandler::setErrorListener()##.
*
* 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:
*
* PhutilErrorHandler::setErrorListener($some_callback);
*
* 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 {
private static $errorListener = null;
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
*/
public static function setErrorListener($listener) {
self::$errorListener = $listener;
}
/* -( 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.
*
* @param Exception|Throwable Exception to unnest.
* @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.
*
* @param Exception|Throwable A possibly-nested exception.
* @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.
*
* @param PhutilErrorTrap Trap to add.
* @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.
*
* @param PhutilErrorTrap Trap to remove.
* @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.
*
* @param int Error code.
* @param string Error message.
* @param string File where the error occurred.
* @param int Line on which the error occurred.
* @param wild Error context information.
* @return void
* @task internal
*/
- public static function handleError($num, $str, $file, $line, $ctx) {
+ public static function handleError($num, $str, $file, $line, $ctx = null) {
foreach (self::$traps as $trap) {
- $trap->addError($num, $str, $file, $line, $ctx);
+ $trap->addError($num, $str, $file, $line);
}
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;
}
// 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) ||
($num === E_USER_NOTICE)) {
$trace = debug_backtrace();
array_shift($trace);
self::dispatchErrorMessage(
self::ERROR,
$str,
array(
'file' => $file,
'line' => $line,
- 'context' => $ctx,
'error_code' => $num,
'trace' => $trace,
));
return;
}
// 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);
}
// 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);
}
/**
* 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}.
*
* @param Exception|Throwable Uncaught exception object.
* @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.
*
* @param trace A stacktrace, e.g. from debug_backtrace();
* @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.
*
* @param trace A stacktrace, e.g. from debug_backtrace();
* @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.
*
* @param const Event type constant.
* @param wild Event value.
* @param dict Event metadata.
* @return void
* @task internal
*/
public static function dispatchErrorMessage($event, $value, $metadata) {
$timestamp = strftime('%Y-%m-%d %H:%M:%S');
switch ($event) {
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;
case self::DEPRECATED:
$default_message = sprintf(
'[%s] DEPRECATED: %s is deprecated; %s',
$timestamp,
$value,
$metadata['why']);
$metadata['default_message'] = $default_message;
error_log($default_message);
break;
default:
error_log(pht('Unknown event %s', $event));
break;
}
if (self::$errorListener) {
static $handling_error;
if ($handling_error) {
error_log(
'Error handler was reentered, some errors were not passed to the '.
'listener.');
return;
}
$handling_error = true;
call_user_func(self::$errorListener, $event, $value, $metadata);
$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':
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.
*
* @param Exception|Throwable Exception to retrieve a trace for.
* @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));
}
}
diff --git a/src/error/PhutilErrorTrap.php b/src/error/PhutilErrorTrap.php
index 94d291d0..252a37f9 100644
--- a/src/error/PhutilErrorTrap.php
+++ b/src/error/PhutilErrorTrap.php
@@ -1,83 +1,82 @@
<?php
/**
* Trap PHP errors while this object is alive, so they can be accessed and
* included in exceptions or other types of logging. For example, if you have
* code like this:
*
* $res = proc_open(...);
*
* There is no easy way to throw an informative exception if the `proc_open()`
* fails. In some cases you may be able to use `error_get_last()`, but this is
* unreliable (if `proc_open()` fails because `disable_functions` is set, it
* does not capture the error) and can not capture more than one error.
*
* You can trap errors while executing this code instead:
*
* $trap = new PhutilErrorTrap();
* $res = proc_open(...);
* $err = $trap->getErrorsAsString();
* $trap->destroy();
*
* if (!$res) {
* throw new Exception('proc_open() failed: '.$err);
* }
*
* IMPORTANT: You must explicitly destroy traps because they register
* themselves with @{class:PhutilErrorHandler}, and thus will not be destroyed
* when `unset()`.
*
* Some notes on traps:
*
* - Traps catch all errors, including those silenced by `@`.
* - Traps do not prevent errors from reaching other standard handlers. You
* can use `@` to keep errors out of the logs while still trapping them.
* - Traps capture all errors until they are explicitly destroyed. This means
* that you should not create long-lived traps, or they may consume
* unbounded amounts of memory to hold the error log.
*/
final class PhutilErrorTrap extends Phobject {
private $destroyed;
private $errors = array();
- public function addError($num, $str, $file, $line, $ctx) {
+ public function addError($num, $str, $file, $line) {
$this->errors[] = array(
'num' => $num,
'str' => $str,
'file' => $file,
'line' => $line,
- 'ctx' => $ctx,
);
return $this;
}
public function getErrorsAsString() {
$out = array();
foreach ($this->errors as $error) {
$out[] = $error['str'];
}
return implode("\n", $out);
}
public function destroy() {
if (!$this->destroyed) {
PhutilErrorHandler::removeErrorTrap($this);
$this->errors = array();
$this->destroyed = true;
}
}
public function getTrapKey() {
return spl_object_hash($this);
}
public function __construct() {
PhutilErrorHandler::addErrorTrap($this);
}
public function __toString() {
return $this->getErrorsAsString();
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Mar 27, 19:33 (1 d, 11 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
619459
Default Alt Text
(22 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment