148 lines
3.7 KiB
PHP
148 lines
3.7 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace BlaxSoftware\ReactPhpKernel;
|
||
|
|
|
||
|
|
use BlaxSoftware\ReactPhpKernel\Contracts\Server;
|
||
|
|
use BlaxSoftware\ReactPhpKernel\Process\ChildReaper;
|
||
|
|
use BlaxSoftware\ReactPhpKernel\Process\SignalHandler;
|
||
|
|
use React\EventLoop\Loop;
|
||
|
|
use React\EventLoop\LoopInterface;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The shared ReactPHP backbone.
|
||
|
|
*
|
||
|
|
* Extracted from blax-software/laravel-websockets so that WebSockets, WebRTC
|
||
|
|
* signaling, and future protocols all attach to ONE long-lived process, one
|
||
|
|
* event loop, one IPC primitive and one supervision/signal story instead of each
|
||
|
|
* re-implementing the plumbing.
|
||
|
|
*
|
||
|
|
* Usage:
|
||
|
|
* (new Kernel())
|
||
|
|
* ->reapChildren() // optional: auto-reap forked children
|
||
|
|
* ->register($webSocketServer) // any Contracts\Server
|
||
|
|
* ->register($webRtcSignaling)
|
||
|
|
* ->onBoot(fn (Kernel $k) => ...)
|
||
|
|
* ->run(); // boots servers, installs signals, runs the loop
|
||
|
|
*/
|
||
|
|
final class Kernel
|
||
|
|
{
|
||
|
|
private LoopInterface $loop;
|
||
|
|
|
||
|
|
/** @var Server[] */
|
||
|
|
private array $servers = [];
|
||
|
|
|
||
|
|
/** @var array<callable> */
|
||
|
|
private array $onBoot = [];
|
||
|
|
|
||
|
|
/** @var array<callable> */
|
||
|
|
private array $onShutdown = [];
|
||
|
|
|
||
|
|
private bool $running = false;
|
||
|
|
|
||
|
|
private ?ChildReaper $reaper = null;
|
||
|
|
|
||
|
|
public function __construct(?LoopInterface $loop = null)
|
||
|
|
{
|
||
|
|
// Default to the global loop so co-located libraries share one reactor.
|
||
|
|
$this->loop = $loop ?? Loop::get();
|
||
|
|
}
|
||
|
|
|
||
|
|
public function loop(): LoopInterface
|
||
|
|
{
|
||
|
|
return $this->loop;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Attach a protocol server (WS, WebRTC, ...) to the shared loop. */
|
||
|
|
public function register(Server $server): self
|
||
|
|
{
|
||
|
|
$this->servers[] = $server;
|
||
|
|
|
||
|
|
return $this;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function onBoot(callable $callback): self
|
||
|
|
{
|
||
|
|
$this->onBoot[] = $callback;
|
||
|
|
|
||
|
|
return $this;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function onShutdown(callable $callback): self
|
||
|
|
{
|
||
|
|
$this->onShutdown[] = $callback;
|
||
|
|
|
||
|
|
return $this;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Automatically reap exited forked children so they never accumulate as
|
||
|
|
* zombies (the concern behind laravel-websockets #982). Opt-in because not
|
||
|
|
* every deployment forks.
|
||
|
|
*/
|
||
|
|
public function reapChildren(float $interval = 1.0): self
|
||
|
|
{
|
||
|
|
$this->reaper = new ChildReaper($this->loop);
|
||
|
|
$this->reaper->install($interval);
|
||
|
|
|
||
|
|
return $this;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Boot every registered server, install signal handlers, run the loop (blocks). */
|
||
|
|
public function run(): void
|
||
|
|
{
|
||
|
|
if ($this->running) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
$this->running = true;
|
||
|
|
|
||
|
|
foreach ($this->servers as $server) {
|
||
|
|
$server->boot($this->loop);
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($this->onBoot as $callback) {
|
||
|
|
$callback($this);
|
||
|
|
}
|
||
|
|
|
||
|
|
SignalHandler::install($this->loop, fn () => $this->stop());
|
||
|
|
|
||
|
|
$this->loop->run();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Graceful shutdown: tear down servers, run shutdown hooks, stop the loop. */
|
||
|
|
public function stop(): void
|
||
|
|
{
|
||
|
|
if (! $this->running) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($this->servers as $server) {
|
||
|
|
try {
|
||
|
|
$server->shutdown();
|
||
|
|
} catch (\Throwable) {
|
||
|
|
// A failing teardown must not block the others.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($this->onShutdown as $callback) {
|
||
|
|
try {
|
||
|
|
$callback($this);
|
||
|
|
} catch (\Throwable) {
|
||
|
|
// ignore
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->reaper?->reapAll();
|
||
|
|
$this->reaper?->uninstall();
|
||
|
|
|
||
|
|
$this->running = false;
|
||
|
|
$this->loop->stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
public function isRunning(): bool
|
||
|
|
{
|
||
|
|
return $this->running;
|
||
|
|
}
|
||
|
|
}
|