chore: finish package to Blax standard (namespace, README, tests, scaffolding)
- Rename namespace BlaxSoftware\ReactPhpKernel -> Blax\ReactPhpKernel (house standard; the BlaxSoftware\LaravelWebSockets form is grandfathered only there) - README rewritten to the Blax OSS package principles: OSS banner, title+badges, emoji feature list, quick start, what's-in-the-box, testing, star history - Add PHPUnit suite (13 tests / 19 assertions, green) + phpunit.xml.dist - Add pint.json (laravel preset, applied), composer scripts (test/lint), .gitattributes export-ignores, test.sh (nix), CHANGELOG - composer.json: Blax psr-4, PHP ^8.1-^8.4, dev deps rel learn-atc #1055 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
96b3de6a53
commit
3c710b5132
|
|
@ -0,0 +1,7 @@
|
||||||
|
/.gitattributes export-ignore
|
||||||
|
/.gitignore export-ignore
|
||||||
|
/phpunit.xml.dist export-ignore
|
||||||
|
/pint.json export-ignore
|
||||||
|
/test.sh export-ignore
|
||||||
|
/tests export-ignore
|
||||||
|
/.github export-ignore
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to `blax-software/reactphp-kernel` are documented here. This
|
||||||
|
project adheres to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
||||||
|
Blax Software's backward-compatibility guarantee (every push to `master` is a
|
||||||
|
potentially-shipped, backward-compatible release).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `Kernel` — owns the ReactPHP loop, registers `Contracts\Server`s, installs
|
||||||
|
signal handlers, `run()` and graceful `stop()`.
|
||||||
|
- `Contracts\Server` — an attachable protocol server (`boot(LoopInterface)` +
|
||||||
|
`shutdown()`).
|
||||||
|
- `Server\SocketServerFactory` — plain-TCP or TLS listening socket, no framework
|
||||||
|
coupling.
|
||||||
|
- `Ipc\SocketPairIpc` — event-driven parent/child IPC over a Unix socket pair
|
||||||
|
(lifted from `blax-software/laravel-websockets`; it was already
|
||||||
|
framework-agnostic).
|
||||||
|
- `Process\SignalHandler` — SIGINT/SIGTERM → graceful shutdown.
|
||||||
|
- `Process\ChildReaper` — reaps exited forked children (SIGCHLD + periodic
|
||||||
|
backstop); the reusable form of laravel-websockets #982.
|
||||||
136
README.md
136
README.md
|
|
@ -1,63 +1,59 @@
|
||||||
# blax-software/reactphp-kernel
|
[](https://github.com/blax-software)
|
||||||
|
|
||||||
A small, protocol-agnostic **ReactPHP backbone**: one long-lived process, one
|
# ReactPHP Kernel
|
||||||
event loop, one IPC primitive, and one signal/graceful-shutdown story that many
|
|
||||||
protocol servers attach to.
|
|
||||||
|
|
||||||
It is the shared foundation extracted from
|
[](https://php.net)
|
||||||
[`blax-software/laravel-websockets`](https://git.blax.at/blax-software/laravel-websockets)
|
[](https://reactphp.org)
|
||||||
so that WebSockets, **WebRTC signaling/media**
|
[](#testing)
|
||||||
([`blax-software/laravel-webrtc`](https://git.blax.at/blax-software/laravel-webrtc)),
|
[](#testing)
|
||||||
and future protocols (SIP, ...) do not each re-implement the plumbing.
|
[](LICENSE)
|
||||||
|
|
||||||
```
|
A tiny, protocol-agnostic ReactPHP backbone — one long-lived process, one event loop, one IPC primitive, and one graceful-shutdown story that many protocol servers attach to.
|
||||||
blax-software/reactphp-kernel <- this package (the backbone)
|
|
||||||
├─ laravel-websockets <- WS on the kernel
|
## Features
|
||||||
└─ laravel-webrtc <- WebRTC signaling on the kernel
|
|
||||||
└─ Rust/str0m media core <- via ext-php-rs
|
- 🧩 **One loop, many protocols** — WebSockets, WebRTC signaling, SIP and more share a single `Kernel` instead of each spinning up its own runtime
|
||||||
|
- 🔌 **Tiny `Server` contract** — implement `boot(LoopInterface)` + `shutdown()` and you're on the shared loop
|
||||||
|
- 🧵 **Socket-pair IPC** — event-driven parent↔child messaging with zero polling
|
||||||
|
- 🛎️ **Graceful shutdown** — SIGINT/SIGTERM tear every server down cleanly before the loop stops
|
||||||
|
- 🧹 **Child reaping built in** — no zombie processes from forked workers (SIGCHLD + periodic backstop)
|
||||||
|
- 🔐 **Plain or TLS listeners** — one factory, no framework coupling
|
||||||
|
- 🪶 **Dependency-light** — just `react/event-loop`, `react/socket`, `react/stream`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require blax-software/reactphp-kernel
|
||||||
```
|
```
|
||||||
|
|
||||||
## What's in the box
|
Requires PHP 8.1+, plus `ext-pcntl` (signals + child reaping) and `ext-sockets` (IPC) for the full feature set.
|
||||||
|
|
||||||
| Piece | Responsibility |
|
## Quick Start
|
||||||
|---|---|
|
|
||||||
| `Kernel` | Owns the loop; registers `Server`s; installs signals; `run()` / graceful `stop()`. |
|
|
||||||
| `Contracts\Server` | An attachable protocol server: `boot(LoopInterface)` (non-blocking) + `shutdown()`. |
|
|
||||||
| `Server\SocketServerFactory` | Plain-TCP or TLS listening socket (no framework coupling). |
|
|
||||||
| `Ipc\SocketPairIpc` | Event-driven parent/child IPC over a Unix socket pair (no polling). |
|
|
||||||
| `Process\SignalHandler` | SIGINT/SIGTERM → graceful shutdown (no-op without ext-pcntl). |
|
|
||||||
| `Process\ChildReaper` | Reaps exited forked children (SIGCHLD + periodic backstop) — the reusable form of laravel-websockets #982. |
|
|
||||||
|
|
||||||
## Usage
|
Implement a `Server`, register it, `run()`:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use BlaxSoftware\ReactPhpKernel\Kernel;
|
use Blax\ReactPhpKernel\Kernel;
|
||||||
|
use Blax\ReactPhpKernel\Contracts\Server;
|
||||||
(new Kernel())
|
use Blax\ReactPhpKernel\Server\SocketServerFactory;
|
||||||
->reapChildren() // optional: auto-reap forked children
|
|
||||||
->register($webSocketServer) // any Contracts\Server
|
|
||||||
->register($webRtcSignaling)
|
|
||||||
->onBoot(fn (Kernel $k) => /* warm caches, announce ready, ... */ null)
|
|
||||||
->run(); // boots servers, installs signals, runs the loop (blocks)
|
|
||||||
```
|
|
||||||
|
|
||||||
Implement `Contracts\Server` to plug in a protocol:
|
|
||||||
|
|
||||||
```php
|
|
||||||
use BlaxSoftware\ReactPhpKernel\Contracts\Server;
|
|
||||||
use BlaxSoftware\ReactPhpKernel\Server\SocketServerFactory;
|
|
||||||
use React\EventLoop\LoopInterface;
|
use React\EventLoop\LoopInterface;
|
||||||
|
use React\Socket\ConnectionInterface;
|
||||||
|
|
||||||
final class MyServer implements Server
|
final class EchoServer implements Server
|
||||||
{
|
{
|
||||||
private $socket = null;
|
private $socket = null;
|
||||||
|
|
||||||
public function name(): string { return 'my-server'; }
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'echo';
|
||||||
|
}
|
||||||
|
|
||||||
public function boot(LoopInterface $loop): void
|
public function boot(LoopInterface $loop): void
|
||||||
{
|
{
|
||||||
$this->socket = SocketServerFactory::create('0.0.0.0:9000', $loop);
|
$this->socket = SocketServerFactory::create('0.0.0.0:9001', $loop);
|
||||||
$this->socket->on('connection', function ($conn) { /* ... */ });
|
$this->socket->on('connection', function (ConnectionInterface $conn) {
|
||||||
|
$conn->on('data', fn ($data) => $conn->write($data)); // echo it back
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function shutdown(): void
|
public function shutdown(): void
|
||||||
|
|
@ -65,15 +61,57 @@ final class MyServer implements Server
|
||||||
$this->socket?->close();
|
$this->socket?->close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(new Kernel())
|
||||||
|
->reapChildren() // auto-reap forked children
|
||||||
|
->register(new EchoServer())
|
||||||
|
->onBoot(fn (Kernel $k) => fwrite(STDERR, "ready\n"))
|
||||||
|
->run(); // boots servers, installs signals, runs the loop (blocks)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Requirements
|
`Ctrl-C` (SIGINT) or `SIGTERM` triggers a graceful `stop()`: every registered server's `shutdown()` runs, then the loop stops.
|
||||||
|
|
||||||
- PHP >= 8.1
|
## What's in the box
|
||||||
- `react/event-loop`, `react/socket`, `react/stream`
|
|
||||||
- `ext-pcntl` (suggested) for signal handling + child reaping
|
| Class | Responsibility |
|
||||||
- `ext-sockets` (suggested) for `SocketPairIpc`
|
|---|---|
|
||||||
|
| `Kernel` | Owns the loop; `register()` servers; installs signals; `run()` / graceful `stop()`. |
|
||||||
|
| `Contracts\Server` | An attachable protocol server: `boot(LoopInterface)` (non-blocking) + `shutdown()`. |
|
||||||
|
| `Server\SocketServerFactory` | Plain-TCP or TLS listening socket. |
|
||||||
|
| `Ipc\SocketPairIpc` | Event-driven parent/child IPC over a Unix socket pair. |
|
||||||
|
| `Process\SignalHandler` | SIGINT/SIGTERM → graceful shutdown. |
|
||||||
|
| `Process\ChildReaper` | Reaps exited forked children (SIGCHLD + periodic backstop). |
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
It's the shared foundation extracted from [`blax-software/laravel-websockets`](https://github.com/blax-software/laravel-websockets) so that WebSockets and [`blax-software/laravel-webrtc`](https://github.com/blax-software/laravel-webrtc) (WebRTC signaling + a Rust/str0m media core) don't each re-implement the loop, IPC, signals and supervision. A new protocol gets the plumbing for free.
|
||||||
|
|
||||||
|
```
|
||||||
|
reactphp-kernel this package — the backbone
|
||||||
|
├─ laravel-websockets WS on the kernel
|
||||||
|
└─ laravel-webrtc WebRTC signaling on the kernel
|
||||||
|
└─ Rust/str0m core via ext-php-rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
The suite runs against a real ReactPHP loop, with no external services.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT © Blax Software
|
MIT. See [LICENSE](LICENSE).
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
<a href="https://www.star-history.com/?repos=blax-software%2Freactphp-kernel&type=date&legend=top-left">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=blax-software/reactphp-kernel&type=date&theme=dark&legend=top-left" />
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=blax-software/reactphp-kernel&type=date&legend=top-left" />
|
||||||
|
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=blax-software/reactphp-kernel&type=date&legend=top-left" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,14 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.1",
|
"php": "^8.1|^8.2|^8.3|^8.4",
|
||||||
"react/event-loop": "^1.5",
|
"react/event-loop": "^1.5",
|
||||||
"react/socket": "^1.15",
|
"react/socket": "^1.15",
|
||||||
"react/stream": "^1.3"
|
"react/stream": "^1.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^10.5"
|
"phpunit/phpunit": "^10.5|^11.0",
|
||||||
|
"laravel/pint": "^1.13"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"ext-pcntl": "Signal handling (graceful shutdown) and reaping of forked children.",
|
"ext-pcntl": "Signal handling (graceful shutdown) and reaping of forked children.",
|
||||||
|
|
@ -25,14 +26,20 @@
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"BlaxSoftware\\ReactPhpKernel\\": "src/"
|
"Blax\\ReactPhpKernel\\": "src/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"BlaxSoftware\\ReactPhpKernel\\Tests\\": "tests/"
|
"Blax\\ReactPhpKernel\\Tests\\": "tests/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "vendor/bin/phpunit",
|
||||||
|
"test-coverage": "vendor/bin/phpunit --coverage-html coverage",
|
||||||
|
"lint": "vendor/bin/pint",
|
||||||
|
"lint-test": "vendor/bin/pint --test"
|
||||||
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sort-packages": true
|
"sort-packages": true
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
processIsolation="false"
|
||||||
|
stopOnFailure="false"
|
||||||
|
cacheDirectory=".phpunit.cache"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="ReactPHP Kernel Test Suite">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">./src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace BlaxSoftware\ReactPhpKernel\Contracts;
|
namespace Blax\ReactPhpKernel\Contracts;
|
||||||
|
|
||||||
use React\EventLoop\LoopInterface;
|
use React\EventLoop\LoopInterface;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace BlaxSoftware\ReactPhpKernel\Ipc;
|
namespace Blax\ReactPhpKernel\Ipc;
|
||||||
|
|
||||||
use React\EventLoop\LoopInterface;
|
use React\EventLoop\LoopInterface;
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ final class SocketPairIpc
|
||||||
/** Create a new socket pair for IPC. Must be called BEFORE fork(). */
|
/** Create a new socket pair for IPC. Must be called BEFORE fork(). */
|
||||||
public static function create(LoopInterface $loop): self
|
public static function create(LoopInterface $loop): self
|
||||||
{
|
{
|
||||||
$instance = new self();
|
$instance = new self;
|
||||||
$instance->loop = $loop;
|
$instance->loop = $loop;
|
||||||
|
|
||||||
if (! socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets)) {
|
if (! socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets)) {
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace BlaxSoftware\ReactPhpKernel;
|
namespace Blax\ReactPhpKernel;
|
||||||
|
|
||||||
use BlaxSoftware\ReactPhpKernel\Contracts\Server;
|
use Blax\ReactPhpKernel\Contracts\Server;
|
||||||
use BlaxSoftware\ReactPhpKernel\Process\ChildReaper;
|
use Blax\ReactPhpKernel\Process\ChildReaper;
|
||||||
use BlaxSoftware\ReactPhpKernel\Process\SignalHandler;
|
use Blax\ReactPhpKernel\Process\SignalHandler;
|
||||||
use React\EventLoop\Loop;
|
use React\EventLoop\Loop;
|
||||||
use React\EventLoop\LoopInterface;
|
use React\EventLoop\LoopInterface;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace BlaxSoftware\ReactPhpKernel\Process;
|
namespace Blax\ReactPhpKernel\Process;
|
||||||
|
|
||||||
use React\EventLoop\LoopInterface;
|
use React\EventLoop\LoopInterface;
|
||||||
use React\EventLoop\TimerInterface;
|
use React\EventLoop\TimerInterface;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace BlaxSoftware\ReactPhpKernel\Process;
|
namespace Blax\ReactPhpKernel\Process;
|
||||||
|
|
||||||
use React\EventLoop\LoopInterface;
|
use React\EventLoop\LoopInterface;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace BlaxSoftware\ReactPhpKernel\Server;
|
namespace Blax\ReactPhpKernel\Server;
|
||||||
|
|
||||||
use React\EventLoop\LoopInterface;
|
use React\EventLoop\LoopInterface;
|
||||||
use React\Socket\SocketServer;
|
use React\Socket\SocketServer;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env nix-shell
|
||||||
|
#!nix-shell -i bash -p php83 php83Extensions.sockets php83Extensions.pcntl php83Extensions.mbstring php83Extensions.xml php83Extensions.xmlwriter php83Extensions.tokenizer
|
||||||
|
|
||||||
|
# Test script for NixOS — runs PHPUnit with the extensions the kernel needs
|
||||||
|
# (ext-sockets for SocketPairIpc, ext-pcntl for signals/child reaping).
|
||||||
|
|
||||||
|
echo "Running ReactPHP Kernel tests..."
|
||||||
|
echo "PHP version: $(php --version | head -n 1)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
vendor/bin/phpunit "$@"
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Blax\ReactPhpKernel\Tests;
|
||||||
|
|
||||||
|
use Blax\ReactPhpKernel\Process\ChildReaper;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use React\EventLoop\StreamSelectLoop;
|
||||||
|
|
||||||
|
final class ChildReaperTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_reap_all_returns_zero_when_there_are_no_children(): void
|
||||||
|
{
|
||||||
|
if (! function_exists('pcntl_waitpid')) {
|
||||||
|
$this->markTestSkipped('ext-pcntl not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
$reaper = new ChildReaper(new StreamSelectLoop);
|
||||||
|
|
||||||
|
$this->assertSame(0, $reaper->reapAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_install_and_uninstall_do_not_throw(): void
|
||||||
|
{
|
||||||
|
$loop = new StreamSelectLoop;
|
||||||
|
$reaper = new ChildReaper($loop);
|
||||||
|
|
||||||
|
$reaper->install(0.1);
|
||||||
|
$reaper->uninstall();
|
||||||
|
|
||||||
|
// Reaching here without an exception is the assertion.
|
||||||
|
$this->assertTrue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_reap_all_is_safe_to_call_repeatedly(): void
|
||||||
|
{
|
||||||
|
if (! function_exists('pcntl_waitpid')) {
|
||||||
|
$this->markTestSkipped('ext-pcntl not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
$reaper = new ChildReaper(new StreamSelectLoop);
|
||||||
|
|
||||||
|
$this->assertSame(0, $reaper->reapAll());
|
||||||
|
$this->assertSame(0, $reaper->reapAll());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Blax\ReactPhpKernel\Tests\Fixtures;
|
||||||
|
|
||||||
|
use Blax\ReactPhpKernel\Contracts\Server;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A test-double protocol server that records whether it was booted / shut down
|
||||||
|
* and lets a test hook onto the boot moment (e.g. to schedule the kernel stop).
|
||||||
|
*/
|
||||||
|
final class RecordingServer implements Server
|
||||||
|
{
|
||||||
|
public bool $booted = false;
|
||||||
|
|
||||||
|
public bool $wasShutDown = false;
|
||||||
|
|
||||||
|
/** @var callable|null function(LoopInterface): void */
|
||||||
|
public $onBoot = null;
|
||||||
|
|
||||||
|
public function __construct(private string $id = 'recording') {}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(LoopInterface $loop): void
|
||||||
|
{
|
||||||
|
$this->booted = true;
|
||||||
|
|
||||||
|
if ($this->onBoot !== null) {
|
||||||
|
($this->onBoot)($loop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shutdown(): void
|
||||||
|
{
|
||||||
|
$this->wasShutDown = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Blax\ReactPhpKernel\Tests;
|
||||||
|
|
||||||
|
use Blax\ReactPhpKernel\Contracts\Server;
|
||||||
|
use Blax\ReactPhpKernel\Kernel;
|
||||||
|
use Blax\ReactPhpKernel\Tests\Fixtures\RecordingServer;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
use React\EventLoop\StreamSelectLoop;
|
||||||
|
|
||||||
|
final class KernelTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_uses_the_injected_loop(): void
|
||||||
|
{
|
||||||
|
$loop = new StreamSelectLoop;
|
||||||
|
|
||||||
|
$this->assertSame($loop, (new Kernel($loop))->loop());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_boots_and_shuts_down_registered_servers_with_hooks(): void
|
||||||
|
{
|
||||||
|
$loop = new StreamSelectLoop;
|
||||||
|
$kernel = new Kernel($loop);
|
||||||
|
|
||||||
|
$server = new RecordingServer;
|
||||||
|
// Stop the kernel on the next tick so run() returns instead of blocking.
|
||||||
|
$server->onBoot = fn (LoopInterface $l) => $l->futureTick(fn () => $kernel->stop());
|
||||||
|
|
||||||
|
$bootHook = false;
|
||||||
|
$shutdownHook = false;
|
||||||
|
|
||||||
|
$kernel
|
||||||
|
->register($server)
|
||||||
|
->onBoot(function () use (&$bootHook) {
|
||||||
|
$bootHook = true;
|
||||||
|
})
|
||||||
|
->onShutdown(function () use (&$shutdownHook) {
|
||||||
|
$shutdownHook = true;
|
||||||
|
})
|
||||||
|
->run();
|
||||||
|
|
||||||
|
$this->assertTrue($server->booted, 'server should have been booted');
|
||||||
|
$this->assertTrue($server->wasShutDown, 'server should have been shut down');
|
||||||
|
$this->assertTrue($bootHook, 'onBoot hook should have fired');
|
||||||
|
$this->assertTrue($shutdownHook, 'onShutdown hook should have fired');
|
||||||
|
$this->assertFalse($kernel->isRunning());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_stop_is_idempotent_and_safe_before_run(): void
|
||||||
|
{
|
||||||
|
$kernel = new Kernel(new StreamSelectLoop);
|
||||||
|
|
||||||
|
$kernel->stop(); // never ran — must be a harmless no-op
|
||||||
|
$kernel->stop();
|
||||||
|
|
||||||
|
$this->assertFalse($kernel->isRunning());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_a_failing_server_shutdown_does_not_block_the_others(): void
|
||||||
|
{
|
||||||
|
$loop = new StreamSelectLoop;
|
||||||
|
$kernel = new Kernel($loop);
|
||||||
|
|
||||||
|
$throwing = new class implements Server
|
||||||
|
{
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'throwing';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(LoopInterface $loop): void {}
|
||||||
|
|
||||||
|
public function shutdown(): void
|
||||||
|
{
|
||||||
|
throw new \RuntimeException('teardown failed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$good = new RecordingServer;
|
||||||
|
$good->onBoot = fn (LoopInterface $l) => $l->futureTick(fn () => $kernel->stop());
|
||||||
|
|
||||||
|
$kernel->register($throwing)->register($good)->run();
|
||||||
|
|
||||||
|
$this->assertTrue($good->wasShutDown, 'a throwing teardown must not stop the others');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Blax\ReactPhpKernel\Tests;
|
||||||
|
|
||||||
|
use Blax\ReactPhpKernel\Ipc\SocketPairIpc;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use React\EventLoop\StreamSelectLoop;
|
||||||
|
|
||||||
|
final class SocketPairIpcTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
if (! SocketPairIpc::isSupported()) {
|
||||||
|
$this->markTestSkipped('ext-sockets (socket_create_pair / socket_export_stream) not available');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_reports_support_on_this_platform(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(SocketPairIpc::isSupported());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_returns_an_unconfigured_instance(): void
|
||||||
|
{
|
||||||
|
$ipc = SocketPairIpc::create(new StreamSelectLoop);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(SocketPairIpc::class, $ipc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_double_configuration_is_rejected(): void
|
||||||
|
{
|
||||||
|
$ipc = SocketPairIpc::create(new StreamSelectLoop);
|
||||||
|
$ipc->setupChild();
|
||||||
|
|
||||||
|
$this->expectException(\LogicException::class);
|
||||||
|
$ipc->setupChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_send_from_a_non_child_is_rejected(): void
|
||||||
|
{
|
||||||
|
$ipc = SocketPairIpc::create(new StreamSelectLoop);
|
||||||
|
|
||||||
|
// Not configured as child -> sendToParent must refuse.
|
||||||
|
$this->expectException(\LogicException::class);
|
||||||
|
$ipc->sendToParent('nope');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Blax\ReactPhpKernel\Tests;
|
||||||
|
|
||||||
|
use Blax\ReactPhpKernel\Server\SocketServerFactory;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use React\EventLoop\StreamSelectLoop;
|
||||||
|
use React\Socket\SocketServer;
|
||||||
|
|
||||||
|
final class SocketServerFactoryTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_creates_a_plain_tcp_listener(): void
|
||||||
|
{
|
||||||
|
$server = SocketServerFactory::create('127.0.0.1:0', new StreamSelectLoop);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(SocketServer::class, $server);
|
||||||
|
$this->assertStringStartsWith('tcp://127.0.0.1:', (string) $server->getAddress());
|
||||||
|
|
||||||
|
$server->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_binds_an_ephemeral_port_that_can_be_read_back(): void
|
||||||
|
{
|
||||||
|
$server = SocketServerFactory::create('127.0.0.1:0', new StreamSelectLoop);
|
||||||
|
|
||||||
|
$address = (string) $server->getAddress();
|
||||||
|
$port = (int) parse_url($address, PHP_URL_PORT);
|
||||||
|
|
||||||
|
$this->assertGreaterThan(0, $port, 'an ephemeral port should have been assigned');
|
||||||
|
|
||||||
|
$server->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue