diff --git a/scripts/__init_script__.php b/scripts/__init_script__.php
index d4559e9..53cb235 100644
--- a/scripts/__init_script__.php
+++ b/scripts/__init_script__.php
@@ -1,101 +1,91 @@
 <?php
 
+declare(ticks = 1);
+
 function __phutil_init_script__() {
   // Adjust the runtime language configuration to be reasonable and inline with
   // expectations. We do this first, then load libraries.
 
   // There may be some kind of auto-prepend script configured which starts an
   // output buffer. Discard any such output buffers so messages can be sent to
   // stdout (if a user wants to capture output from a script, there are a large
   // number of ways they can accomplish it legitimately; historically, we ran
   // into this on only one install which had some bizarre configuration, but it
   // was difficult to diagnose because the symptom is "no messages of any
   // kind").
   while (ob_get_level() > 0) {
     ob_end_clean();
   }
 
   error_reporting(E_ALL | E_STRICT);
 
   $config_map = array(
     // Always display script errors. Without this, they may not appear, which is
     // unhelpful when users encounter a problem. On the web this is a security
     // concern because you don't want to expose errors to clients, but in a
     // script context we always want to show errors.
     'display_errors'              => true,
 
     // Send script error messages to the server's `error_log` setting.
     'log_errors'                  => true,
 
     // Set the error log to the default, so errors go to stderr. Without this
     // errors may end up in some log, and users may not know where the log is
     // or check it.
     'error_log'                   => null,
 
     // XDebug raises a fatal error if the call stack gets too deep, but the
     // default setting is 100, which we may exceed legitimately with module
     // includes (and in other cases, like recursive filesystem operations
     // applied to 100+ levels of directory nesting). Stop it from triggering:
     // we explicitly limit recursive algorithms which should be limited.
     //
     // After Feb 2014, XDebug interprets a value of 0 to mean "do not allow any
     // function calls". Previously, 0 effectively disabled this check. For
     // context, see T5027.
     'xdebug.max_nesting_level'    => PHP_INT_MAX,
 
     // Don't limit memory, doing so just generally just prevents us from
     // processing large inputs without many tangible benefits.
     'memory_limit'                => -1,
   );
 
   foreach ($config_map as $config_key => $config_value) {
     ini_set($config_key, $config_value);
   }
 
   if (!ini_get('date.timezone')) {
     // If the timezone isn't set, PHP issues a warning whenever you try to parse
     // a date (like those from Git or Mercurial logs), even if the date contains
     // timezone information (like "PST" or "-0700") which makes the
     // environmental timezone setting is completely irrelevant. We never rely on
     // the system timezone setting in any capacity, so prevent PHP from flipping
     // out by setting it to a safe default (UTC) if it isn't set to some other
     // value.
     date_default_timezone_set('UTC');
   }
 
   // Adjust `include_path`.
   ini_set('include_path', implode(PATH_SEPARATOR, array(
     dirname(dirname(__FILE__)).'/externals/includes',
     ini_get('include_path'),
   )));
 
   // Disable the insanely dangerous XML entity loader by default.
   if (function_exists('libxml_disable_entity_loader')) {
     libxml_disable_entity_loader(true);
   }
 
   // Now, load libphutil.
 
   $root = dirname(dirname(__FILE__));
   require_once $root.'/src/__phutil_library_init__.php';
 
   PhutilErrorHandler::initialize();
+  $router = PhutilSignalRouter::initialize();
 
-  // If possible, install a signal handler for SIGHUP which prints the current
-  // backtrace out to a named file. This is particularly helpful in debugging
-  // hung/spinning processes.
-  if (function_exists('pcntl_signal')) {
-    pcntl_signal(SIGHUP, '__phutil_signal_handler__');
-  }
-}
-
-function __phutil_signal_handler__($signal_number) {
-  $e = new Exception();
-  $pid = getmypid();
-  // Some Phabricator daemons may not be attached to a terminal.
-  Filesystem::writeFile(
-    sys_get_temp_dir().'/phabricator_backtrace_'.$pid,
-    $e->getTraceAsString());
+  $handler = new PhutilBacktraceSignalHandler();
+  $router->installHandler('phutil.backtrace', $handler);
 }
 
 __phutil_init_script__();
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 0594027..7e62eab 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,1000 +1,1008 @@
 <?php
 
 /**
  * This file is automatically generated. Use 'arc liberate' to rebuild it.
  *
  * @generated
  * @phutil-library-version 2
  */
 phutil_register_library_map(array(
   '__library_version__' => 2,
   'class' => array(
     'AASTNode' => 'parser/aast/api/AASTNode.php',
     'AASTNodeList' => 'parser/aast/api/AASTNodeList.php',
     'AASTToken' => 'parser/aast/api/AASTToken.php',
     'AASTTree' => 'parser/aast/api/AASTTree.php',
     'AbstractDirectedGraph' => 'utils/AbstractDirectedGraph.php',
     'AbstractDirectedGraphTestCase' => 'utils/__tests__/AbstractDirectedGraphTestCase.php',
     'AphrontAccessDeniedQueryException' => 'aphront/storage/exception/AphrontAccessDeniedQueryException.php',
     'AphrontBaseMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php',
     'AphrontCharacterSetQueryException' => 'aphront/storage/exception/AphrontCharacterSetQueryException.php',
     'AphrontConnectionLostQueryException' => 'aphront/storage/exception/AphrontConnectionLostQueryException.php',
     'AphrontConnectionQueryException' => 'aphront/storage/exception/AphrontConnectionQueryException.php',
     'AphrontCountQueryException' => 'aphront/storage/exception/AphrontCountQueryException.php',
     'AphrontDatabaseConnection' => 'aphront/storage/connection/AphrontDatabaseConnection.php',
     'AphrontDatabaseTransactionState' => 'aphront/storage/connection/AphrontDatabaseTransactionState.php',
     'AphrontDeadlockQueryException' => 'aphront/storage/exception/AphrontDeadlockQueryException.php',
     'AphrontDuplicateKeyQueryException' => 'aphront/storage/exception/AphrontDuplicateKeyQueryException.php',
     'AphrontInvalidCredentialsQueryException' => 'aphront/storage/exception/AphrontInvalidCredentialsQueryException.php',
     'AphrontIsolatedDatabaseConnection' => 'aphront/storage/connection/AphrontIsolatedDatabaseConnection.php',
     'AphrontLockTimeoutQueryException' => 'aphront/storage/exception/AphrontLockTimeoutQueryException.php',
     'AphrontMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php',
     'AphrontMySQLiDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php',
     'AphrontNotSupportedQueryException' => 'aphront/storage/exception/AphrontNotSupportedQueryException.php',
     'AphrontObjectMissingQueryException' => 'aphront/storage/exception/AphrontObjectMissingQueryException.php',
     'AphrontParameterQueryException' => 'aphront/storage/exception/AphrontParameterQueryException.php',
     'AphrontQueryException' => 'aphront/storage/exception/AphrontQueryException.php',
     'AphrontQueryTimeoutQueryException' => 'aphront/storage/exception/AphrontQueryTimeoutQueryException.php',
     'AphrontRecoverableQueryException' => 'aphront/storage/exception/AphrontRecoverableQueryException.php',
     'AphrontRequestStream' => 'aphront/requeststream/AphrontRequestStream.php',
     'AphrontSchemaQueryException' => 'aphront/storage/exception/AphrontSchemaQueryException.php',
     'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/AphrontScopedUnguardedWriteCapability.php',
     'AphrontWriteGuard' => 'aphront/writeguard/AphrontWriteGuard.php',
     'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php',
     'CaseInsensitiveArray' => 'utils/CaseInsensitiveArray.php',
     'CaseInsensitiveArrayTestCase' => 'utils/__tests__/CaseInsensitiveArrayTestCase.php',
     'CommandException' => 'future/exec/CommandException.php',
     'ConduitClient' => 'conduit/ConduitClient.php',
     'ConduitClientException' => 'conduit/ConduitClientException.php',
     'ConduitClientTestCase' => 'conduit/__tests__/ConduitClientTestCase.php',
     'ConduitFuture' => 'conduit/ConduitFuture.php',
     'ExecFuture' => 'future/exec/ExecFuture.php',
     'ExecFutureTestCase' => 'future/exec/__tests__/ExecFutureTestCase.php',
     'ExecPassthruTestCase' => 'future/exec/__tests__/ExecPassthruTestCase.php',
     'FileFinder' => 'filesystem/FileFinder.php',
     'FileFinderTestCase' => 'filesystem/__tests__/FileFinderTestCase.php',
     'FileList' => 'filesystem/FileList.php',
     'Filesystem' => 'filesystem/Filesystem.php',
     'FilesystemException' => 'filesystem/FilesystemException.php',
     'FilesystemTestCase' => 'filesystem/__tests__/FilesystemTestCase.php',
     'Future' => 'future/Future.php',
     'FutureIterator' => 'future/FutureIterator.php',
     'FutureIteratorTestCase' => 'future/__tests__/FutureIteratorTestCase.php',
     'FutureProxy' => 'future/FutureProxy.php',
     'HTTPFuture' => 'future/http/HTTPFuture.php',
     'HTTPFutureCURLResponseStatus' => 'future/http/status/HTTPFutureCURLResponseStatus.php',
     'HTTPFutureCertificateResponseStatus' => 'future/http/status/HTTPFutureCertificateResponseStatus.php',
     'HTTPFutureHTTPResponseStatus' => 'future/http/status/HTTPFutureHTTPResponseStatus.php',
     'HTTPFutureParseResponseStatus' => 'future/http/status/HTTPFutureParseResponseStatus.php',
     'HTTPFutureResponseStatus' => 'future/http/status/HTTPFutureResponseStatus.php',
     'HTTPFutureTransportResponseStatus' => 'future/http/status/HTTPFutureTransportResponseStatus.php',
     'HTTPSFuture' => 'future/http/HTTPSFuture.php',
     'ImmediateFuture' => 'future/ImmediateFuture.php',
     'LibphutilUSEnglishTranslation' => 'internationalization/translation/LibphutilUSEnglishTranslation.php',
     'LinesOfALarge' => 'filesystem/linesofalarge/LinesOfALarge.php',
     'LinesOfALargeExecFuture' => 'filesystem/linesofalarge/LinesOfALargeExecFuture.php',
     'LinesOfALargeExecFutureTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php',
     'LinesOfALargeFile' => 'filesystem/linesofalarge/LinesOfALargeFile.php',
     'LinesOfALargeFileTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php',
     'MFilterTestHelper' => 'utils/__tests__/MFilterTestHelper.php',
     'PHPASTParserTestCase' => 'parser/xhpast/__tests__/PHPASTParserTestCase.php',
     'PhageAgentBootloader' => 'phage/bootloader/PhageAgentBootloader.php',
     'PhageAgentTestCase' => 'phage/__tests__/PhageAgentTestCase.php',
     'PhagePHPAgent' => 'phage/agent/PhagePHPAgent.php',
     'PhagePHPAgentBootloader' => 'phage/bootloader/PhagePHPAgentBootloader.php',
     'Phobject' => 'object/Phobject.php',
     'PhobjectTestCase' => 'object/__tests__/PhobjectTestCase.php',
     'PhutilAPCKeyValueCache' => 'cache/PhutilAPCKeyValueCache.php',
     'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php',
     'PhutilAWSException' => 'future/aws/PhutilAWSException.php',
     'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php',
     'PhutilAWSManagementWorkflow' => 'future/aws/management/PhutilAWSManagementWorkflow.php',
     'PhutilAWSS3DeleteManagementWorkflow' => 'future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php',
     'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php',
     'PhutilAWSS3GetManagementWorkflow' => 'future/aws/management/PhutilAWSS3GetManagementWorkflow.php',
     'PhutilAWSS3ManagementWorkflow' => 'future/aws/management/PhutilAWSS3ManagementWorkflow.php',
     'PhutilAWSS3PutManagementWorkflow' => 'future/aws/management/PhutilAWSS3PutManagementWorkflow.php',
     'PhutilAWSv4Signature' => 'future/aws/PhutilAWSv4Signature.php',
     'PhutilAWSv4SignatureTestCase' => 'future/aws/__tests__/PhutilAWSv4SignatureTestCase.php',
     'PhutilAggregateException' => 'error/PhutilAggregateException.php',
     'PhutilAllCapsEnglishLocale' => 'internationalization/locales/PhutilAllCapsEnglishLocale.php',
     'PhutilAmazonAuthAdapter' => 'auth/PhutilAmazonAuthAdapter.php',
     'PhutilArgumentParser' => 'parser/argument/PhutilArgumentParser.php',
     'PhutilArgumentParserException' => 'parser/argument/exception/PhutilArgumentParserException.php',
     'PhutilArgumentParserTestCase' => 'parser/argument/__tests__/PhutilArgumentParserTestCase.php',
     'PhutilArgumentSpecification' => 'parser/argument/PhutilArgumentSpecification.php',
     'PhutilArgumentSpecificationException' => 'parser/argument/exception/PhutilArgumentSpecificationException.php',
     'PhutilArgumentSpecificationTestCase' => 'parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php',
     'PhutilArgumentSpellingCorrector' => 'parser/argument/PhutilArgumentSpellingCorrector.php',
     'PhutilArgumentSpellingCorrectorTestCase' => 'parser/argument/__tests__/PhutilArgumentSpellingCorrectorTestCase.php',
     'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php',
     'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php',
     'PhutilArray' => 'utils/PhutilArray.php',
     'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php',
     'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php',
     'PhutilAsanaAuthAdapter' => 'auth/PhutilAsanaAuthAdapter.php',
     'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php',
     'PhutilAuthAdapter' => 'auth/PhutilAuthAdapter.php',
     'PhutilAuthConfigurationException' => 'auth/exception/PhutilAuthConfigurationException.php',
     'PhutilAuthCredentialException' => 'auth/exception/PhutilAuthCredentialException.php',
     'PhutilAuthException' => 'auth/exception/PhutilAuthException.php',
     'PhutilAuthUserAbortedException' => 'auth/exception/PhutilAuthUserAbortedException.php',
+    'PhutilBacktraceSignalHandler' => 'future/exec/PhutilBacktraceSignalHandler.php',
     'PhutilBallOfPHP' => 'phage/util/PhutilBallOfPHP.php',
     'PhutilBitbucketAuthAdapter' => 'auth/PhutilBitbucketAuthAdapter.php',
     'PhutilBootloader' => 'moduleutils/PhutilBootloader.php',
     'PhutilBootloaderException' => 'moduleutils/PhutilBootloaderException.php',
     'PhutilBritishEnglishLocale' => 'internationalization/locales/PhutilBritishEnglishLocale.php',
     'PhutilBufferedIterator' => 'utils/PhutilBufferedIterator.php',
     'PhutilBufferedIteratorTestCase' => 'utils/__tests__/PhutilBufferedIteratorTestCase.php',
     'PhutilBugtraqParser' => 'parser/PhutilBugtraqParser.php',
     'PhutilBugtraqParserTestCase' => 'parser/__tests__/PhutilBugtraqParserTestCase.php',
     'PhutilCIDRBlock' => 'ip/PhutilCIDRBlock.php',
     'PhutilCIDRList' => 'ip/PhutilCIDRList.php',
     'PhutilCLikeCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php',
     'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php',
+    'PhutilCallbackSignalHandler' => 'future/exec/PhutilCallbackSignalHandler.php',
     'PhutilChannel' => 'channel/PhutilChannel.php',
     'PhutilChannelChannel' => 'channel/PhutilChannelChannel.php',
     'PhutilChannelTestCase' => 'channel/__tests__/PhutilChannelTestCase.php',
     'PhutilChunkedIterator' => 'utils/PhutilChunkedIterator.php',
     'PhutilChunkedIteratorTestCase' => 'utils/__tests__/PhutilChunkedIteratorTestCase.php',
     'PhutilClassMapQuery' => 'symbols/PhutilClassMapQuery.php',
     'PhutilCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCodeSnippetContextFreeGrammar.php',
     'PhutilCommandString' => 'xsprintf/PhutilCommandString.php',
     'PhutilConsole' => 'console/PhutilConsole.php',
     'PhutilConsoleBlock' => 'console/view/PhutilConsoleBlock.php',
     'PhutilConsoleConcatenatedView' => 'console/view/PhutilConsoleConcatenatedView.php',
     'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php',
     'PhutilConsoleList' => 'console/view/PhutilConsoleList.php',
     'PhutilConsoleMessage' => 'console/PhutilConsoleMessage.php',
     'PhutilConsoleProgressBar' => 'console/PhutilConsoleProgressBar.php',
     'PhutilConsoleServer' => 'console/PhutilConsoleServer.php',
     'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php',
     'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php',
     'PhutilConsoleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php',
     'PhutilConsoleTable' => 'console/view/PhutilConsoleTable.php',
     'PhutilConsoleView' => 'console/view/PhutilConsoleView.php',
     'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php',
     'PhutilContextFreeGrammar' => 'grammar/PhutilContextFreeGrammar.php',
     'PhutilCowsay' => 'utils/PhutilCowsay.php',
     'PhutilCowsayTestCase' => 'utils/__tests__/PhutilCowsayTestCase.php',
     'PhutilCsprintfTestCase' => 'xsprintf/__tests__/PhutilCsprintfTestCase.php',
     'PhutilCzechLocale' => 'internationalization/locales/PhutilCzechLocale.php',
     'PhutilDaemon' => 'daemon/PhutilDaemon.php',
     'PhutilDaemonHandle' => 'daemon/PhutilDaemonHandle.php',
     'PhutilDaemonOverseer' => 'daemon/PhutilDaemonOverseer.php',
     'PhutilDaemonOverseerModule' => 'daemon/PhutilDaemonOverseerModule.php',
     'PhutilDefaultSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php',
     'PhutilDefaultSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php',
     'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php',
     'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php',
     'PhutilDeferredLog' => 'filesystem/PhutilDeferredLog.php',
     'PhutilDeferredLogTestCase' => 'filesystem/__tests__/PhutilDeferredLogTestCase.php',
     'PhutilDirectedScalarGraph' => 'utils/PhutilDirectedScalarGraph.php',
     'PhutilDirectoryFixture' => 'filesystem/PhutilDirectoryFixture.php',
     'PhutilDirectoryKeyValueCache' => 'cache/PhutilDirectoryKeyValueCache.php',
     'PhutilDisqusAuthAdapter' => 'auth/PhutilDisqusAuthAdapter.php',
     'PhutilDivinerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDivinerSyntaxHighlighter.php',
     'PhutilDocblockParser' => 'parser/PhutilDocblockParser.php',
     'PhutilDocblockParserTestCase' => 'parser/__tests__/PhutilDocblockParserTestCase.php',
     'PhutilEditDistanceMatrix' => 'utils/PhutilEditDistanceMatrix.php',
     'PhutilEditDistanceMatrixTestCase' => 'utils/__tests__/PhutilEditDistanceMatrixTestCase.php',
     'PhutilEditorConfig' => 'parser/PhutilEditorConfig.php',
     'PhutilEditorConfigTestCase' => 'parser/__tests__/PhutilEditorConfigTestCase.php',
     'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php',
     'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php',
     'PhutilEmojiLocale' => 'internationalization/locales/PhutilEmojiLocale.php',
     'PhutilEmptyAuthAdapter' => 'auth/PhutilEmptyAuthAdapter.php',
     'PhutilEnglishCanadaLocale' => 'internationalization/locales/PhutilEnglishCanadaLocale.php',
     'PhutilErrorHandler' => 'error/PhutilErrorHandler.php',
     'PhutilErrorHandlerTestCase' => 'error/__tests__/PhutilErrorHandlerTestCase.php',
     'PhutilErrorTrap' => 'error/PhutilErrorTrap.php',
     'PhutilEvent' => 'events/PhutilEvent.php',
     'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php',
     'PhutilEventEngine' => 'events/PhutilEventEngine.php',
     'PhutilEventListener' => 'events/PhutilEventListener.php',
     'PhutilEventType' => 'events/constant/PhutilEventType.php',
     'PhutilExampleBufferedIterator' => 'utils/PhutilExampleBufferedIterator.php',
     'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/PhutilExcessiveServiceCallsDaemon.php',
     'PhutilExecChannel' => 'channel/PhutilExecChannel.php',
     'PhutilExecPassthru' => 'future/exec/PhutilExecPassthru.php',
     'PhutilExecutableFuture' => 'future/exec/PhutilExecutableFuture.php',
     'PhutilExecutionEnvironment' => 'utils/PhutilExecutionEnvironment.php',
     'PhutilExtensionsTestCase' => 'moduleutils/__tests__/PhutilExtensionsTestCase.php',
     'PhutilFacebookAuthAdapter' => 'auth/PhutilFacebookAuthAdapter.php',
     'PhutilFatalDaemon' => 'daemon/torture/PhutilFatalDaemon.php',
     'PhutilFileLock' => 'filesystem/PhutilFileLock.php',
     'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php',
     'PhutilFileTree' => 'filesystem/PhutilFileTree.php',
     'PhutilFrenchLocale' => 'internationalization/locales/PhutilFrenchLocale.php',
     'PhutilGermanLocale' => 'internationalization/locales/PhutilGermanLocale.php',
     'PhutilGitHubAuthAdapter' => 'auth/PhutilGitHubAuthAdapter.php',
     'PhutilGitHubFuture' => 'future/github/PhutilGitHubFuture.php',
     'PhutilGitHubResponse' => 'future/github/PhutilGitHubResponse.php',
     'PhutilGitURI' => 'parser/PhutilGitURI.php',
     'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php',
     'PhutilGoogleAuthAdapter' => 'auth/PhutilGoogleAuthAdapter.php',
     'PhutilHTTPEngineExtension' => 'future/http/PhutilHTTPEngineExtension.php',
     'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php',
     'PhutilHashingIterator' => 'utils/PhutilHashingIterator.php',
     'PhutilHashingIteratorTestCase' => 'utils/__tests__/PhutilHashingIteratorTestCase.php',
     'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php',
     'PhutilHgsprintfTestCase' => 'xsprintf/__tests__/PhutilHgsprintfTestCase.php',
     'PhutilHighIntensityIntervalDaemon' => 'daemon/torture/PhutilHighIntensityIntervalDaemon.php',
     'PhutilINIParserException' => 'parser/exception/PhutilINIParserException.php',
     'PhutilIPAddress' => 'ip/PhutilIPAddress.php',
     'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php',
     'PhutilInRequestKeyValueCache' => 'cache/PhutilInRequestKeyValueCache.php',
     'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php',
     'PhutilInvalidRuleParserGeneratorException' => 'parser/generator/exception/PhutilInvalidRuleParserGeneratorException.php',
     'PhutilInvalidStateException' => 'exception/PhutilInvalidStateException.php',
     'PhutilInvalidStateExceptionTestCase' => 'exception/__tests__/PhutilInvalidStateExceptionTestCase.php',
     'PhutilInvisibleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilInvisibleSyntaxHighlighter.php',
     'PhutilIrreducibleRuleParserGeneratorException' => 'parser/generator/exception/PhutilIrreducibleRuleParserGeneratorException.php',
     'PhutilJIRAAuthAdapter' => 'auth/PhutilJIRAAuthAdapter.php',
     'PhutilJSON' => 'parser/PhutilJSON.php',
     'PhutilJSONFragmentLexer' => 'lexer/PhutilJSONFragmentLexer.php',
     'PhutilJSONFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilJSONFragmentLexerHighlighterTestCase.php',
     'PhutilJSONParser' => 'parser/PhutilJSONParser.php',
     'PhutilJSONParserException' => 'parser/exception/PhutilJSONParserException.php',
     'PhutilJSONParserTestCase' => 'parser/__tests__/PhutilJSONParserTestCase.php',
     'PhutilJSONProtocolChannel' => 'channel/PhutilJSONProtocolChannel.php',
     'PhutilJSONProtocolChannelTestCase' => 'channel/__tests__/PhutilJSONProtocolChannelTestCase.php',
     'PhutilJSONTestCase' => 'parser/__tests__/PhutilJSONTestCase.php',
     'PhutilJavaCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilJavaCodeSnippetContextFreeGrammar.php',
     'PhutilKeyValueCache' => 'cache/PhutilKeyValueCache.php',
     'PhutilKeyValueCacheNamespace' => 'cache/PhutilKeyValueCacheNamespace.php',
     'PhutilKeyValueCacheProfiler' => 'cache/PhutilKeyValueCacheProfiler.php',
     'PhutilKeyValueCacheProxy' => 'cache/PhutilKeyValueCacheProxy.php',
     'PhutilKeyValueCacheStack' => 'cache/PhutilKeyValueCacheStack.php',
     'PhutilKeyValueCacheTestCase' => 'cache/__tests__/PhutilKeyValueCacheTestCase.php',
     'PhutilKoreanLocale' => 'internationalization/locales/PhutilKoreanLocale.php',
     'PhutilLDAPAuthAdapter' => 'auth/PhutilLDAPAuthAdapter.php',
     'PhutilLanguageGuesser' => 'parser/PhutilLanguageGuesser.php',
     'PhutilLanguageGuesserTestCase' => 'parser/__tests__/PhutilLanguageGuesserTestCase.php',
     'PhutilLexer' => 'lexer/PhutilLexer.php',
     'PhutilLexerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php',
     'PhutilLibraryConflictException' => 'moduleutils/PhutilLibraryConflictException.php',
     'PhutilLibraryMapBuilder' => 'moduleutils/PhutilLibraryMapBuilder.php',
     'PhutilLibraryTestCase' => '__tests__/PhutilLibraryTestCase.php',
     'PhutilLipsumContextFreeGrammar' => 'grammar/PhutilLipsumContextFreeGrammar.php',
     'PhutilLocale' => 'internationalization/PhutilLocale.php',
     'PhutilLocaleTestCase' => 'internationalization/__tests__/PhutilLocaleTestCase.php',
     'PhutilLock' => 'filesystem/PhutilLock.php',
     'PhutilLockException' => 'filesystem/PhutilLockException.php',
     'PhutilLogFileChannel' => 'channel/PhutilLogFileChannel.php',
     'PhutilLunarPhase' => 'utils/PhutilLunarPhase.php',
     'PhutilLunarPhaseTestCase' => 'utils/__tests__/PhutilLunarPhaseTestCase.php',
     'PhutilMarkupEngine' => 'markup/PhutilMarkupEngine.php',
     'PhutilMarkupTestCase' => 'markup/__tests__/PhutilMarkupTestCase.php',
     'PhutilMemcacheKeyValueCache' => 'cache/PhutilMemcacheKeyValueCache.php',
     'PhutilMethodNotImplementedException' => 'error/PhutilMethodNotImplementedException.php',
     'PhutilMetricsChannel' => 'channel/PhutilMetricsChannel.php',
     'PhutilMissingSymbolException' => 'symbols/exception/PhutilMissingSymbolException.php',
     'PhutilModuleUtilsTestCase' => 'moduleutils/__tests__/PhutilModuleUtilsTestCase.php',
     'PhutilNiceDaemon' => 'daemon/torture/PhutilNiceDaemon.php',
     'PhutilNumber' => 'internationalization/PhutilNumber.php',
     'PhutilOAuth1AuthAdapter' => 'auth/PhutilOAuth1AuthAdapter.php',
     'PhutilOAuth1Future' => 'future/oauth/PhutilOAuth1Future.php',
     'PhutilOAuth1FutureTestCase' => 'future/oauth/__tests__/PhutilOAuth1FutureTestCase.php',
     'PhutilOAuthAuthAdapter' => 'auth/PhutilOAuthAuthAdapter.php',
     'PhutilOnDiskKeyValueCache' => 'cache/PhutilOnDiskKeyValueCache.php',
     'PhutilOpaqueEnvelope' => 'error/PhutilOpaqueEnvelope.php',
     'PhutilOpaqueEnvelopeKey' => 'error/PhutilOpaqueEnvelopeKey.php',
     'PhutilOpaqueEnvelopeTestCase' => 'error/__tests__/PhutilOpaqueEnvelopeTestCase.php',
     'PhutilPHPCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilPHPCodeSnippetContextFreeGrammar.php',
     'PhutilPHPFragmentLexer' => 'lexer/PhutilPHPFragmentLexer.php',
     'PhutilPHPFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php',
     'PhutilPHPFragmentLexerTestCase' => 'lexer/__tests__/PhutilPHPFragmentLexerTestCase.php',
     'PhutilPHPObjectProtocolChannel' => 'channel/PhutilPHPObjectProtocolChannel.php',
     'PhutilPHPObjectProtocolChannelTestCase' => 'channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php',
     'PhutilParserGenerator' => 'parser/PhutilParserGenerator.php',
     'PhutilParserGeneratorException' => 'parser/generator/exception/PhutilParserGeneratorException.php',
     'PhutilParserGeneratorTestCase' => 'parser/__tests__/PhutilParserGeneratorTestCase.php',
     'PhutilPayPalAPIFuture' => 'future/paypal/PhutilPayPalAPIFuture.php',
     'PhutilPerson' => 'internationalization/PhutilPerson.php',
     'PhutilPersonTest' => 'internationalization/__tests__/PhutilPersonTest.php',
     'PhutilPersonaAuthAdapter' => 'auth/PhutilPersonaAuthAdapter.php',
     'PhutilPhabricatorAuthAdapter' => 'auth/PhutilPhabricatorAuthAdapter.php',
     'PhutilPhtTestCase' => 'internationalization/__tests__/PhutilPhtTestCase.php',
     'PhutilPirateEnglishLocale' => 'internationalization/locales/PhutilPirateEnglishLocale.php',
     'PhutilPortugueseBrazilLocale' => 'internationalization/locales/PhutilPortugueseBrazilLocale.php',
     'PhutilPortuguesePortugalLocale' => 'internationalization/locales/PhutilPortuguesePortugalLocale.php',
     'PhutilPregsprintfTestCase' => 'xsprintf/__tests__/PhutilPregsprintfTestCase.php',
     'PhutilProcessGroupDaemon' => 'daemon/torture/PhutilProcessGroupDaemon.php',
     'PhutilProseDiff' => 'utils/PhutilProseDiff.php',
     'PhutilProseDiffTestCase' => 'utils/__tests__/PhutilProseDiffTestCase.php',
     'PhutilProseDifferenceEngine' => 'utils/PhutilProseDifferenceEngine.php',
     'PhutilProtocolChannel' => 'channel/PhutilProtocolChannel.php',
     'PhutilProxyException' => 'error/PhutilProxyException.php',
     'PhutilProxyIterator' => 'utils/PhutilProxyIterator.php',
     'PhutilPygmentizeParser' => 'parser/PhutilPygmentizeParser.php',
     'PhutilPygmentizeParserTestCase' => 'parser/__tests__/PhutilPygmentizeParserTestCase.php',
     'PhutilPygmentsSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php',
     'PhutilPythonFragmentLexer' => 'lexer/PhutilPythonFragmentLexer.php',
     'PhutilQsprintfInterface' => 'xsprintf/PhutilQsprintfInterface.php',
     'PhutilQueryStringParser' => 'parser/PhutilQueryStringParser.php',
     'PhutilQueryStringParserTestCase' => 'parser/__tests__/PhutilQueryStringParserTestCase.php',
     'PhutilRainbowSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php',
     'PhutilRawEnglishLocale' => 'internationalization/locales/PhutilRawEnglishLocale.php',
     'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php',
     'PhutilReadableSerializerTestCase' => 'readableserializer/__tests__/PhutilReadableSerializerTestCase.php',
     'PhutilRealNameContextFreeGrammar' => 'grammar/PhutilRealNameContextFreeGrammar.php',
     'PhutilRemarkupBlockInterpreter' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockInterpreter.php',
     'PhutilRemarkupBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockRule.php',
     'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/PhutilRemarkupBlockStorage.php',
     'PhutilRemarkupBoldRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupBoldRule.php',
     'PhutilRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupCodeBlockRule.php',
     'PhutilRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupDefaultBlockRule.php',
     'PhutilRemarkupDelRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDelRule.php',
     'PhutilRemarkupDocumentLinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php',
     'PhutilRemarkupEngine' => 'markup/engine/PhutilRemarkupEngine.php',
     'PhutilRemarkupEngineTestCase' => 'markup/engine/__tests__/PhutilRemarkupEngineTestCase.php',
     'PhutilRemarkupEscapeRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupEscapeRemarkupRule.php',
     'PhutilRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHeaderBlockRule.php',
     'PhutilRemarkupHighlightRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php',
     'PhutilRemarkupHorizontalRuleBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php',
     'PhutilRemarkupHyperlinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php',
     'PhutilRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInlineBlockRule.php',
     'PhutilRemarkupInterpreterBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInterpreterBlockRule.php',
     'PhutilRemarkupItalicRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupItalicRule.php',
     'PhutilRemarkupLinebreaksRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupLinebreaksRule.php',
     'PhutilRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php',
     'PhutilRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupLiteralBlockRule.php',
     'PhutilRemarkupMonospaceRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupMonospaceRule.php',
     'PhutilRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupNoteBlockRule.php',
     'PhutilRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupQuotesBlockRule.php',
     'PhutilRemarkupReplyBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupReplyBlockRule.php',
     'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRule.php',
     'PhutilRemarkupSimpleTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php',
     'PhutilRemarkupTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTableBlockRule.php',
     'PhutilRemarkupTestInterpreterRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php',
     'PhutilRemarkupUnderlineRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupUnderlineRule.php',
     'PhutilRope' => 'utils/PhutilRope.php',
     'PhutilRopeTestCase' => 'utils/__tests__/PhutilRopeTestCase.php',
     'PhutilSafeHTML' => 'markup/PhutilSafeHTML.php',
     'PhutilSafeHTMLProducerInterface' => 'markup/PhutilSafeHTMLProducerInterface.php',
     'PhutilSafeHTMLTestCase' => 'markup/__tests__/PhutilSafeHTMLTestCase.php',
     'PhutilSaturateStdoutDaemon' => 'daemon/torture/PhutilSaturateStdoutDaemon.php',
     'PhutilServiceProfiler' => 'serviceprofiler/PhutilServiceProfiler.php',
     'PhutilShellLexer' => 'lexer/PhutilShellLexer.php',
     'PhutilShellLexerTestCase' => 'lexer/__tests__/PhutilShellLexerTestCase.php',
+    'PhutilSignalHandler' => 'future/exec/PhutilSignalHandler.php',
+    'PhutilSignalRouter' => 'future/exec/PhutilSignalRouter.php',
     'PhutilSimpleOptions' => 'parser/PhutilSimpleOptions.php',
     'PhutilSimpleOptionsLexer' => 'lexer/PhutilSimpleOptionsLexer.php',
     'PhutilSimpleOptionsLexerTestCase' => 'lexer/__tests__/PhutilSimpleOptionsLexerTestCase.php',
     'PhutilSimpleOptionsTestCase' => 'parser/__tests__/PhutilSimpleOptionsTestCase.php',
     'PhutilSimplifiedChineseLocale' => 'internationalization/locales/PhutilSimplifiedChineseLocale.php',
     'PhutilSlackAuthAdapter' => 'auth/PhutilSlackAuthAdapter.php',
     'PhutilSlackFuture' => 'future/slack/PhutilSlackFuture.php',
     'PhutilSocketChannel' => 'channel/PhutilSocketChannel.php',
     'PhutilSortVector' => 'utils/PhutilSortVector.php',
     'PhutilSpanishSpainLocale' => 'internationalization/locales/PhutilSpanishSpainLocale.php',
     'PhutilSprite' => 'sprites/PhutilSprite.php',
     'PhutilSpriteSheet' => 'sprites/PhutilSpriteSheet.php',
     'PhutilStreamIterator' => 'utils/PhutilStreamIterator.php',
     'PhutilSymbolLoader' => 'symbols/PhutilSymbolLoader.php',
     'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilSyntaxHighlighter.php',
     'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilSyntaxHighlighterEngine.php',
     'PhutilSyntaxHighlighterException' => 'markup/syntax/highlighter/PhutilSyntaxHighlighterException.php',
     'PhutilSystem' => 'utils/PhutilSystem.php',
     'PhutilSystemTestCase' => 'utils/__tests__/PhutilSystemTestCase.php',
     'PhutilTerminalString' => 'xsprintf/PhutilTerminalString.php',
     'PhutilTestPhobject' => 'object/__tests__/PhutilTestPhobject.php',
     'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php',
     'PhutilTraditionalChineseLocale' => 'internationalization/locales/PhutilTraditionalChineseLocale.php',
     'PhutilTranslation' => 'internationalization/PhutilTranslation.php',
     'PhutilTranslationTestCase' => 'internationalization/__tests__/PhutilTranslationTestCase.php',
     'PhutilTranslator' => 'internationalization/PhutilTranslator.php',
     'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php',
     'PhutilTsprintfTestCase' => 'xsprintf/__tests__/PhutilTsprintfTestCase.php',
     'PhutilTwitchAuthAdapter' => 'auth/PhutilTwitchAuthAdapter.php',
     'PhutilTwitchFuture' => 'future/twitch/PhutilTwitchFuture.php',
     'PhutilTwitterAuthAdapter' => 'auth/PhutilTwitterAuthAdapter.php',
     'PhutilTypeCheckException' => 'parser/exception/PhutilTypeCheckException.php',
     'PhutilTypeExtraParametersException' => 'parser/exception/PhutilTypeExtraParametersException.php',
     'PhutilTypeLexer' => 'lexer/PhutilTypeLexer.php',
     'PhutilTypeMissingParametersException' => 'parser/exception/PhutilTypeMissingParametersException.php',
     'PhutilTypeSpec' => 'parser/PhutilTypeSpec.php',
     'PhutilTypeSpecTestCase' => 'parser/__tests__/PhutilTypeSpecTestCase.php',
     'PhutilURI' => 'parser/PhutilURI.php',
     'PhutilURITestCase' => 'parser/__tests__/PhutilURITestCase.php',
     'PhutilUSEnglishLocale' => 'internationalization/locales/PhutilUSEnglishLocale.php',
     'PhutilUTF8StringTruncator' => 'utils/PhutilUTF8StringTruncator.php',
     'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php',
     'PhutilUnknownSymbolParserGeneratorException' => 'parser/generator/exception/PhutilUnknownSymbolParserGeneratorException.php',
     'PhutilUnreachableRuleParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableRuleParserGeneratorException.php',
     'PhutilUnreachableTerminalParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableTerminalParserGeneratorException.php',
     'PhutilUrisprintfTestCase' => 'xsprintf/__tests__/PhutilUrisprintfTestCase.php',
     'PhutilUtilsTestCase' => 'utils/__tests__/PhutilUtilsTestCase.php',
     'PhutilVeryWowEnglishLocale' => 'internationalization/locales/PhutilVeryWowEnglishLocale.php',
     'PhutilWordPressAuthAdapter' => 'auth/PhutilWordPressAuthAdapter.php',
     'PhutilWordPressFuture' => 'future/wordpress/PhutilWordPressFuture.php',
     'PhutilXHPASTBinary' => 'parser/xhpast/bin/PhutilXHPASTBinary.php',
     'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php',
     'PhutilXHPASTSyntaxHighlighterFuture' => 'markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php',
     'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php',
     'QueryFuture' => 'future/query/QueryFuture.php',
     'TempFile' => 'filesystem/TempFile.php',
     'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php',
     'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php',
     'XHPASTNodeTestCase' => 'parser/xhpast/api/__tests__/XHPASTNodeTestCase.php',
     'XHPASTSyntaxErrorException' => 'parser/xhpast/api/XHPASTSyntaxErrorException.php',
     'XHPASTToken' => 'parser/xhpast/api/XHPASTToken.php',
     'XHPASTTree' => 'parser/xhpast/api/XHPASTTree.php',
     'XHPASTTreeTestCase' => 'parser/xhpast/api/__tests__/XHPASTTreeTestCase.php',
     'XsprintfUnknownConversionException' => 'xsprintf/exception/XsprintfUnknownConversionException.php',
   ),
   'function' => array(
     'array_fuse' => 'utils/utils.php',
     'array_interleave' => 'utils/utils.php',
     'array_mergev' => 'utils/utils.php',
     'array_select_keys' => 'utils/utils.php',
     'assert_instances_of' => 'utils/utils.php',
     'assert_stringlike' => 'utils/utils.php',
     'coalesce' => 'utils/utils.php',
     'csprintf' => 'xsprintf/csprintf.php',
     'exec_manual' => 'future/exec/execx.php',
     'execx' => 'future/exec/execx.php',
     'head' => 'utils/utils.php',
     'head_key' => 'utils/utils.php',
     'hgsprintf' => 'xsprintf/hgsprintf.php',
     'hsprintf' => 'markup/render.php',
     'id' => 'utils/utils.php',
     'idx' => 'utils/utils.php',
     'idxv' => 'utils/utils.php',
     'ifilter' => 'utils/utils.php',
     'igroup' => 'utils/utils.php',
     'ipull' => 'utils/utils.php',
     'isort' => 'utils/utils.php',
     'jsprintf' => 'xsprintf/jsprintf.php',
     'last' => 'utils/utils.php',
     'last_key' => 'utils/utils.php',
     'ldap_sprintf' => 'xsprintf/ldapsprintf.php',
     'mfilter' => 'utils/utils.php',
     'mgroup' => 'utils/utils.php',
     'mpull' => 'utils/utils.php',
     'msort' => 'utils/utils.php',
     'msortv' => 'utils/utils.php',
     'newv' => 'utils/utils.php',
     'nonempty' => 'utils/utils.php',
     'phlog' => 'error/phlog.php',
     'pht' => 'internationalization/pht.php',
     'phutil_censor_credentials' => 'utils/utils.php',
     'phutil_console_confirm' => 'console/format.php',
     'phutil_console_format' => 'console/format.php',
     'phutil_console_get_terminal_width' => 'console/format.php',
     'phutil_console_prompt' => 'console/format.php',
     'phutil_console_require_tty' => 'console/format.php',
     'phutil_console_wrap' => 'console/format.php',
     'phutil_count' => 'internationalization/pht.php',
     'phutil_date_format' => 'utils/viewutils.php',
     'phutil_deprecated' => 'moduleutils/moduleutils.php',
     'phutil_error_listener_example' => 'error/phlog.php',
     'phutil_escape_html' => 'markup/render.php',
     'phutil_escape_html_newlines' => 'markup/render.php',
     'phutil_escape_uri' => 'markup/render.php',
     'phutil_escape_uri_path_component' => 'markup/render.php',
     'phutil_fnmatch' => 'utils/utils.php',
     'phutil_format_bytes' => 'utils/viewutils.php',
     'phutil_format_relative_time' => 'utils/viewutils.php',
     'phutil_format_relative_time_detailed' => 'utils/viewutils.php',
     'phutil_format_units_generic' => 'utils/viewutils.php',
     'phutil_fwrite_nonblocking_stream' => 'utils/utils.php',
     'phutil_get_current_library_name' => 'moduleutils/moduleutils.php',
     'phutil_get_library_name_for_root' => 'moduleutils/moduleutils.php',
     'phutil_get_library_root' => 'moduleutils/moduleutils.php',
     'phutil_get_library_root_for_path' => 'moduleutils/moduleutils.php',
     'phutil_get_signal_name' => 'future/exec/execx.php',
     'phutil_hashes_are_identical' => 'utils/utils.php',
     'phutil_implode_html' => 'markup/render.php',
     'phutil_ini_decode' => 'utils/utils.php',
     'phutil_is_hiphop_runtime' => 'utils/utils.php',
     'phutil_is_utf8' => 'utils/utf8.php',
     'phutil_is_utf8_slowly' => 'utils/utf8.php',
     'phutil_is_utf8_with_only_bmp_characters' => 'utils/utf8.php',
     'phutil_is_windows' => 'utils/utils.php',
     'phutil_json_decode' => 'utils/utils.php',
     'phutil_json_encode' => 'utils/utils.php',
     'phutil_load_library' => 'moduleutils/core.php',
     'phutil_loggable_string' => 'utils/utils.php',
     'phutil_parse_bytes' => 'utils/viewutils.php',
     'phutil_passthru' => 'future/exec/execx.php',
     'phutil_register_library' => 'moduleutils/core.php',
     'phutil_register_library_map' => 'moduleutils/core.php',
     'phutil_safe_html' => 'markup/render.php',
     'phutil_split_lines' => 'utils/utils.php',
     'phutil_tag' => 'markup/render.php',
     'phutil_tag_div' => 'markup/render.php',
     'phutil_unescape_uri_path_component' => 'markup/render.php',
     'phutil_units' => 'utils/utils.php',
     'phutil_utf8_console_strlen' => 'utils/utf8.php',
     'phutil_utf8_convert' => 'utils/utf8.php',
     'phutil_utf8_encode_codepoint' => 'utils/utf8.php',
     'phutil_utf8_hard_wrap' => 'utils/utf8.php',
     'phutil_utf8_hard_wrap_html' => 'utils/utf8.php',
     'phutil_utf8_is_combining_character' => 'utils/utf8.php',
     'phutil_utf8_strlen' => 'utils/utf8.php',
     'phutil_utf8_strtolower' => 'utils/utf8.php',
     'phutil_utf8_strtoupper' => 'utils/utf8.php',
     'phutil_utf8_strtr' => 'utils/utf8.php',
     'phutil_utf8_ucwords' => 'utils/utf8.php',
     'phutil_utf8ize' => 'utils/utf8.php',
     'phutil_utf8v' => 'utils/utf8.php',
     'phutil_utf8v_codepoints' => 'utils/utf8.php',
     'phutil_utf8v_combine_characters' => 'utils/utf8.php',
     'phutil_utf8v_combined' => 'utils/utf8.php',
     'phutil_validate_json' => 'utils/utils.php',
     'phutil_var_export' => 'utils/utils.php',
     'ppull' => 'utils/utils.php',
     'pregsprintf' => 'xsprintf/pregsprintf.php',
     'qsprintf' => 'xsprintf/qsprintf.php',
     'qsprintf_check_scalar_type' => 'xsprintf/qsprintf.php',
     'qsprintf_check_type' => 'xsprintf/qsprintf.php',
     'queryfx' => 'xsprintf/queryfx.php',
     'queryfx_all' => 'xsprintf/queryfx.php',
     'queryfx_one' => 'xsprintf/queryfx.php',
     'tsprintf' => 'xsprintf/tsprintf.php',
     'urisprintf' => 'xsprintf/urisprintf.php',
     'vcsprintf' => 'xsprintf/csprintf.php',
     'vjsprintf' => 'xsprintf/jsprintf.php',
     'vqsprintf' => 'xsprintf/qsprintf.php',
     'vurisprintf' => 'xsprintf/urisprintf.php',
     'xhp_parser_node_constants' => 'parser/xhpast/parser_nodes.php',
     'xhpast_parser_token_constants' => 'parser/xhpast/parser_tokens.php',
     'xsprintf' => 'xsprintf/xsprintf.php',
     'xsprintf_callback_example' => 'xsprintf/xsprintf.php',
     'xsprintf_command' => 'xsprintf/csprintf.php',
     'xsprintf_javascript' => 'xsprintf/jsprintf.php',
     'xsprintf_ldap' => 'xsprintf/ldapsprintf.php',
     'xsprintf_mercurial' => 'xsprintf/hgsprintf.php',
     'xsprintf_query' => 'xsprintf/qsprintf.php',
     'xsprintf_regex' => 'xsprintf/pregsprintf.php',
     'xsprintf_terminal' => 'xsprintf/tsprintf.php',
     'xsprintf_uri' => 'xsprintf/urisprintf.php',
   ),
   'xmap' => array(
     'AASTNode' => 'Phobject',
     'AASTNodeList' => array(
       'Phobject',
       'Countable',
       'Iterator',
     ),
     'AASTToken' => 'Phobject',
     'AASTTree' => 'Phobject',
     'AbstractDirectedGraph' => 'Phobject',
     'AbstractDirectedGraphTestCase' => 'PhutilTestCase',
     'AphrontAccessDeniedQueryException' => 'AphrontQueryException',
     'AphrontBaseMySQLDatabaseConnection' => 'AphrontDatabaseConnection',
     'AphrontCharacterSetQueryException' => 'AphrontQueryException',
     'AphrontConnectionLostQueryException' => 'AphrontRecoverableQueryException',
     'AphrontConnectionQueryException' => 'AphrontQueryException',
     'AphrontCountQueryException' => 'AphrontQueryException',
     'AphrontDatabaseConnection' => array(
       'Phobject',
       'PhutilQsprintfInterface',
     ),
     'AphrontDatabaseTransactionState' => 'Phobject',
     'AphrontDeadlockQueryException' => 'AphrontRecoverableQueryException',
     'AphrontDuplicateKeyQueryException' => 'AphrontQueryException',
     'AphrontInvalidCredentialsQueryException' => 'AphrontQueryException',
     'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
     'AphrontLockTimeoutQueryException' => 'AphrontRecoverableQueryException',
     'AphrontMySQLDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
     'AphrontMySQLiDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
     'AphrontNotSupportedQueryException' => 'AphrontQueryException',
     'AphrontObjectMissingQueryException' => 'AphrontQueryException',
     'AphrontParameterQueryException' => 'AphrontQueryException',
     'AphrontQueryException' => 'Exception',
     'AphrontQueryTimeoutQueryException' => 'AphrontRecoverableQueryException',
     'AphrontRecoverableQueryException' => 'AphrontQueryException',
     'AphrontRequestStream' => 'Phobject',
     'AphrontSchemaQueryException' => 'AphrontQueryException',
     'AphrontScopedUnguardedWriteCapability' => 'Phobject',
     'AphrontWriteGuard' => 'Phobject',
     'BaseHTTPFuture' => 'Future',
     'CaseInsensitiveArray' => 'PhutilArray',
     'CaseInsensitiveArrayTestCase' => 'PhutilTestCase',
     'CommandException' => 'Exception',
     'ConduitClient' => 'Phobject',
     'ConduitClientException' => 'Exception',
     'ConduitClientTestCase' => 'PhutilTestCase',
     'ConduitFuture' => 'FutureProxy',
     'ExecFuture' => 'PhutilExecutableFuture',
     'ExecFutureTestCase' => 'PhutilTestCase',
     'ExecPassthruTestCase' => 'PhutilTestCase',
     'FileFinder' => 'Phobject',
     'FileFinderTestCase' => 'PhutilTestCase',
     'FileList' => 'Phobject',
     'Filesystem' => 'Phobject',
     'FilesystemException' => 'Exception',
     'FilesystemTestCase' => 'PhutilTestCase',
     'Future' => 'Phobject',
     'FutureIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'FutureIteratorTestCase' => 'PhutilTestCase',
     'FutureProxy' => 'Future',
     'HTTPFuture' => 'BaseHTTPFuture',
     'HTTPFutureCURLResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPFutureCertificateResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPFutureHTTPResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPFutureParseResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPFutureResponseStatus' => 'Exception',
     'HTTPFutureTransportResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPSFuture' => 'BaseHTTPFuture',
     'ImmediateFuture' => 'Future',
     'LibphutilUSEnglishTranslation' => 'PhutilTranslation',
     'LinesOfALarge' => array(
       'Phobject',
       'Iterator',
     ),
     'LinesOfALargeExecFuture' => 'LinesOfALarge',
     'LinesOfALargeExecFutureTestCase' => 'PhutilTestCase',
     'LinesOfALargeFile' => 'LinesOfALarge',
     'LinesOfALargeFileTestCase' => 'PhutilTestCase',
     'MFilterTestHelper' => 'Phobject',
     'PHPASTParserTestCase' => 'PhutilTestCase',
     'PhageAgentBootloader' => 'Phobject',
     'PhageAgentTestCase' => 'PhutilTestCase',
     'PhagePHPAgent' => 'Phobject',
     'PhagePHPAgentBootloader' => 'PhageAgentBootloader',
     'Phobject' => 'Iterator',
     'PhobjectTestCase' => 'PhutilTestCase',
     'PhutilAPCKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilAWSEC2Future' => 'PhutilAWSFuture',
     'PhutilAWSException' => 'Exception',
     'PhutilAWSFuture' => 'FutureProxy',
     'PhutilAWSManagementWorkflow' => 'PhutilArgumentWorkflow',
     'PhutilAWSS3DeleteManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
     'PhutilAWSS3Future' => 'PhutilAWSFuture',
     'PhutilAWSS3GetManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
     'PhutilAWSS3ManagementWorkflow' => 'PhutilAWSManagementWorkflow',
     'PhutilAWSS3PutManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
     'PhutilAWSv4Signature' => 'Phobject',
     'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase',
     'PhutilAggregateException' => 'Exception',
     'PhutilAllCapsEnglishLocale' => 'PhutilLocale',
     'PhutilAmazonAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilArgumentParser' => 'Phobject',
     'PhutilArgumentParserException' => 'Exception',
     'PhutilArgumentParserTestCase' => 'PhutilTestCase',
     'PhutilArgumentSpecification' => 'Phobject',
     'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException',
     'PhutilArgumentSpecificationTestCase' => 'PhutilTestCase',
     'PhutilArgumentSpellingCorrector' => 'Phobject',
     'PhutilArgumentSpellingCorrectorTestCase' => 'PhutilTestCase',
     'PhutilArgumentUsageException' => 'PhutilArgumentParserException',
     'PhutilArgumentWorkflow' => 'Phobject',
     'PhutilArray' => array(
       'Phobject',
       'Countable',
       'ArrayAccess',
       'Iterator',
     ),
     'PhutilArrayTestCase' => 'PhutilTestCase',
     'PhutilArrayWithDefaultValue' => 'PhutilArray',
     'PhutilAsanaAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilAsanaFuture' => 'FutureProxy',
     'PhutilAuthAdapter' => 'Phobject',
     'PhutilAuthConfigurationException' => 'PhutilAuthException',
     'PhutilAuthCredentialException' => 'PhutilAuthException',
     'PhutilAuthException' => 'Exception',
     'PhutilAuthUserAbortedException' => 'PhutilAuthException',
+    'PhutilBacktraceSignalHandler' => 'PhutilSignalHandler',
     'PhutilBallOfPHP' => 'Phobject',
     'PhutilBitbucketAuthAdapter' => 'PhutilOAuth1AuthAdapter',
     'PhutilBootloaderException' => 'Exception',
     'PhutilBritishEnglishLocale' => 'PhutilLocale',
     'PhutilBufferedIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'PhutilBufferedIteratorTestCase' => 'PhutilTestCase',
     'PhutilBugtraqParser' => 'Phobject',
     'PhutilBugtraqParserTestCase' => 'PhutilTestCase',
     'PhutilCIDRBlock' => 'Phobject',
     'PhutilCIDRList' => 'Phobject',
     'PhutilCLikeCodeSnippetContextFreeGrammar' => 'PhutilCodeSnippetContextFreeGrammar',
     'PhutilCallbackFilterIterator' => 'FilterIterator',
+    'PhutilCallbackSignalHandler' => 'PhutilSignalHandler',
     'PhutilChannel' => 'Phobject',
     'PhutilChannelChannel' => 'PhutilChannel',
     'PhutilChannelTestCase' => 'PhutilTestCase',
     'PhutilChunkedIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'PhutilChunkedIteratorTestCase' => 'PhutilTestCase',
     'PhutilClassMapQuery' => 'Phobject',
     'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar',
     'PhutilCommandString' => 'Phobject',
     'PhutilConsole' => 'Phobject',
     'PhutilConsoleBlock' => 'PhutilConsoleView',
     'PhutilConsoleConcatenatedView' => 'PhutilConsoleView',
     'PhutilConsoleFormatter' => 'Phobject',
     'PhutilConsoleList' => 'PhutilConsoleView',
     'PhutilConsoleMessage' => 'Phobject',
     'PhutilConsoleProgressBar' => 'Phobject',
     'PhutilConsoleServer' => 'Phobject',
     'PhutilConsoleServerChannel' => 'PhutilChannelChannel',
     'PhutilConsoleStdinNotInteractiveException' => 'Exception',
     'PhutilConsoleSyntaxHighlighter' => 'Phobject',
     'PhutilConsoleTable' => 'PhutilConsoleView',
     'PhutilConsoleView' => 'Phobject',
     'PhutilConsoleWrapTestCase' => 'PhutilTestCase',
     'PhutilContextFreeGrammar' => 'Phobject',
     'PhutilCowsay' => 'Phobject',
     'PhutilCowsayTestCase' => 'PhutilTestCase',
     'PhutilCsprintfTestCase' => 'PhutilTestCase',
     'PhutilCzechLocale' => 'PhutilLocale',
     'PhutilDaemon' => 'Phobject',
     'PhutilDaemonHandle' => 'Phobject',
     'PhutilDaemonOverseer' => 'Phobject',
     'PhutilDaemonOverseerModule' => 'Phobject',
     'PhutilDefaultSyntaxHighlighter' => 'Phobject',
     'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine',
     'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy',
     'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'PhutilTestCase',
     'PhutilDeferredLog' => 'Phobject',
     'PhutilDeferredLogTestCase' => 'PhutilTestCase',
     'PhutilDirectedScalarGraph' => 'AbstractDirectedGraph',
     'PhutilDirectoryFixture' => 'Phobject',
     'PhutilDirectoryKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilDisqusAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilDivinerSyntaxHighlighter' => 'Phobject',
     'PhutilDocblockParser' => 'Phobject',
     'PhutilDocblockParserTestCase' => 'PhutilTestCase',
     'PhutilEditDistanceMatrix' => 'Phobject',
     'PhutilEditDistanceMatrixTestCase' => 'PhutilTestCase',
     'PhutilEditorConfig' => 'Phobject',
     'PhutilEditorConfigTestCase' => 'PhutilTestCase',
     'PhutilEmailAddress' => 'Phobject',
     'PhutilEmailAddressTestCase' => 'PhutilTestCase',
     'PhutilEmojiLocale' => 'PhutilLocale',
     'PhutilEmptyAuthAdapter' => 'PhutilAuthAdapter',
     'PhutilEnglishCanadaLocale' => 'PhutilLocale',
     'PhutilErrorHandler' => 'Phobject',
     'PhutilErrorHandlerTestCase' => 'PhutilTestCase',
     'PhutilErrorTrap' => 'Phobject',
     'PhutilEvent' => 'Phobject',
     'PhutilEventConstants' => 'Phobject',
     'PhutilEventEngine' => 'Phobject',
     'PhutilEventListener' => 'Phobject',
     'PhutilEventType' => 'PhutilEventConstants',
     'PhutilExampleBufferedIterator' => 'PhutilBufferedIterator',
     'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon',
     'PhutilExecChannel' => 'PhutilChannel',
     'PhutilExecPassthru' => 'PhutilExecutableFuture',
     'PhutilExecutableFuture' => 'Future',
     'PhutilExecutionEnvironment' => 'Phobject',
     'PhutilExtensionsTestCase' => 'PhutilTestCase',
     'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilFatalDaemon' => 'PhutilTortureTestDaemon',
     'PhutilFileLock' => 'PhutilLock',
     'PhutilFileLockTestCase' => 'PhutilTestCase',
     'PhutilFileTree' => 'Phobject',
     'PhutilFrenchLocale' => 'PhutilLocale',
     'PhutilGermanLocale' => 'PhutilLocale',
     'PhutilGitHubAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilGitHubFuture' => 'FutureProxy',
     'PhutilGitHubResponse' => 'Phobject',
     'PhutilGitURI' => 'Phobject',
     'PhutilGitURITestCase' => 'PhutilTestCase',
     'PhutilGoogleAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilHTTPEngineExtension' => 'Phobject',
     'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon',
     'PhutilHashingIterator' => array(
       'PhutilProxyIterator',
       'Iterator',
     ),
     'PhutilHashingIteratorTestCase' => 'PhutilTestCase',
     'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow',
     'PhutilHgsprintfTestCase' => 'PhutilTestCase',
     'PhutilHighIntensityIntervalDaemon' => 'PhutilTortureTestDaemon',
     'PhutilINIParserException' => 'Exception',
     'PhutilIPAddress' => 'Phobject',
     'PhutilIPAddressTestCase' => 'PhutilTestCase',
     'PhutilInRequestKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilInteractiveEditor' => 'Phobject',
     'PhutilInvalidRuleParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilInvalidStateException' => 'Exception',
     'PhutilInvalidStateExceptionTestCase' => 'PhutilTestCase',
     'PhutilInvisibleSyntaxHighlighter' => 'Phobject',
     'PhutilIrreducibleRuleParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilJIRAAuthAdapter' => 'PhutilOAuth1AuthAdapter',
     'PhutilJSON' => 'Phobject',
     'PhutilJSONFragmentLexer' => 'PhutilLexer',
     'PhutilJSONFragmentLexerHighlighterTestCase' => 'PhutilTestCase',
     'PhutilJSONParser' => 'Phobject',
     'PhutilJSONParserException' => 'Exception',
     'PhutilJSONParserTestCase' => 'PhutilTestCase',
     'PhutilJSONProtocolChannel' => 'PhutilProtocolChannel',
     'PhutilJSONProtocolChannelTestCase' => 'PhutilTestCase',
     'PhutilJSONTestCase' => 'PhutilTestCase',
     'PhutilJavaCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
     'PhutilKeyValueCache' => 'Phobject',
     'PhutilKeyValueCacheNamespace' => 'PhutilKeyValueCacheProxy',
     'PhutilKeyValueCacheProfiler' => 'PhutilKeyValueCacheProxy',
     'PhutilKeyValueCacheProxy' => 'PhutilKeyValueCache',
     'PhutilKeyValueCacheStack' => 'PhutilKeyValueCache',
     'PhutilKeyValueCacheTestCase' => 'PhutilTestCase',
     'PhutilKoreanLocale' => 'PhutilLocale',
     'PhutilLDAPAuthAdapter' => 'PhutilAuthAdapter',
     'PhutilLanguageGuesser' => 'Phobject',
     'PhutilLanguageGuesserTestCase' => 'PhutilTestCase',
     'PhutilLexer' => 'Phobject',
     'PhutilLexerSyntaxHighlighter' => 'PhutilSyntaxHighlighter',
     'PhutilLibraryConflictException' => 'Exception',
     'PhutilLibraryMapBuilder' => 'Phobject',
     'PhutilLibraryTestCase' => 'PhutilTestCase',
     'PhutilLipsumContextFreeGrammar' => 'PhutilContextFreeGrammar',
     'PhutilLocale' => 'Phobject',
     'PhutilLocaleTestCase' => 'PhutilTestCase',
     'PhutilLock' => 'Phobject',
     'PhutilLockException' => 'Exception',
     'PhutilLogFileChannel' => 'PhutilChannelChannel',
     'PhutilLunarPhase' => 'Phobject',
     'PhutilLunarPhaseTestCase' => 'PhutilTestCase',
     'PhutilMarkupEngine' => 'Phobject',
     'PhutilMarkupTestCase' => 'PhutilTestCase',
     'PhutilMemcacheKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilMethodNotImplementedException' => 'Exception',
     'PhutilMetricsChannel' => 'PhutilChannelChannel',
     'PhutilMissingSymbolException' => 'Exception',
     'PhutilModuleUtilsTestCase' => 'PhutilTestCase',
     'PhutilNiceDaemon' => 'PhutilTortureTestDaemon',
     'PhutilNumber' => 'Phobject',
     'PhutilOAuth1AuthAdapter' => 'PhutilAuthAdapter',
     'PhutilOAuth1Future' => 'FutureProxy',
     'PhutilOAuth1FutureTestCase' => 'PhutilTestCase',
     'PhutilOAuthAuthAdapter' => 'PhutilAuthAdapter',
     'PhutilOnDiskKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilOpaqueEnvelope' => 'Phobject',
     'PhutilOpaqueEnvelopeKey' => 'Phobject',
     'PhutilOpaqueEnvelopeTestCase' => 'PhutilTestCase',
     'PhutilPHPCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
     'PhutilPHPFragmentLexer' => 'PhutilLexer',
     'PhutilPHPFragmentLexerHighlighterTestCase' => 'PhutilTestCase',
     'PhutilPHPFragmentLexerTestCase' => 'PhutilTestCase',
     'PhutilPHPObjectProtocolChannel' => 'PhutilProtocolChannel',
     'PhutilPHPObjectProtocolChannelTestCase' => 'PhutilTestCase',
     'PhutilParserGenerator' => 'Phobject',
     'PhutilParserGeneratorException' => 'Exception',
     'PhutilParserGeneratorTestCase' => 'PhutilTestCase',
     'PhutilPayPalAPIFuture' => 'FutureProxy',
     'PhutilPersonTest' => array(
       'Phobject',
       'PhutilPerson',
     ),
     'PhutilPersonaAuthAdapter' => 'PhutilAuthAdapter',
     'PhutilPhabricatorAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilPhtTestCase' => 'PhutilTestCase',
     'PhutilPirateEnglishLocale' => 'PhutilLocale',
     'PhutilPortugueseBrazilLocale' => 'PhutilLocale',
     'PhutilPortuguesePortugalLocale' => 'PhutilLocale',
     'PhutilPregsprintfTestCase' => 'PhutilTestCase',
     'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon',
     'PhutilProseDiff' => 'Phobject',
     'PhutilProseDiffTestCase' => 'PhutilTestCase',
     'PhutilProseDifferenceEngine' => 'Phobject',
     'PhutilProtocolChannel' => 'PhutilChannelChannel',
     'PhutilProxyException' => 'Exception',
     'PhutilProxyIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'PhutilPygmentizeParser' => 'Phobject',
     'PhutilPygmentizeParserTestCase' => 'PhutilTestCase',
     'PhutilPygmentsSyntaxHighlighter' => 'Phobject',
     'PhutilPythonFragmentLexer' => 'PhutilLexer',
     'PhutilQueryStringParser' => 'Phobject',
     'PhutilQueryStringParserTestCase' => 'PhutilTestCase',
     'PhutilRainbowSyntaxHighlighter' => 'Phobject',
     'PhutilRawEnglishLocale' => 'PhutilLocale',
     'PhutilReadableSerializer' => 'Phobject',
     'PhutilReadableSerializerTestCase' => 'PhutilTestCase',
     'PhutilRealNameContextFreeGrammar' => 'PhutilContextFreeGrammar',
     'PhutilRemarkupBlockInterpreter' => 'Phobject',
     'PhutilRemarkupBlockRule' => 'Phobject',
     'PhutilRemarkupBlockStorage' => 'Phobject',
     'PhutilRemarkupBoldRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupCodeBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupDefaultBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupDelRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupDocumentLinkRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupEngine' => 'PhutilMarkupEngine',
     'PhutilRemarkupEngineTestCase' => 'PhutilTestCase',
     'PhutilRemarkupEscapeRemarkupRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupHeaderBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupHighlightRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupHorizontalRuleBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupHyperlinkRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupInlineBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupInterpreterBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupItalicRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupLinebreaksRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupListBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupLiteralBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupMonospaceRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupNoteBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupQuotesBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupReplyBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupRule' => 'Phobject',
     'PhutilRemarkupSimpleTableBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupTableBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupTestInterpreterRule' => 'PhutilRemarkupBlockInterpreter',
     'PhutilRemarkupUnderlineRule' => 'PhutilRemarkupRule',
     'PhutilRope' => 'Phobject',
     'PhutilRopeTestCase' => 'PhutilTestCase',
     'PhutilSafeHTML' => 'Phobject',
     'PhutilSafeHTMLTestCase' => 'PhutilTestCase',
     'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon',
     'PhutilServiceProfiler' => 'Phobject',
     'PhutilShellLexer' => 'PhutilLexer',
     'PhutilShellLexerTestCase' => 'PhutilTestCase',
+    'PhutilSignalHandler' => 'Phobject',
+    'PhutilSignalRouter' => 'Phobject',
     'PhutilSimpleOptions' => 'Phobject',
     'PhutilSimpleOptionsLexer' => 'PhutilLexer',
     'PhutilSimpleOptionsLexerTestCase' => 'PhutilTestCase',
     'PhutilSimpleOptionsTestCase' => 'PhutilTestCase',
     'PhutilSimplifiedChineseLocale' => 'PhutilLocale',
     'PhutilSlackAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilSlackFuture' => 'FutureProxy',
     'PhutilSocketChannel' => 'PhutilChannel',
     'PhutilSortVector' => 'Phobject',
     'PhutilSpanishSpainLocale' => 'PhutilLocale',
     'PhutilSprite' => 'Phobject',
     'PhutilSpriteSheet' => 'Phobject',
     'PhutilStreamIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'PhutilSyntaxHighlighter' => 'Phobject',
     'PhutilSyntaxHighlighterEngine' => 'Phobject',
     'PhutilSyntaxHighlighterException' => 'Exception',
     'PhutilSystem' => 'Phobject',
     'PhutilSystemTestCase' => 'PhutilTestCase',
     'PhutilTerminalString' => 'Phobject',
     'PhutilTestPhobject' => 'Phobject',
     'PhutilTortureTestDaemon' => 'PhutilDaemon',
     'PhutilTraditionalChineseLocale' => 'PhutilLocale',
     'PhutilTranslation' => 'Phobject',
     'PhutilTranslationTestCase' => 'PhutilTestCase',
     'PhutilTranslator' => 'Phobject',
     'PhutilTranslatorTestCase' => 'PhutilTestCase',
     'PhutilTsprintfTestCase' => 'PhutilTestCase',
     'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilTwitchFuture' => 'FutureProxy',
     'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter',
     'PhutilTypeCheckException' => 'Exception',
     'PhutilTypeExtraParametersException' => 'Exception',
     'PhutilTypeLexer' => 'PhutilLexer',
     'PhutilTypeMissingParametersException' => 'Exception',
     'PhutilTypeSpec' => 'Phobject',
     'PhutilTypeSpecTestCase' => 'PhutilTestCase',
     'PhutilURI' => 'Phobject',
     'PhutilURITestCase' => 'PhutilTestCase',
     'PhutilUSEnglishLocale' => 'PhutilLocale',
     'PhutilUTF8StringTruncator' => 'Phobject',
     'PhutilUTF8TestCase' => 'PhutilTestCase',
     'PhutilUnknownSymbolParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilUnreachableRuleParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilUnreachableTerminalParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilUrisprintfTestCase' => 'PhutilTestCase',
     'PhutilUtilsTestCase' => 'PhutilTestCase',
     'PhutilVeryWowEnglishLocale' => 'PhutilLocale',
     'PhutilWordPressAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilWordPressFuture' => 'FutureProxy',
     'PhutilXHPASTBinary' => 'Phobject',
     'PhutilXHPASTSyntaxHighlighter' => 'Phobject',
     'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy',
     'PhutilXHPASTSyntaxHighlighterTestCase' => 'PhutilTestCase',
     'QueryFuture' => 'Future',
     'TempFile' => 'Phobject',
     'TestAbstractDirectedGraph' => 'AbstractDirectedGraph',
     'XHPASTNode' => 'AASTNode',
     'XHPASTNodeTestCase' => 'PhutilTestCase',
     'XHPASTSyntaxErrorException' => 'Exception',
     'XHPASTToken' => 'AASTToken',
     'XHPASTTree' => 'AASTTree',
     'XHPASTTreeTestCase' => 'PhutilTestCase',
     'XsprintfUnknownConversionException' => 'InvalidArgumentException',
   ),
 ));
