Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions etc/bootstrap/signals.php.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

use parallel\Channel;
use ReactParallel\Runtime\Signal;
use ReactParallel\Runtime\SignalListener;

require '{{AUTOLOAD}}';

/** @var Channel<Signal> $channel */
$channel = Channel::open('{{CHANNEL}}');

SignalListener::listen($channel);

unlink(__FILE__);
5 changes: 3 additions & 2 deletions etc/qa/composer-require-checker.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
"symbol-whitelist" : [
"null", "true", "false",
"static", "self", "parent",
"array", "string", "int", "float", "bool", "iterable", "callable", "void", "object", "mixed",
"Safe\\json_encode", "futurePromise", "WyriHaximus\\Constants\\ComposerAutoloader\\LOCATION"
"array", "string", "int", "float", "bool", "iterable", "callable", "void", "object", "mixed"
],
"php-core-extensions" : [
"Core",
"date",
"pcntl",
"pcre",
"Phar",
"posix",
"Reflection",
"SPL",
"standard"
Expand Down
47 changes: 46 additions & 1 deletion src/Runtime.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,30 @@
namespace ReactParallel\Runtime;

use Closure;
use parallel\Channel;
use parallel\Runtime as ParallelRuntime;
use React\EventLoop\Loop;
use ReactParallel\EventLoop\EventLoopBridge;

use function file_get_contents;
use function file_put_contents;
use function posix_getpid;
use function posix_kill;
use function React\Async\async;
use function str_replace;
use function sys_get_temp_dir;
use function tempnam;
use function uniqid;

use const WyriHaximus\Constants\ComposerAutoloader\LOCATION;

final readonly class Runtime
{
private ParallelRuntime $runtime;

/** @var Channel<Signal> */
private Channel $signalChannel;

/** @api */
public static function create(EventLoopBridge $eventLoopBridge): self
{
Expand All @@ -22,7 +37,11 @@ public static function create(EventLoopBridge $eventLoopBridge): self

public function __construct(private EventLoopBridge $eventLoopBridge, string $autoload)
{
$this->runtime = new ParallelRuntime($autoload);
$channelName = '.well-known/react-parallel/runtime/signals/' . uniqid('', true);
/** @phpstan-ignore assign.propertyType */
$this->signalChannel = Channel::make($channelName, Channel::Infinite);
$this->runtime = new ParallelRuntime($this->createBootstrapFile($autoload, $channelName));
$this->forwardSignals();
}

/**
Expand Down Expand Up @@ -72,4 +91,30 @@ public function kill(): void
{
$this->runtime->kill();
}

private function forwardSignals(): void
{
Loop::addTimer(0.0, async(function (): void {
foreach ($this->eventLoopBridge->observe($this->signalChannel) as $signal) {
posix_kill(posix_getpid(), $signal->number);
}
}));
}

private function createBootstrapFile(string $autoload, string $channelName): string
{
$file = tempnam(sys_get_temp_dir(), 'react_parallel_');
/** @phpstan-ignore wyrihaximus.reactphp.blocking.function.filePutContents */
file_put_contents(
$file,
str_replace(
['{{AUTOLOAD}}', '{{CHANNEL}}'],
[$autoload, $channelName],
/** @phpstan-ignore wyrihaximus.reactphp.blocking.function.fileGetContents, argument.type */
file_get_contents(__DIR__ . '/../etc/bootstrap/signals.php.template'),
),
);

return $file;
}
}
40 changes: 40 additions & 0 deletions src/Signal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace ReactParallel\Runtime;

use const SIGHUP;
use const SIGINT;
use const SIGQUIT;
use const SIGTERM;

/** @api */
final readonly class Signal
{
private function __construct(
public int $number,
public string $name,
) {
}

public static function sigint(): self
{
return new self(SIGINT, 'SIGINT');
}

public static function sigterm(): self
{
return new self(SIGTERM, 'SIGTERM');
}

public static function sighup(): self
{
return new self(SIGHUP, 'SIGHUP');
}

public static function sigquit(): self
{
return new self(SIGQUIT, 'SIGQUIT');
}
}
42 changes: 42 additions & 0 deletions src/SignalListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace ReactParallel\Runtime;

use parallel\Channel;

use function pcntl_async_signals;
use function pcntl_signal;

use const SIGHUP;
use const SIGINT;
use const SIGQUIT;
use const SIGTERM;

final class SignalListener
{
/**
* @param Channel<Signal> $channel
*
* @api
*/
public static function listen(Channel $channel): void
{
pcntl_async_signals(true);

$channel = $channel;
pcntl_signal(SIGINT, static function () use ($channel): void {
$channel->send(Signal::sigint());
});
pcntl_signal(SIGTERM, static function () use ($channel): void {
$channel->send(Signal::sigterm());
});
pcntl_signal(SIGHUP, static function () use ($channel): void {
$channel->send(Signal::sighup());
});
pcntl_signal(SIGQUIT, static function () use ($channel): void {
$channel->send(Signal::sigquit());
});
}
}
46 changes: 46 additions & 0 deletions tests/RuntimeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@
use TheOrville\Exceptions\LatchcombException;
use WyriHaximus\AsyncTestUtilities\AsyncTestCase;

use function extension_loaded;
use function pcntl_async_signals;
use function pcntl_signal;
use function pcntl_signal_get_handler;
use function React\Async\await;
use function sleep;
use function WyriHaximus\React\timedPromise;

use const SIGTERM;

final class RuntimeTest extends AsyncTestCase
{
#[Test]
Expand Down Expand Up @@ -95,4 +101,44 @@ public function weKilledTheThread(): void

await($promise);
}

#[Test]
public function signalChannelIsConfigured(): void
{
$runtime = Runtime::create(new EventLoopBridge());

try {
$result = $runtime->run(static fn (): bool => extension_loaded('pcntl'));

self::assertTrue($result);
} finally {
$runtime->close();
}
}

#[Test]
public function signalsAreForwardedToMainProcess(): void
{
$signalReceived = false;

pcntl_signal(SIGTERM, static function () use (&$signalReceived): void {
$signalReceived = true;
});
pcntl_async_signals(true);

$runtime = Runtime::create(new EventLoopBridge());

try {
$mainPid = \posix_getpid();
$mainPid = $mainPid;
$runtime->run(static function (int $mainPid): void {
/** @phpstan-ignore wyrihaximus.reactphp.blocking.function.posixKill */
\posix_kill($mainPid, \SIGTERM);
}, [$mainPid]);
} finally {
$runtime->close();
}

self::assertTrue($signalReceived);
}
}
72 changes: 72 additions & 0 deletions tests/SignalListenerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace ReactParallel\Tests\Runtime;

