PageRenderTime 50ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/src/error/PhutilErrorHandler.php

http://github.com/facebook/libphutil
PHP | 596 lines | 323 code | 79 blank | 194 comment | 45 complexity | c51fcbdbd733f14ee3b8191f2da75a0e MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. /**
  3. * Improve PHP error logs and optionally route errors, exceptions and debugging
  4. * information to a central listener.
  5. *
  6. * This class takes over the PHP error and exception handlers when you call
  7. * ##PhutilErrorHandler::initialize()## and forwards all debugging information
  8. * to a listener you install with ##PhutilErrorHandler::setErrorListener()##.
  9. *
  10. * To use PhutilErrorHandler, which will enhance the messages printed to the
  11. * PHP error log, just initialize it:
  12. *
  13. * PhutilErrorHandler::initialize();
  14. *
  15. * To additionally install a custom listener which can print error information
  16. * to some other file or console, register a listener:
  17. *
  18. * PhutilErrorHandler::setErrorListener($some_callback);
  19. *
  20. * For information on writing an error listener, see
  21. * @{function:phutil_error_listener_example}. Providing a listener is optional,
  22. * you will benefit from improved error logs even without one.
  23. *
  24. * Phabricator uses this class to drive the DarkConsole "Error Log" plugin.
  25. *
  26. * @task config Configuring Error Dispatch
  27. * @task exutil Exception Utilities
  28. * @task trap Error Traps
  29. * @task internal Internals
  30. */
  31. final class PhutilErrorHandler extends Phobject {
  32. private static $errorListener = null;
  33. private static $initialized = false;
  34. private static $traps = array();
  35. const EXCEPTION = 'exception';
  36. const ERROR = 'error';
  37. const PHLOG = 'phlog';
  38. const DEPRECATED = 'deprecated';
  39. /* -( Configuring Error Dispatch )----------------------------------------- */
  40. /**
  41. * Registers this class as the PHP error and exception handler. This will
  42. * overwrite any previous handlers!
  43. *
  44. * @return void
  45. * @task config
  46. */
  47. public static function initialize() {
  48. self::$initialized = true;
  49. set_error_handler(array(__CLASS__, 'handleError'));
  50. set_exception_handler(array(__CLASS__, 'handleException'));
  51. }
  52. /**
  53. * Provide an optional listener callback which will receive all errors,
  54. * exceptions and debugging messages. It can then print them to a web console,
  55. * for example.
  56. *
  57. * See @{function:phutil_error_listener_example} for details about the
  58. * callback parameters and operation.
  59. *
  60. * @return void
  61. * @task config
  62. */
  63. public static function setErrorListener($listener) {
  64. self::$errorListener = $listener;
  65. }
  66. /* -( Exception Utilities )------------------------------------------------ */
  67. /**
  68. * Gets the previous exception of a nested exception. Prior to PHP 5.3 you
  69. * can use @{class:PhutilProxyException} to nest exceptions; after PHP 5.3
  70. * all exceptions are nestable.
  71. *
  72. * @param Exception|Throwable Exception to unnest.
  73. * @return Exception|Throwable|null Previous exception, if one exists.
  74. * @task exutil
  75. */
  76. public static function getPreviousException($ex) {
  77. if (method_exists($ex, 'getPrevious')) {
  78. return $ex->getPrevious();
  79. }
  80. if (method_exists($ex, 'getPreviousException')) {
  81. return $ex->getPreviousException();
  82. }
  83. return null;
  84. }
  85. /**
  86. * Find the most deeply nested exception from a possibly-nested exception.
  87. *
  88. * @param Exception|Throwable A possibly-nested exception.
  89. * @return Exception|Throwable Deepest exception in the nest.
  90. * @task exutil
  91. */
  92. public static function getRootException($ex) {
  93. $root = $ex;
  94. while (self::getPreviousException($root)) {
  95. $root = self::getPreviousException($root);
  96. }
  97. return $root;
  98. }
  99. /* -( Trapping Errors )---------------------------------------------------- */
  100. /**
  101. * Adds an error trap. Normally you should not invoke this directly;
  102. * @{class:PhutilErrorTrap} registers itself on construction.
  103. *
  104. * @param PhutilErrorTrap Trap to add.
  105. * @return void
  106. * @task trap
  107. */
  108. public static function addErrorTrap(PhutilErrorTrap $trap) {
  109. $key = $trap->getTrapKey();
  110. self::$traps[$key] = $trap;
  111. }
  112. /**
  113. * Removes an error trap. Normally you should not invoke this directly;
  114. * @{class:PhutilErrorTrap} deregisters itself on destruction.
  115. *
  116. * @param PhutilErrorTrap Trap to remove.
  117. * @return void
  118. * @task trap
  119. */
  120. public static function removeErrorTrap(PhutilErrorTrap $trap) {
  121. $key = $trap->getTrapKey();
  122. unset(self::$traps[$key]);
  123. }
  124. /* -( Internals )---------------------------------------------------------- */
  125. /**
  126. * Determine if PhutilErrorHandler has been initialized.
  127. *
  128. * @return bool True if initialized.
  129. * @task internal
  130. */
  131. public static function hasInitialized() {
  132. return self::$initialized;
  133. }
  134. /**
  135. * Handles PHP errors and dispatches them forward. This is a callback for
  136. * ##set_error_handler()##. You should not call this function directly; use
  137. * @{function:phlog} to print debugging messages or ##trigger_error()## to
  138. * trigger PHP errors.
  139. *
  140. * This handler converts E_RECOVERABLE_ERROR messages from violated typehints
  141. * into @{class:InvalidArgumentException}s.
  142. *
  143. * This handler converts other E_RECOVERABLE_ERRORs into
  144. * @{class:RuntimeException}s.
  145. *
  146. * This handler converts E_NOTICE messages from uses of undefined variables
  147. * into @{class:RuntimeException}s.
  148. *
  149. * @param int Error code.
  150. * @param string Error message.
  151. * @param string File where the error occurred.
  152. * @param int Line on which the error occurred.
  153. * @param wild Error context information.
  154. * @return void
  155. * @task internal
  156. */
  157. public static function handleError($num, $str, $file, $line, $ctx) {
  158. foreach (self::$traps as $trap) {
  159. $trap->addError($num, $str, $file, $line, $ctx);
  160. }
  161. if ((error_reporting() & $num) == 0) {
  162. // Respect the use of "@" to silence warnings: if this error was
  163. // emitted from a context where "@" was in effect, the
  164. // value returned by error_reporting() will be 0. This is the
  165. // recommended way to check for this, see set_error_handler() docs
  166. // on php.net.
  167. return false;
  168. }
  169. // Convert typehint failures into exceptions.
  170. if (preg_match('/^Argument (\d+) passed to (\S+) must be/', $str)) {
  171. throw new InvalidArgumentException($str);
  172. }
  173. // Convert other E_RECOVERABLE_ERRORs into generic runtime exceptions.
  174. if ($num == E_RECOVERABLE_ERROR) {
  175. throw new RuntimeException($str);
  176. }
  177. // Convert uses of undefined variables into exceptions.
  178. if (preg_match('/^Undefined variable: /', $str)) {
  179. throw new RuntimeException($str);
  180. }
  181. // Convert uses of undefined properties into exceptions.
  182. if (preg_match('/^Undefined property: /', $str)) {
  183. throw new RuntimeException($str);
  184. }
  185. // Convert undefined constants into exceptions. Usually this means there
  186. // is a missing `$` and the program is horribly broken.
  187. if (preg_match('/^Use of undefined constant /', $str)) {
  188. throw new RuntimeException($str);
  189. }
  190. $trace = debug_backtrace();
  191. array_shift($trace);
  192. self::dispatchErrorMessage(
  193. self::ERROR,
  194. $str,
  195. array(
  196. 'file' => $file,
  197. 'line' => $line,
  198. 'context' => $ctx,
  199. 'error_code' => $num,
  200. 'trace' => $trace,
  201. ));
  202. }
  203. /**
  204. * Handles PHP exceptions and dispatches them forward. This is a callback for
  205. * ##set_exception_handler()##. You should not call this function directly;
  206. * to print exceptions, pass the exception object to @{function:phlog}.
  207. *
  208. * @param Exception|Throwable Uncaught exception object.
  209. * @return void
  210. * @task internal
  211. */
  212. public static function handleException($ex) {
  213. self::dispatchErrorMessage(
  214. self::EXCEPTION,
  215. $ex,
  216. array(
  217. 'file' => $ex->getFile(),
  218. 'line' => $ex->getLine(),
  219. 'trace' => self::getExceptionTrace($ex),
  220. 'catch_trace' => debug_backtrace(),
  221. ));
  222. // Normally, PHP exits with code 255 after an uncaught exception is thrown.
  223. // However, if we install an exception handler (as we have here), it exits
  224. // with code 0 instead. Script execution terminates after this function
  225. // exits in either case, so exit explicitly with the correct exit code.
  226. exit(255);
  227. }
  228. /**
  229. * Output a stacktrace to the PHP error log.
  230. *
  231. * @param trace A stacktrace, e.g. from debug_backtrace();
  232. * @return void
  233. * @task internal
  234. */
  235. public static function outputStacktrace($trace) {
  236. $lines = explode("\n", self::formatStacktrace($trace));
  237. foreach ($lines as $line) {
  238. error_log($line);
  239. }
  240. }
  241. /**
  242. * Format a stacktrace for output.
  243. *
  244. * @param trace A stacktrace, e.g. from debug_backtrace();
  245. * @return string Human-readable trace.
  246. * @task internal
  247. */
  248. public static function formatStacktrace($trace) {
  249. $result = array();
  250. $libinfo = self::getLibraryVersions();
  251. if ($libinfo) {
  252. foreach ($libinfo as $key => $dict) {
  253. $info = array();
  254. foreach ($dict as $dkey => $dval) {
  255. $info[] = $dkey.'='.$dval;
  256. }
  257. $libinfo[$key] = $key.'('.implode(', ', $info).')';
  258. }
  259. $result[] = implode(', ', $libinfo);
  260. }
  261. foreach ($trace as $key => $entry) {
  262. $line = ' #'.$key.' ';
  263. if (!empty($entry['xid'])) {
  264. if ($entry['xid'] != 1) {
  265. $line .= '<#'.$entry['xid'].'> ';
  266. }
  267. }
  268. if (isset($entry['class'])) {
  269. $line .= $entry['class'].'::';
  270. }
  271. $line .= idx($entry, 'function', '');
  272. if (isset($entry['args'])) {
  273. $args = array();
  274. foreach ($entry['args'] as $arg) {
  275. // NOTE: Print out object types, not values. Values sometimes contain
  276. // sensitive information and are usually not particularly helpful
  277. // for debugging.
  278. $type = (gettype($arg) == 'object')
  279. ? get_class($arg)
  280. : gettype($arg);
  281. $args[] = $type;
  282. }
  283. $line .= '('.implode(', ', $args).')';
  284. }
  285. if (isset($entry['file'])) {
  286. $file = self::adjustFilePath($entry['file']);
  287. $line .= ' called at ['.$file.':'.$entry['line'].']';
  288. }
  289. $result[] = $line;
  290. }
  291. return implode("\n", $result);
  292. }
  293. /**
  294. * All different types of error messages come here before they are
  295. * dispatched to the listener; this method also prints them to the PHP error
  296. * log.
  297. *
  298. * @param const Event type constant.
  299. * @param wild Event value.
  300. * @param dict Event metadata.
  301. * @return void
  302. * @task internal
  303. */
  304. public static function dispatchErrorMessage($event, $value, $metadata) {
  305. $timestamp = strftime('%Y-%m-%d %H:%M:%S');
  306. switch ($event) {
  307. case self::ERROR:
  308. $default_message = sprintf(
  309. '[%s] ERROR %d: %s at [%s:%d]',
  310. $timestamp,
  311. $metadata['error_code'],
  312. $value,
  313. $metadata['file'],
  314. $metadata['line']);
  315. $metadata['default_message'] = $default_message;
  316. error_log($default_message);
  317. self::outputStacktrace($metadata['trace']);
  318. break;
  319. case self::EXCEPTION:
  320. $messages = array();
  321. $current = $value;
  322. do {
  323. $messages[] = '('.get_class($current).') '.$current->getMessage();
  324. } while ($current = self::getPreviousException($current));
  325. $messages = implode(' {>} ', $messages);
  326. if (strlen($messages) > 4096) {
  327. $messages = substr($messages, 0, 4096).'...';
  328. }
  329. $default_message = sprintf(
  330. '[%s] EXCEPTION: %s at [%s:%d]',
  331. $timestamp,
  332. $messages,
  333. self::adjustFilePath(self::getRootException($value)->getFile()),
  334. self::getRootException($value)->getLine());
  335. $metadata['default_message'] = $default_message;
  336. error_log($default_message);
  337. self::outputStacktrace($metadata['trace']);
  338. break;
  339. case self::PHLOG:
  340. $default_message = sprintf(
  341. '[%s] PHLOG: %s at [%s:%d]',
  342. $timestamp,
  343. PhutilReadableSerializer::printShort($value),
  344. $metadata['file'],
  345. $metadata['line']);
  346. $metadata['default_message'] = $default_message;
  347. error_log($default_message);
  348. break;
  349. case self::DEPRECATED:
  350. $default_message = sprintf(
  351. '[%s] DEPRECATED: %s is deprecated; %s',
  352. $timestamp,
  353. $value,
  354. $metadata['why']);
  355. $metadata['default_message'] = $default_message;
  356. error_log($default_message);
  357. break;
  358. default:
  359. error_log(pht('Unknown event %s', $event));
  360. break;
  361. }
  362. if (self::$errorListener) {
  363. static $handling_error;
  364. if ($handling_error) {
  365. error_log(
  366. 'Error handler was reentered, some errors were not passed to the '.
  367. 'listener.');
  368. return;
  369. }
  370. $handling_error = true;
  371. call_user_func(self::$errorListener, $event, $value, $metadata);
  372. $handling_error = false;
  373. }
  374. }
  375. public static function adjustFilePath($path) {
  376. // Compute known library locations so we can emit relative paths if the
  377. // file resides inside a known library. This is a little cleaner to read,
  378. // and limits the number of false positives we get about full path
  379. // disclosure via HackerOne.
  380. $bootloader = PhutilBootloader::getInstance();
  381. $libraries = $bootloader->getAllLibraries();
  382. $roots = array();
  383. foreach ($libraries as $library) {
  384. $root = $bootloader->getLibraryRoot($library);
  385. // For these libraries, the effective root is one level up.
  386. switch ($library) {
  387. case 'phutil':
  388. case 'arcanist':
  389. case 'phabricator':
  390. $root = dirname($root);
  391. break;
  392. }
  393. if (!strncmp($root, $path, strlen($root))) {
  394. return '<'.$library.'>'.substr($path, strlen($root));
  395. }
  396. }
  397. return $path;
  398. }
  399. public static function getLibraryVersions() {
  400. $libinfo = array();
  401. $bootloader = PhutilBootloader::getInstance();
  402. foreach ($bootloader->getAllLibraries() as $library) {
  403. $root = phutil_get_library_root($library);
  404. $try_paths = array(
  405. $root,
  406. dirname($root),
  407. );
  408. $libinfo[$library] = array();
  409. $get_refs = array('master');
  410. foreach ($try_paths as $try_path) {
  411. // Try to read what the HEAD of the repository is pointed at. This is
  412. // normally the name of a branch ("ref").
  413. $try_file = $try_path.'/.git/HEAD';
  414. if (@file_exists($try_file)) {
  415. $head = @file_get_contents($try_file);
  416. $matches = null;
  417. if (preg_match('(^ref: refs/heads/(.*)$)', trim($head), $matches)) {
  418. $libinfo[$library]['head'] = trim($matches[1]);
  419. $get_refs[] = trim($matches[1]);
  420. } else {
  421. $libinfo[$library]['head'] = trim($head);
  422. }
  423. break;
  424. }
  425. }
  426. // Try to read which commit relevant branch heads are at.
  427. foreach (array_unique($get_refs) as $ref) {
  428. foreach ($try_paths as $try_path) {
  429. $try_file = $try_path.'/.git/refs/heads/'.$ref;
  430. if (@file_exists($try_file)) {
  431. $hash = @file_get_contents($try_file);
  432. if ($hash) {
  433. $libinfo[$library]['ref.'.$ref] = substr(trim($hash), 0, 12);
  434. break;
  435. }
  436. }
  437. }
  438. }
  439. // Look for extension files.
  440. $custom = @scandir($root.'/extensions/');
  441. if ($custom) {
  442. $count = 0;
  443. foreach ($custom as $custom_path) {
  444. if (preg_match('/\.php$/', $custom_path)) {
  445. $count++;
  446. }
  447. }
  448. if ($count) {
  449. $libinfo[$library]['custom'] = $count;
  450. }
  451. }
  452. }
  453. ksort($libinfo);
  454. return $libinfo;
  455. }
  456. /**
  457. * Get a full trace across all proxied and aggregated exceptions.
  458. *
  459. * This attempts to build a set of stack frames which completely represent
  460. * all of the places an exception came from, even if it came from multiple
  461. * origins and has been aggregated or proxied.
  462. *
  463. * @param Exception|Throwable Exception to retrieve a trace for.
  464. * @return list<wild> List of stack frames.
  465. */
  466. public static function getExceptionTrace($ex) {
  467. $id = 1;
  468. // Keep track of discovered exceptions which we need to build traces for.
  469. $stack = array(
  470. array($id, $ex),
  471. );
  472. $frames = array();
  473. while ($info = array_shift($stack)) {
  474. list($xid, $ex) = $info;
  475. // We're going from top-level exception down in bredth-first order, but
  476. // want to build a trace in approximately standard order (deepest part of
  477. // the call stack to most shallow) so we need to reverse each list of
  478. // frames and then reverse everything at the end.
  479. $ex_frames = array_reverse($ex->getTrace());
  480. $ex_frames = array_values($ex_frames);
  481. $last_key = (count($ex_frames) - 1);
  482. foreach ($ex_frames as $frame_key => $frame) {
  483. $frame['xid'] = $xid;
  484. // If this is a child/previous exception and we're on the deepest frame
  485. // and missing file/line data, fill it in from the exception itself.
  486. if ($xid > 1 && ($frame_key == $last_key)) {
  487. if (empty($frame['file'])) {
  488. $frame['file'] = $ex->getFile();
  489. $frame['line'] = $ex->getLine();
  490. }
  491. }
  492. // Since the exceptions are likely to share the most shallow frames,
  493. // try to add those to the trace only once.
  494. if (isset($frame['file']) && isset($frame['line'])) {
  495. $signature = $frame['file'].':'.$frame['line'];
  496. if (empty($frames[$signature])) {
  497. $frames[$signature] = $frame;
  498. }
  499. } else {
  500. $frames[] = $frame;
  501. }
  502. }
  503. // If this is a proxy exception, add the proxied exception.
  504. $prev = self::getPreviousException($ex);
  505. if ($prev) {
  506. $stack[] = array(++$id, $prev);
  507. }
  508. // If this is an aggregate exception, add the child exceptions.
  509. if ($ex instanceof PhutilAggregateException) {
  510. foreach ($ex->getExceptions() as $child) {
  511. $stack[] = array(++$id, $child);
  512. }
  513. }
  514. }
  515. return array_values(array_reverse($frames));
  516. }
  517. }