diff --git a/src/daemon/PhutilDaemon.php b/src/daemon/PhutilDaemon.php
index 43a6c3f..629a567 100644
--- a/src/daemon/PhutilDaemon.php
+++ b/src/daemon/PhutilDaemon.php
@@ -1,410 +1,407 @@
 <?php
 
 /**
  * Scaffolding for implementing robust background processing scripts.
  *
  *
  * Autoscaling
  * ===========
  *
  * Autoscaling automatically launches copies of a daemon when it is busy
  * (scaling the pool up) and stops them when they're idle (scaling the pool
  * down). This is appropriate for daemons which perform highly parallelizable
  * work.
  *
  * To make a daemon support autoscaling, the implementation should look
  * something like this:
  *
  *   while (!$this->shouldExit()) {
  *     if (work_available()) {
  *       $this->willBeginWork();
  *       do_work();
  *       $this->sleep(0);
  *     } else {
  *       $this->willBeginIdle();
  *       $this->sleep(1);
  *     }
  *   }
  *
  * In particular, call @{method:willBeginWork} before becoming busy, and
  * @{method:willBeginIdle} when no work is available. If the daemon is launched
  * into an autoscale pool, this will cause the pool to automatically scale up
  * when busy and down when idle.
  *
  * See @{class:PhutilHighIntensityIntervalDaemon} for an example of a simple
  * autoscaling daemon.
  *
  * Launching a daemon which does not make these callbacks into an autoscale
  * pool will have no effect.
  *
  * @task overseer Communicating With the Overseer
  * @task autoscale Autoscaling Daemon Pools
  */
 abstract class PhutilDaemon extends Phobject {
 
   const MESSAGETYPE_STDOUT = 'stdout';
   const MESSAGETYPE_HEARTBEAT = 'heartbeat';
   const MESSAGETYPE_BUSY = 'busy';
   const MESSAGETYPE_IDLE = 'idle';
   const MESSAGETYPE_DOWN = 'down';
 
   const WORKSTATE_BUSY = 'busy';
   const WORKSTATE_IDLE = 'idle';
 
   private $argv;
   private $traceMode;
   private $traceMemory;
   private $verbose;
   private $notifyReceived;
   private $inGracefulShutdown;
   private $workState = null;
   private $idleSince = null;
   private $autoscaleProperties = array();
 
   final public function setVerbose($verbose) {
     $this->verbose = $verbose;
     return $this;
   }
 
   final public function getVerbose() {
     return $this->verbose;
   }
 
-  private static $sighandlerInstalled;
-
   final public function __construct(array $argv) {
     $this->argv = $argv;
 
-    if (!self::$sighandlerInstalled) {
-      self::$sighandlerInstalled = true;
-      pcntl_signal(SIGTERM, __CLASS__.'::exitOnSignal');
+    $router = PhutilSignalRouter::getRouter();
+    $handler_key = 'daemon.term';
+    if (!$router->getHandler($handler_key)) {
+      $handler = new PhutilCallbackSignalHandler(
+        SIGTERM,
+        __CLASS__.'::onTermSignal');
+      $router->installHandler($handler_key, $handler);
     }
 
     pcntl_signal(SIGINT, array($this, 'onGracefulSignal'));
     pcntl_signal(SIGUSR2, array($this, 'onNotifySignal'));
 
     // Without discard mode, this consumes unbounded amounts of memory. Keep
     // memory bounded.
     PhutilServiceProfiler::getInstance()->enableDiscardMode();
 
     $this->beginStdoutCapture();
   }
 
   final public function __destruct() {
     $this->endStdoutCapture();
   }
 
   final public function stillWorking() {
     $this->emitOverseerMessage(self::MESSAGETYPE_HEARTBEAT, null);
 
     if ($this->traceMemory) {
       $daemon = get_class($this);
       fprintf(
         STDERR,
         "%s %s %s\n",
         '<RAMS>',
         $daemon,
         pht(
           'Memory Usage: %s KB',
           new PhutilNumber(memory_get_usage() / 1024, 1)));
     }
   }
 
   final public function shouldExit() {
     return $this->inGracefulShutdown;
   }
 
   final protected function sleep($duration) {
     $this->notifyReceived = false;
     $this->willSleep($duration);
     $this->stillWorking();
 
     $is_autoscale = $this->isClonedAutoscaleDaemon();
     $scale_down = $this->getAutoscaleDownDuration();
 
     $max_sleep = 60;
     if ($is_autoscale) {
       $max_sleep = min($max_sleep, $scale_down);
     }
 
     if ($is_autoscale) {
       if ($this->workState == self::WORKSTATE_IDLE) {
         $dur = (time() - $this->idleSince);
         $this->log(pht('Idle for %s seconds.', $dur));
       }
     }
 
     while ($duration > 0 &&
       !$this->notifyReceived &&
       !$this->shouldExit()) {
 
       // If this is an autoscaling clone and we've been idle for too long,
       // we're going to scale the pool down by exiting and not restarting. The
       // DOWN message tells the overseer that we don't want to be restarted.
       if ($is_autoscale) {
         if ($this->workState == self::WORKSTATE_IDLE) {
           if ($this->idleSince && ($this->idleSince + $scale_down < time())) {
             $this->inGracefulShutdown = true;
             $this->emitOverseerMessage(self::MESSAGETYPE_DOWN, null);
             $this->log(
               pht(
                 'Daemon was idle for more than %s second(s), '.
                 'scaling pool down.',
                 new PhutilNumber($scale_down)));
             break;
           }
         }
       }
 
       sleep(min($duration, $max_sleep));
       $duration -= $max_sleep;
       $this->stillWorking();
     }
   }
 
   protected function willSleep($duration) {
     return;
   }
 
-  public static function exitOnSignal($signo) {
+  public static function onTermSignal($signo) {
     self::didCatchSignal($signo);
-
-    // Normally, PHP doesn't invoke destructors when exiting in response to
-    // a signal. This forces it to do so, so we have a fighting chance of
-    // releasing any locks, leases or resources on our way out.
-    exit(128 + $signo);
   }
 
   final protected function getArgv() {
     return $this->argv;
   }
 
   final public function execute() {
     $this->willRun();
     $this->run();
   }
 
   abstract protected function run();
 
   final public function setTraceMemory() {
     $this->traceMemory = true;
     return $this;
   }
 
   final public function getTraceMemory() {
     return $this->traceMemory;
   }
 
   final public function setTraceMode() {
     $this->traceMode = true;
     PhutilServiceProfiler::installEchoListener();
     PhutilConsole::getConsole()->getServer()->setEnableLog(true);
     $this->didSetTraceMode();
     return $this;
   }
 
   final public function getTraceMode() {
     return $this->traceMode;
   }
 
   final public function onGracefulSignal($signo) {
     self::didCatchSignal($signo);
     $this->inGracefulShutdown = true;
   }
 
   final public function onNotifySignal($signo) {
     self::didCatchSignal($signo);
     $this->notifyReceived = true;
     $this->onNotify($signo);
   }
 
   protected function onNotify($signo) {
     // This is a hook for subclasses.
   }
 
   protected function willRun() {
     // This is a hook for subclasses.
   }
 
   protected function didSetTraceMode() {
     // This is a hook for subclasses.
   }
 
   final protected function log($message) {
     if ($this->verbose) {
       $daemon = get_class($this);
       fprintf(STDERR, "%s %s %s\n", '<VERB>', $daemon, $message);
     }
   }
 
   private static function didCatchSignal($signo) {
     $signame = phutil_get_signal_name($signo);
     fprintf(
       STDERR,
       "%s Caught signal %s (%s).\n",
       '<SGNL>',
       $signo,
       $signame);
   }
 
 
 /* -(  Communicating With the Overseer  )------------------------------------ */
 
 
   private function beginStdoutCapture() {
     ob_start(array($this, 'didReceiveStdout'), 2);
   }
 
   private function endStdoutCapture() {
     ob_end_flush();
   }
 
   public function didReceiveStdout($data) {
     if (!strlen($data)) {
       return '';
     }
 
     return $this->encodeOverseerMessage(self::MESSAGETYPE_STDOUT, $data);
   }
 
   private function encodeOverseerMessage($type, $data) {
     $structure = array($type);
 
     if ($data !== null) {
       $structure[] = $data;
     }
 
     return json_encode($structure)."\n";
   }
 
   private function emitOverseerMessage($type, $data) {
     $this->endStdoutCapture();
     echo $this->encodeOverseerMessage($type, $data);
     $this->beginStdoutCapture();
   }
 
   public static function errorListener($event, $value, array $metadata) {
     // If the caller has redirected the error log to a file, PHP won't output
     // messages to stderr, so the overseer can't capture them. Install a
     // listener which just  echoes errors to stderr, so the overseer is always
     // aware of errors.
 
     $console = PhutilConsole::getConsole();
     $message = idx($metadata, 'default_message');
 
     if ($message) {
       $console->writeErr("%s\n", $message);
     }
     if (idx($metadata, 'trace')) {
       $trace = PhutilErrorHandler::formatStacktrace($metadata['trace']);
       $console->writeErr("%s\n", $trace);
     }
   }
 
 
 /* -(  Autoscaling  )-------------------------------------------------------- */
 
 
   /**
    * Prepare to become busy. This may autoscale the pool up.
    *
    * This notifies the overseer that the daemon has become busy. If daemons
    * that are part of an autoscale pool are continuously busy for a prolonged
    * period of time, the overseer may scale up the pool.
    *
    * @return this
    * @task autoscale
    */
   protected function willBeginWork() {
     if ($this->workState != self::WORKSTATE_BUSY) {
       $this->workState = self::WORKSTATE_BUSY;
       $this->idleSince = null;
       $this->emitOverseerMessage(self::MESSAGETYPE_BUSY, null);
     }
 
     return $this;
   }
 
 
   /**
    * Prepare to idle. This may autoscale the pool down.
    *
    * This notifies the overseer that the daemon is no longer busy. If daemons
    * that are part of an autoscale pool are idle for a prolonged period of time,
    * they may exit to scale the pool down.
    *
    * @return this
    * @task autoscale
    */
   protected function willBeginIdle() {
     if ($this->workState != self::WORKSTATE_IDLE) {
       $this->workState = self::WORKSTATE_IDLE;
       $this->idleSince = time();
       $this->emitOverseerMessage(self::MESSAGETYPE_IDLE, null);
     }
 
     return $this;
   }
 
 
   /**
    * Determine if this is a clone or the original daemon.
    *
    * @return bool True if this is an cloned autoscaling daemon.
    * @task autoscale
    */
   private function isClonedAutoscaleDaemon() {
     return (bool)$this->getAutoscaleProperty('clone', false);
   }
 
 
   /**
    * Get the duration (in seconds) which a daemon must be continuously idle
    * for before it should exit to scale the pool down.
    *
    * @return int Duration, in seconds.
    * @task autoscale
    */
   private function getAutoscaleDownDuration() {
     return $this->getAutoscaleProperty('down', 15);
   }
 
 
   /**
    * Configure autoscaling for this daemon.
    *
    * @param map<string, wild> Map of autoscale properties.
    * @return this
    * @task autoscale
    */
   public function setAutoscaleProperties(array $autoscale_properties) {
     PhutilTypeSpec::checkMap(
       $autoscale_properties,
       array(
         'group' => 'optional string',
         'up' => 'optional int',
         'down' => 'optional int',
         'pool' => 'optional int',
         'clone' => 'optional bool',
         'reserve' => 'optional int|float',
       ));
 
     $this->autoscaleProperties = $autoscale_properties;
 
     return $this;
   }
 
 
   /**
    * Read autoscaling configuration for this daemon.
    *
    * @param string Property to read.
    * @param wild Default value to return if the property is not set.
    * @return wild Property value, or `$default` if one is not set.
    * @task autoscale
    */
   private function getAutoscaleProperty($key, $default = null) {
     return idx($this->autoscaleProperties, $key, $default);
   }
 
 }
diff --git a/src/future/exec/PhutilBacktraceSignalHandler.php b/src/future/exec/PhutilBacktraceSignalHandler.php
new file mode 100644
index 0000000..131a54b
--- /dev/null
+++ b/src/future/exec/PhutilBacktraceSignalHandler.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * Signal handler for SIGHUP which prints the current backtrace out to a
+ * file. This is particularly helpful in debugging hung/spinning processes.
+ */
+final class PhutilBacktraceSignalHandler extends PhutilSignalHandler {
+
+  public function canHandleSignal(PhutilSignalRouter $router, $signo) {
+    return ($signo === SIGHUP);
+  }
+
+  public function handleSignal(PhutilSignalRouter $router, $signo) {
+    $e = new Exception();
+    $pid = getmypid();
+    // Some Phabricator daemons may not be attached to a terminal.
+    Filesystem::writeFile(
+      sys_get_temp_dir().'/phabricator_backtrace_'.$pid,
+      $e->getTraceAsString());
+  }
+
+}
diff --git a/src/future/exec/PhutilCallbackSignalHandler.php b/src/future/exec/PhutilCallbackSignalHandler.php
new file mode 100644
index 0000000..2cde547
--- /dev/null
+++ b/src/future/exec/PhutilCallbackSignalHandler.php
@@ -0,0 +1,22 @@
+<?php
+
+
+final class PhutilCallbackSignalHandler extends PhutilSignalHandler {
+
+  private $signal;
+  private $callback;
+
+  public function __construct($signal, $callback) {
+    $this->signal = $signal;
+    $this->callback = $callback;
+  }
+
+  public function canHandleSignal(PhutilSignalRouter $router, $signo) {
+    return ($signo === $this->signal);
+  }
+
+  public function handleSignal(PhutilSignalRouter $router, $signo) {
+    call_user_func($this->callback, $signo);
+  }
+
+}
diff --git a/src/future/exec/PhutilSignalHandler.php b/src/future/exec/PhutilSignalHandler.php
new file mode 100644
index 0000000..d0ffb9d
--- /dev/null
+++ b/src/future/exec/PhutilSignalHandler.php
@@ -0,0 +1,8 @@
+<?php
+
+abstract class PhutilSignalHandler extends Phobject {
+
+  abstract public function canHandleSignal(PhutilSignalRouter $router, $signo);
+  abstract public function handleSignal(PhutilSignalRouter $router, $signo);
+
+}
diff --git a/src/future/exec/PhutilSignalRouter.php b/src/future/exec/PhutilSignalRouter.php
new file mode 100644
index 0000000..11e985a
--- /dev/null
+++ b/src/future/exec/PhutilSignalRouter.php
@@ -0,0 +1,80 @@
+<?php
+
+final class PhutilSignalRouter extends Phobject {
+
+  private $handlers = array();
+  private static $router;
+
+  private function __construct() {
+    // <private>
+  }
+
+  public static function initialize() {
+    if (!self::$router) {
+      $router = new self();
+
+      pcntl_signal(SIGHUP, array($router, 'routeSignal'));
+      pcntl_signal(SIGTERM, array($router, 'routeSignal'));
+
+      self::$router = $router;
+    }
+
+    return self::getRouter();
+  }
+
+  public static function getRouter() {
+    if (!self::$router) {
+      throw new Exception(pht('Signal router has not been initialized!'));
+    }
+
+    return self::$router;
+  }
+
+  public function installHandler($key, PhutilSignalHandler $handler) {
+    if (isset($this->handlers[$key])) {
+      throw new Exception(
+        pht(
+          'Signal handler with key "%s" is already installed.',
+          $key));
+    }
+
+    $this->handlers[$key] = $handler;
+
+    return $this;
+  }
+
+  public function getHandler($key) {
+    return idx($this->handlers, $key);
+  }
+
+  public function routeSignal($signo) {
+    $exceptions = array();
+
+    $handlers = $this->handlers;
+    foreach ($handlers as $key => $handler) {
+      try {
+        if ($handler->canHandleSignal($this, $signo)) {
+          $handler->handleSignal($this, $signo);
+        }
+      } catch (Exception $ex) {
+        $exceptions[] = $ex;
+      }
+    }
+
+    if ($exceptions) {
+      throw new PhutilAggregateException(
+        pht(
+          'Signal handlers raised exceptions while handling "%s".',
+          phutil_get_signal_name($signo)));
+    }
+
+    switch ($signo) {
+      case SIGTERM:
+        // Normally, PHP doesn't invoke destructors when exiting in response to
+        // a signal. This forces it to do so, so we have a fighting chance of
+        // releasing any locks, leases or resources on our way out.
+        exit(128 + $signo);
+    }
+  }
+
+}