diff --git a/src/error/PhutilErrorHandler.php b/src/error/PhutilErrorHandler.php index bb02e19..90f541e 100644 --- a/src/error/PhutilErrorHandler.php +++ b/src/error/PhutilErrorHandler.php @@ -1,373 +1,370 @@ getPrevious(); } if (method_exists($ex, 'getPreviousException')) { return $ex->getPreviousException(); } return null; } /** * Find the most deeply nested exception from a possibly-nested exception. * * @param Exception A possibly-nested exception. * @return Exception Deepest exception in the nest. * @task exutil */ public static function getRootException(Exception $ex) { $root = $ex; while (self::getPreviousException($root)) { $root = self::getPreviousException($root); } return $root; } /* -( 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) { 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; } // 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); } $trace = debug_backtrace(); array_shift($trace); self::dispatchErrorMessage( self::ERROR, $str, array( 'file' => $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' => self::getRootException($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) { $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(); 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'].']'; } $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("%F %T"); + $timestamp = strftime('%Y-%m-%d %H:%M:%S'); switch ($event) { case PhutilErrorHandler::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 PhutilErrorHandler::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::getRootException($value)->getFile(), self::getRootException($value)->getLine()); $metadata['default_message'] = $default_message; error_log($default_message); self::outputStacktrace(self::getRootException($value)->getTrace()); break; case PhutilErrorHandler::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 PhutilErrorHandler::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('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; } } } diff --git a/src/error/phlog.php b/src/error/phlog.php index d744db5..6cf4068 100644 --- a/src/error/phlog.php +++ b/src/error/phlog.php @@ -1,68 +1,60 @@ $file, 'line' => $line, 'trace' => $trace)); return $value; } /** * 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!"); }