use parallel\Channel;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use ReactParallel\Runtime\Signal;
use ReactParallel\Runtime\SignalListener;
use WyriHaximus\TestUtilities\TestCase;

use function pcntl_async_signals;
use function pcntl_signal_get_handler;
use function posix_getpid;
use function posix_kill;

use const SIGHUP;
use const SIGINT;
use const SIGQUIT;
use const SIGTERM;

final class SignalListenerTest extends TestCase
{
#[Test]
public function listenRegistersSignalHandlers(): void
{
/** @var Channel<Signal> $channel */
$channel = Channel::make('test-signals', Channel::Infinite);

SignalListener::listen($channel);

pcntl_async_signals(false);

self::assertNotFalse(/** @phpstan-ignore-line */ pcntl_signal_get_handler(SIGINT));
self::assertNotFalse(/** @phpstan-ignore-line */ pcntl_signal_get_handler(SIGTERM));
self::assertNotFalse(/** @phpstan-ignore-line */ pcntl_signal_get_handler(SIGHUP));
self::assertNotFalse(/** @phpstan-ignore-line */ pcntl_signal_get_handler(SIGQUIT));
}

Check failure on line 40 in tests/SignalListenerTest.php

View workflow job for this annotation

GitHub Actions / Continuous Integration / Continuous Integration / Run stan on PHP 8.4 (zts) with highest dependency preference

Syntax error, unexpected '}' on line 40

Check failure on line 40 in tests/SignalListenerTest.php

View workflow job for this annotation

GitHub Actions / Continuous Integration / Continuous Integration / Run stan on PHP 8.4 (zts) with lowest dependency preference

Syntax error, unexpected '}' on line 40

Check failure on line 40 in tests/SignalListenerTest.php

View workflow job for this annotation

GitHub Actions / Continuous Integration / Continuous Integration / Run stan on PHP 8.5 (zts) with locked dependency preference

Syntax error, unexpected '}' on line 40

Check failure on line 40 in tests/SignalListenerTest.php

View workflow job for this annotation

GitHub Actions / Continuous Integration / Continuous Integration / Run stan on PHP 8.4 (zts) with locked dependency preference

Syntax error, unexpected '}' on line 40

Check failure on line 40 in tests/SignalListenerTest.php

View workflow job for this annotation

GitHub Actions / Continuous Integration / Continuous Integration / Run stan on PHP 8.5 (zts) with highest dependency preference

Syntax error, unexpected '}' on line 40

Check failure on line 40 in tests/SignalListenerTest.php

View workflow job for this annotation

GitHub Actions / Continuous Integration / Continuous Integration / Run stan on PHP 8.5 (zts) with lowest dependency preference

Syntax error, unexpected '}' on line 40

/** @return array<string, array{int, string}> */
public static function signals(): iterable
{
return [
'SIGINT' => [SIGINT, 'SIGINT'],
'SIGTERM' => [SIGTERM, 'SIGTERM'],
'SIGHUP' => [SIGHUP, 'SIGHUP'],
'SIGQUIT' => [SIGQUIT, 'SIGQUIT'],
];
}

#[Test]
#[DataProvider('signals')]
public function listenSendsSignalToChannel(int $signalNumber, string $signalName): void
{
/** @var Channel<Signal> $channel */
$channel = Channel::make('test-signals-' . $signalName, Channel::Infinite);

SignalListener::listen($channel);

pcntl_async_signals(true);

posix_kill(posix_getpid(), $signalNumber);

$signal = $channel->recv();
/** @phpstan-ignore staticMethod.alreadyNarrowedType */
self::assertInstanceOf(Signal::class, $signal);
self::assertSame($signalNumber, $signal->number);
self::assertSame($signalName, $signal->name);
}
}
40 changes: 40 additions & 0 deletions tests/SignalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace ReactParallel\Tests\Runtime;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use ReactParallel\Runtime\Signal;
use WyriHaximus\TestUtilities\TestCase;

use const SIGHUP;
use const SIGINT;
use const SIGQUIT;
use const SIGTERM;

final class SignalTest extends TestCase
{
/** @return array<string, array{callable(): Signal, int, string}> */
public static function signalFactoryMethods(): iterable
{
return [
'sigint' => [Signal::class . '::sigint', SIGINT, 'SIGINT'],
'sigterm' => [Signal::class . '::sigterm', SIGTERM, 'SIGTERM'],
'sighup' => [Signal::class . '::sighup', SIGHUP, 'SIGHUP'],
'sigquit' => [Signal::class . '::sigquit', SIGQUIT, 'SIGQUIT'],
];
}

#[Test]
#[DataProvider('signalFactoryMethods')]
public function createsSignalWithCorrectNumberAndName(callable $factory, int $expectedNumber, string $expectedName): void
{
/** @var Signal $signal */
$signal = $factory();

self::assertSame($expectedNumber, $signal->number);
self::assertSame($expectedName, $signal->name);
}
}
Loading