diff --git a/src/App.php b/src/App.php index 96fc1fc..270393e 100644 --- a/src/App.php +++ b/src/App.php @@ -207,12 +207,40 @@ private function runLoop() \fwrite(STDERR, (string)$orig); }); + try { + Loop::addSignal(\defined('SIGINT') ? \SIGINT : 2, $f1 = function () use ($socket) { + if (\PHP_VERSION_ID >= 70200 && \stream_isatty(\STDIN)) { + echo "\r"; + } + $this->sapi->log('Received SIGINT, stopping loop'); + + $socket->close(); + Loop::stop(); + }); + Loop::addSignal(\defined('SIGTERM') ? \SIGTERM : 15, $f2 = function () use ($socket) { + $this->sapi->log('Received SIGTERM, stopping loop'); + + $socket->close(); + Loop::stop(); + }); + } catch (\BadMethodCallException $e) { // @codeCoverageIgnoreStart + $this->sapi->log('Notice: No signal handler support, installing ext-ev or ext-pcntl recommended for production use.'); + } // @codeCoverageIgnoreEnd + do { Loop::run(); - // Fiber compatibility mode for PHP < 8.1: Restart loop as long as socket is available - $this->sapi->log('Warning: Loop restarted. Upgrade to react/async v4 recommended for production use.'); - } while ($socket->getAddress() !== null); + if ($socket->getAddress() !== null) { + // Fiber compatibility mode for PHP < 8.1: Restart loop as long as socket is available + $this->sapi->log('Warning: Loop restarted. Upgrade to react/async v4 recommended for production use.'); + } else { + break; + } + } while (true); + + // remove signal handlers when loop stops (if registered) + Loop::removeSignal(\defined('SIGINT') ? \SIGINT : 2, $f1); + Loop::removeSignal(\defined('SIGTERM') ? \SIGTERM : 15, $f2 ?? 'printf'); } private function runOnce() diff --git a/tests/AppTest.php b/tests/AppTest.php index 008685b..845f6e7 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -169,6 +169,8 @@ public function testRunWillReportListeningAddressAndRunLoopWithSocketServer() Loop::removeReadStream($socket); fclose($socket); + + Loop::stop(); }); $this->expectOutputRegex('/' . preg_quote('Listening on http://127.0.0.1:8080' . PHP_EOL, '/') . '.*/'); @@ -191,6 +193,8 @@ public function testRunWillReportListeningAddressFromEnvironmentAndRunLoopWithSo Loop::removeReadStream($socket); fclose($socket); + + Loop::stop(); }); $this->expectOutputRegex('/' . preg_quote('Listening on http://' . $addr . PHP_EOL, '/') . '.*/'); @@ -209,12 +213,72 @@ public function testRunWillReportListeningAddressFromEnvironmentWithRandomPortAn Loop::removeReadStream($socket); fclose($socket); + + Loop::stop(); }); $this->expectOutputRegex('/' . preg_quote('Listening on http://127.0.0.1:', '/') . '\d+' . PHP_EOL . '.*/'); $app->run(); } + public function testRunWillRestartLoopUntilSocketIsClosed() + { + $_SERVER['X_LISTEN'] = '127.0.0.1:0'; + $app = new App(); + + // lovely: remove socket server on next tick to terminate loop + Loop::futureTick(function () { + $resources = get_resources(); + $socket = end($resources); + + Loop::futureTick(function () use ($socket) { + Loop::removeReadStream($socket); + fclose($socket); + + Loop::stop(); + }); + + Loop::stop(); + }); + + $this->expectOutputRegex('/' . preg_quote('Warning: Loop restarted. Upgrade to react/async v4 recommended for production use.' . PHP_EOL, '/') . '$/'); + $app->run(); + } + + /** + * @requires function pcntl_signal + * @requires function posix_kill + */ + public function testRunWillStopWhenReceivingSigint() + { + $_SERVER['X_LISTEN'] = '127.0.0.1:0'; + $app = new App(); + + Loop::futureTick(function () { + posix_kill(getmypid(), defined('SIGINT') ? SIGINT : 2); + }); + + $this->expectOutputRegex('/' . preg_quote('Received SIGINT, stopping loop' . PHP_EOL, '/') . '$/'); + $app->run(); + } + + /** + * @requires function pcntl_signal + * @requires function posix_kill + */ + public function testRunWillStopWhenReceivingSigterm() + { + $_SERVER['X_LISTEN'] = '127.0.0.1:0'; + $app = new App(); + + Loop::futureTick(function () { + posix_kill(getmypid(), defined('SIGTERM') ? SIGTERM : 15); + }); + + $this->expectOutputRegex('/' . preg_quote('Received SIGTERM, stopping loop' . PHP_EOL, '/') . '$/'); + $app->run(); + } + public function testRunAppWithEmptyAddressThrows() { $_SERVER['X_LISTEN'] = '';