diff --git a/scripts/__init_script__.php b/scripts/__init_script__.php --- a/scripts/__init_script__.php +++ b/scripts/__init_script__.php @@ -1,5 +1,7 @@ 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 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -119,6 +119,7 @@ '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', @@ -132,6 +133,7 @@ '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', @@ -358,6 +360,8 @@ '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', @@ -680,6 +684,7 @@ 'PhutilAuthCredentialException' => 'PhutilAuthException', 'PhutilAuthException' => 'Exception', 'PhutilAuthUserAbortedException' => 'PhutilAuthException', + 'PhutilBacktraceSignalHandler' => 'PhutilSignalHandler', 'PhutilBallOfPHP' => 'Phobject', 'PhutilBitbucketAuthAdapter' => 'PhutilOAuth1AuthAdapter', 'PhutilBootloaderException' => 'Exception', @@ -695,6 +700,7 @@ 'PhutilCIDRList' => 'Phobject', 'PhutilCLikeCodeSnippetContextFreeGrammar' => 'PhutilCodeSnippetContextFreeGrammar', 'PhutilCallbackFilterIterator' => 'FilterIterator', + 'PhutilCallbackSignalHandler' => 'PhutilSignalHandler', 'PhutilChannel' => 'Phobject', 'PhutilChannelChannel' => 'PhutilChannel', 'PhutilChannelTestCase' => 'PhutilTestCase', @@ -930,6 +936,8 @@ 'PhutilServiceProfiler' => 'Phobject', 'PhutilShellLexer' => 'PhutilLexer', 'PhutilShellLexerTestCase' => 'PhutilTestCase', + 'PhutilSignalHandler' => 'Phobject', + 'PhutilSignalRouter' => 'Phobject', 'PhutilSimpleOptions' => 'Phobject', 'PhutilSimpleOptionsLexer' => 'PhutilLexer', 'PhutilSimpleOptionsLexerTestCase' => 'PhutilTestCase', diff --git a/src/daemon/PhutilDaemon.php b/src/daemon/PhutilDaemon.php --- a/src/daemon/PhutilDaemon.php +++ b/src/daemon/PhutilDaemon.php @@ -70,14 +70,16 @@ 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')); @@ -166,13 +168,8 @@ 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() { diff --git a/src/future/exec/PhutilBacktraceSignalHandler.php b/src/future/exec/PhutilBacktraceSignalHandler.php new file mode 100644 --- /dev/null +++ b/src/future/exec/PhutilBacktraceSignalHandler.php @@ -0,0 +1,22 @@ +getTraceAsString()); + } + +} diff --git a/src/future/exec/PhutilCallbackSignalHandler.php b/src/future/exec/PhutilCallbackSignalHandler.php new file mode 100644 --- /dev/null +++ b/src/future/exec/PhutilCallbackSignalHandler.php @@ -0,0 +1,22 @@ +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 --- /dev/null +++ b/src/future/exec/PhutilSignalHandler.php @@ -0,0 +1,8 @@ + + } + + 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); + } + } + +}