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