2026-07-07 11:07:47 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
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>
2026-07-07 11:24:36 +00:00
|
|
|
namespace Blax\ReactPhpKernel\Ipc;
|
2026-07-07 11:07:47 +00:00
|
|
|
|
|
|
|
|
use React\EventLoop\LoopInterface;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Event-driven IPC using Unix socket pairs.
|
|
|
|
|
*
|
|
|
|
|
* Provides instant notification when a child process sends data, eliminating
|
|
|
|
|
* polling. Lifted verbatim (bar the namespace) from blax-software/laravel-websockets
|
|
|
|
|
* because it is already framework-agnostic — it belongs to the shared backbone.
|
|
|
|
|
*
|
|
|
|
|
* Usage:
|
|
|
|
|
* 1. Before fork: $ipc = SocketPairIpc::create($loop);
|
|
|
|
|
* 2. After fork in parent: $ipc->setupParent($onData, $onClose);
|
|
|
|
|
* 3. After fork in child: $ipc->setupChild(); $ipc->sendToParent($data);
|
|
|
|
|
*/
|
|
|
|
|
final class SocketPairIpc
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* Socket pair: [0] = parent side, [1] = child side.
|
|
|
|
|
*
|
|
|
|
|
* @var resource[]|null
|
|
|
|
|
*/
|
|
|
|
|
private ?array $sockets = null;
|
|
|
|
|
|
|
|
|
|
private ?LoopInterface $loop = null;
|
|
|
|
|
|
|
|
|
|
/** 'parent' | 'child' | null (unconfigured). */
|
|
|
|
|
private ?string $role = null;
|
|
|
|
|
|
|
|
|
|
/** Exported stream resource for ReactPHP. */
|
|
|
|
|
private $stream = null;
|
|
|
|
|
|
|
|
|
|
/** Create a new socket pair for IPC. Must be called BEFORE fork(). */
|
|
|
|
|
public static function create(LoopInterface $loop): self
|
|
|
|
|
{
|
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>
2026-07-07 11:24:36 +00:00
|
|
|
$instance = new self;
|
2026-07-07 11:07:47 +00:00
|
|
|
$instance->loop = $loop;
|
|
|
|
|
|
|
|
|
|
if (! socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets)) {
|
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>
2026-07-07 11:24:36 +00:00
|
|
|
throw new \RuntimeException('Failed to create socket pair: '.socket_strerror(socket_last_error()));
|
2026-07-07 11:07:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$instance->sockets = $sockets;
|
|
|
|
|
|
|
|
|
|
return $instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup the parent side after fork: close the child socket and read async.
|
|
|
|
|
*
|
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>
2026-07-07 11:24:36 +00:00
|
|
|
* @param callable $onData function(string $message): void — newline-delimited frames
|
2026-07-07 11:07:47 +00:00
|
|
|
* @param callable|null $onClose function(): void — child closed the channel
|
|
|
|
|
*/
|
|
|
|
|
public function setupParent(callable $onData, ?callable $onClose = null): void
|
|
|
|
|
{
|
|
|
|
|
if ($this->role !== null) {
|
|
|
|
|
throw new \LogicException('IPC already configured');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->role = 'parent';
|
|
|
|
|
|
|
|
|
|
socket_close($this->sockets[1]);
|
|
|
|
|
socket_set_nonblock($this->sockets[0]);
|
|
|
|
|
|
|
|
|
|
$this->stream = socket_export_stream($this->sockets[0]);
|
|
|
|
|
if ($this->stream === false) {
|
|
|
|
|
throw new \RuntimeException('Failed to export socket as stream');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$buffer = '';
|
|
|
|
|
|
|
|
|
|
$this->loop->addReadStream($this->stream, function ($stream) use ($onData, $onClose, &$buffer) {
|
|
|
|
|
$data = @fread($stream, 65536);
|
|
|
|
|
|
|
|
|
|
if ($data === false || $data === '') {
|
|
|
|
|
if ($buffer !== '') {
|
|
|
|
|
$onData($buffer);
|
|
|
|
|
}
|
|
|
|
|
$this->loop->removeReadStream($stream);
|
|
|
|
|
fclose($stream);
|
|
|
|
|
if ($onClose) {
|
|
|
|
|
$onClose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$buffer .= $data;
|
|
|
|
|
|
|
|
|
|
while (($pos = strpos($buffer, "\n")) !== false) {
|
|
|
|
|
$message = substr($buffer, 0, $pos);
|
|
|
|
|
$buffer = substr($buffer, $pos + 1);
|
|
|
|
|
|
|
|
|
|
if ($message !== '') {
|
|
|
|
|
$onData($message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Setup the child side after fork: close the parent socket. */
|
|
|
|
|
public function setupChild(): void
|
|
|
|
|
{
|
|
|
|
|
if ($this->role !== null) {
|
|
|
|
|
throw new \LogicException('IPC already configured');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->role = 'child';
|
|
|
|
|
|
|
|
|
|
socket_close($this->sockets[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send data from child to parent (newline-delimited; do not embed newlines).
|
|
|
|
|
* Call only from the child after setupChild().
|
|
|
|
|
*/
|
|
|
|
|
public function sendToParent(string $data): bool
|
|
|
|
|
{
|
|
|
|
|
if ($this->role !== 'child') {
|
|
|
|
|
throw new \LogicException('sendToParent can only be called from child');
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-07-07 11:24:36 +00:00
|
|
|
$message = $data."\n";
|
2026-07-07 11:07:47 +00:00
|
|
|
$written = socket_write($this->sockets[1], $message, strlen($message));
|
|
|
|
|
|
|
|
|
|
return $written === strlen($message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Close the child socket (call at the end of the child process). */
|
|
|
|
|
public function closeChild(): void
|
|
|
|
|
{
|
|
|
|
|
if ($this->role === 'child' && $this->sockets[1]) {
|
|
|
|
|
socket_close($this->sockets[1]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @return resource */
|
|
|
|
|
public function getChildSocket()
|
|
|
|
|
{
|
|
|
|
|
return $this->sockets[1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Whether this system supports socket-pair IPC. */
|
|
|
|
|
public static function isSupported(): bool
|
|
|
|
|
{
|
|
|
|
|
return function_exists('socket_create_pair')
|
|
|
|
|
&& function_exists('socket_export_stream');
|
|
|
|
|
}
|
|
|
|
|
}
|