reactphp-kernel/src/Kernel.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;
}
}