diff --git a/composer.json b/composer.json index 18e66e7..c69c780 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ "ext-json": "*", "phpunit/phpunit": "^9", "jetbrains/phpstorm-stubs": "^2019.3", - "psalm/phar": "^4.7" + "psalm/phar": "^5" }, "autoload": { "psr-4": { diff --git a/psalm.xml b/psalm.xml index 7c63f8b..cdb9653 100644 --- a/psalm.xml +++ b/psalm.xml @@ -28,6 +28,13 @@ + + + + + + + diff --git a/src/EventLoop/Driver/StreamSelectDriver.php b/src/EventLoop/Driver/StreamSelectDriver.php index 03b8a1b..e94c486 100644 --- a/src/EventLoop/Driver/StreamSelectDriver.php +++ b/src/EventLoop/Driver/StreamSelectDriver.php @@ -9,6 +9,7 @@ use Revolt\EventLoop\Internal\AbstractDriver; use Revolt\EventLoop\Internal\DriverCallback; use Revolt\EventLoop\Internal\SignalCallback; +use Revolt\EventLoop\Internal\SignalCallbackExtra; use Revolt\EventLoop\Internal\StreamReadableCallback; use Revolt\EventLoop\Internal\StreamWritableCallback; use Revolt\EventLoop\Internal\TimerCallback; @@ -31,10 +32,10 @@ final class StreamSelectDriver extends AbstractDriver private readonly TimerQueue $timerQueue; - /** @var array> */ + /** @var array> */ private array $signalCallbacks = []; - /** @var \SplQueue */ + /** @var \SplQueue */ private readonly \SplQueue $signalQueue; private bool $signalHandling; @@ -101,6 +102,18 @@ public function onSignal(int $signal, \Closure $closure): string return parent::onSignal($signal, $closure); } + /** + * @throws UnsupportedFeatureException If the pcntl extension is not available. + */ + public function onSignalWithInfo(int $signal, \Closure $closure): string + { + if (!$this->signalHandling) { + throw new UnsupportedFeatureException("Signal handling requires the pcntl extension"); + } + + return parent::onSignalWithInfo($signal, $closure); + } + public function getHandle(): mixed { return null; @@ -120,9 +133,12 @@ protected function dispatch(bool $blocking): void \pcntl_signal_dispatch(); while (!$this->signalQueue->isEmpty()) { - $signal = $this->signalQueue->dequeue(); + [$signal, $siginfo] = $this->signalQueue->dequeue(); foreach ($this->signalCallbacks[$signal] as $callback) { + if ($callback instanceof SignalCallbackExtra) { + $callback->siginfo = $siginfo; + } $this->enqueueCallback($callback); } @@ -160,7 +176,7 @@ protected function activate(array $callbacks): void $this->writeStreams[$streamId] = $callback->stream; } elseif ($callback instanceof TimerCallback) { $this->timerQueue->insert($callback); - } elseif ($callback instanceof SignalCallback) { + } elseif ($callback instanceof SignalCallback || $callback instanceof SignalCallbackExtra) { if (!isset($this->signalCallbacks[$callback->signal])) { \set_error_handler(static function (int $errno, string $errstr): bool { throw new UnsupportedFeatureException( @@ -203,7 +219,7 @@ protected function deactivate(DriverCallback $callback): void } } elseif ($callback instanceof TimerCallback) { $this->timerQueue->remove($callback); - } elseif ($callback instanceof SignalCallback) { + } elseif ($callback instanceof SignalCallback || $callback instanceof SignalCallbackExtra) { if (isset($this->signalCallbacks[$callback->signal])) { unset($this->signalCallbacks[$callback->signal][$callback->id]); @@ -304,7 +320,7 @@ private function selectStreams(array $read, array $write, float $timeout): void } if ($timeout > 0) { // Sleep until next timer expires. - /** @psalm-var positive-int $timeout */ + /** @psalm-suppress ArgumentTypeCoercion $timeout is always > 0, even if there is no psalm type to represent a positive float. */ \usleep((int) ($timeout * 1_000_000)); } } @@ -325,9 +341,9 @@ private function getTimeout(): float return $expiration > 0 ? $expiration : 0.0; } - private function handleSignal(int $signal): void + private function handleSignal(int $signal, mixed $siginfo): void { // Queue signals, so we don't suspend inside pcntl_signal_dispatch, which disables signals while it runs - $this->signalQueue->enqueue($signal); + $this->signalQueue->enqueue([$signal, $siginfo]); } } diff --git a/src/EventLoop/Driver/TracingDriver.php b/src/EventLoop/Driver/TracingDriver.php index d16ea32..e4713e2 100644 --- a/src/EventLoop/Driver/TracingDriver.php +++ b/src/EventLoop/Driver/TracingDriver.php @@ -249,7 +249,7 @@ private function getCancelTrace(string $callbackId): string /** * Formats a stacktrace obtained via `debug_backtrace()`. * - * @param array $trace + * @param list, class?: class-string, file?: string, function: string, line?: int, object?: object, type?: string}> $trace * Output of `debug_backtrace()`. * * @return string Formatted stacktrace. @@ -259,7 +259,7 @@ private function formatStacktrace(array $trace): string return \implode("\n", \array_map(static function ($e, $i) { $line = "#{$i} "; - if (isset($e["file"])) { + if (isset($e["file"]) && isset($e['line'])) { $line .= "{$e['file']}:{$e['line']} "; } diff --git a/src/EventLoop/Internal/AbstractDriver.php b/src/EventLoop/Internal/AbstractDriver.php index 78578bd..a87c518 100644 --- a/src/EventLoop/Internal/AbstractDriver.php +++ b/src/EventLoop/Internal/AbstractDriver.php @@ -194,6 +194,16 @@ public function onSignal(int $signal, \Closure $closure): string return $signalCallback->id; } + protected function onSignalWithInfo(int $signal, \Closure $closure): string + { + $signalCallback = new SignalCallbackExtra($this->nextId++, $closure, $signal, null); + + $this->callbacks[$signalCallback->id] = $signalCallback; + $this->enableQueue[$signalCallback->id] = $signalCallback; + + return $signalCallback->id; + } + public function enable(string $callbackId): string { if (!isset($this->callbacks[$callbackId])) { @@ -517,6 +527,7 @@ private function createLoopFiber(): void // Invoke microtasks if we have some $this->invokeCallbacks(); + /** @var bool $this->stopped */ while (!$this->stopped) { if ($this->interrupt) { $this->invokeInterrupt(); @@ -574,6 +585,11 @@ private function createCallbackFiber(): void $callback->id, $callback->signal ), + $callback instanceof SignalCallbackExtra => ($callback->closure)( + $callback->id, + $callback->signal, + $callback->siginfo + ), default => ($callback->closure)($callback->id), }; diff --git a/src/EventLoop/Internal/SignalCallbackExtra.php b/src/EventLoop/Internal/SignalCallbackExtra.php new file mode 100644 index 0000000..ddff510 --- /dev/null +++ b/src/EventLoop/Internal/SignalCallbackExtra.php @@ -0,0 +1,18 @@ +start(function (Driver $loop) use (&$invoked, &$callbackId) { - $callbackId = $loop->onSignal(SIGUSR1, function () use (&$invoked) { + $this->start(function (StreamSelectDriver $loop) use (&$invoked, &$callbackId) { + $callbackId = $loop->onSignalWithInfo(SIGUSR1, function ($cId, $sig, $siginfo) use (&$callbackId, &$invoked) { + $this->assertEquals($callbackId, $cId); + $this->assertEquals(SIGUSR1, $sig); + $this->assertEquals(SIGUSR1, $siginfo['signo']); + $this->assertEquals(\getmypid(), $siginfo['pid']); + $this->assertEquals(\getmyuid(), $siginfo['uid']); $invoked = true; }); @@ -121,7 +126,7 @@ public function testSignalDuringStreamSelectIgnored(): void $sockets = \stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); - $this->start(function (Driver $loop) use ($sockets, &$signalCallbackId) { + $this->start(function (StreamSelectDriver $loop) use ($sockets, &$signalCallbackId) { $socketCallbackIds = [ $loop->onReadable($sockets[0], function () { // nothing