diff --git a/src/error/PhutilErrorHandler.php b/src/error/PhutilErrorHandler.php index d67364c..96495e3 100644 --- a/src/error/PhutilErrorHandler.php +++ b/src/error/PhutilErrorHandler.php @@ -1,328 +1,334 @@ $file, 'line' => $line, 'context' => $ctx, 'error_code' => $num, 'trace' => $trace, )); } /** * 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 Uncaught exception object. * @return void * @task internal */ public static function handleException(Exception $ex) { self::dispatchErrorMessage( self::EXCEPTION, $ex, array( 'file' => $ex->getFile(), 'line' => $ex->getLine(), 'trace' => $ex->getTrace(), '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) { foreach ($trace as $key => $entry) { $line = ' #'.$key.' '; if (isset($entry['class'])) { $line .= $entry['class'].'::'; } $line .= idx($entry, 'function', ''); if (isset($entry['args'])) { $args = array(); foreach ($entry['args'] as $arg) { $args[] = PhutilReadableSerializer::printShort($arg); } $line .= '('.implode(', ', $args).')'; } if (isset($entry['file'])) { $line .= ' called at ['.$entry['file'].':'.$entry['line'].']'; } error_log($line); } } /** * 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("%F %T"); switch ($event) { case PhutilErrorHandler::ERROR: if (error_reporting() === 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. break; } error_log(sprintf( "[%s] ERROR %d: %s at [%s:%d]", $timestamp, $metadata['error_code'], $value, $metadata['file'], $metadata['line'])); self::outputStacktrace($metadata['trace']); break; case PhutilErrorHandler::EXCEPTION: error_log(sprintf( "[%s] EXCEPTION: %s at [%s:%d]", $timestamp, '('.get_class($value).') '.$value->getMessage(), $value->getFile(), $value->getLine())); break; case PhutilErrorHandler::PHLOG: error_log(sprintf( "[%s] PHLOG: %s at [%s:%d]", $timestamp, PhutilReadableSerializer::printShort($value), $metadata['file'], $metadata['line'])); break; case PhutilErrorHandler::DEPRECATED: error_log(sprintf( "[%s] DEPRECATED: %s is deprecated; %s", $timestamp, $value, $metadata['why'])); break; default: error_log('Unknown event '.$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; } } } /** * libphutil log function for development debugging. Takes any argument and * forwards it to registered listeners. This is essentially a more powerful * version of ##error_log()##. * * NOTE: You must call ##PhutilErrorHandler::initialize()## before this will do * anything. * * @param wild Any value you want printed to the error log or other registered * logs/consoles. * @return void * @group error */ function phlog($value) { if (!PhutilErrorHandler::hasInitialized()) { throw new Exception( "Call to phlog() before PhutilErrorHandler::initialize()!"); } // Get the caller information $trace = debug_backtrace(); $file = $trace[0]['file']; $line = $trace[0]['line']; PhutilErrorHandler::dispatchErrorMessage( $value instanceof Exception ? PhutilErrorHandler::EXCEPTION : PhutilErrorHandler::PHLOG, $value, array('file' => $file, 'line' => $line, 'trace' => $trace)); } /** * Example @{class:PhutilErrorHandler} error listener callback. When you call * ##PhutilErrorHandler::setErrorListener()##, you must pass a callback function * with the same signature as this one. * * NOTE: @{class:PhutilErrorHandler} handles writing messages to the error * log, so you only need to provide a listener if you have some other console * (like Phabricator's DarkConsole) which you //also// want to send errors to. * * NOTE: You will receive errors which were silenced with the "@" operator. If * you don't want to display these, test for "@" being in effect by checking if * ##error_reporting() === 0## before displaying the error. * * @param const A PhutilErrorHandler constant, like PhutilErrorHandler::ERROR, * which indicates the event type (e.g. error, exception, * user message). * @param wild The event value, like the Exception object for an exception * event, an error string for an error event, or some user object * for user messages. * @param dict A dictionary of metadata about the event. The keys 'file', * 'line' and 'trace' are always available. Other keys may be * present, depending on the event type. * @return void * @group error */ function phutil_error_listener_example($event, $value, array $metadata) { throw new Exception("This is just an example function!"); }