Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/AmphpIpcPeer.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
use Amp\ByteStream\ReadableResourceStream;
use Amp\ByteStream\WritableResourceStream;
use Amp\DeferredFuture;
use StreamIpc\Transport\AmpByteStreamMessageTransport;
use Revolt\EventLoop;
use StreamIpc\Transport\AmpByteStreamMessageTransport;
use function Amp\Future\awaitFirst;
use function is_resource;

Expand Down
1 change: 1 addition & 0 deletions src/Envelope/ResponsePromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
final class ResponsePromise
{
public const DEFAULT_TIMEOUT = 30.0;

/** @var ?Message The response message, null if not yet received. */
private ?Message $response = null;

Expand Down
4 changes: 2 additions & 2 deletions src/IpcSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use StreamIpc\Envelope\RequestEnvelope;
use StreamIpc\Envelope\ResponseEnvelope;
use StreamIpc\Envelope\ResponsePromise;
use StreamIpc\Message\LogMessage;
use StreamIpc\Message\ErrorMessage;
use StreamIpc\Message\Message;
use StreamIpc\Transport\MessageTransport;
use StreamIpc\Transport\StreamClosedException;
Expand Down Expand Up @@ -91,7 +91,7 @@ public function dispatch(Message $envelope): void
} catch (Throwable $e) {
try {
$this->transport->send(
new ResponseEnvelope($envelope->id, new LogMessage('Error in dispatch: ' . $e->getMessage(), 'error'))
new ResponseEnvelope($envelope->id, new ErrorMessage('Error in dispatch', $e))
);
} catch (StreamClosedException) {
throw $e;
Expand Down
26 changes: 26 additions & 0 deletions src/Message/ErrorMessage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace StreamIpc\Message;

use Throwable;

final readonly class ErrorMessage implements Message
{
private string $errorMessage;

public function __construct(string $errorMessage, ?Throwable $exception = null)
{
if ($exception !== null) {
$errorMessage .= PHP_EOL . get_class($exception) . ': ' . $exception->getMessage() . PHP_EOL . $exception->getTraceAsString();
}

$this->errorMessage = trim($errorMessage);
}

public function toString(): string
{
return $this->errorMessage;
}
}
2 changes: 2 additions & 0 deletions src/Message/LogMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
final readonly class LogMessage implements Message
{
public const LEVEL_JUNK = 'junk';

/**
* Constructs a new LogMessage.
*
Expand Down
3 changes: 2 additions & 1 deletion src/NativeIpcPeer.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

namespace StreamIpc;

use RuntimeException;
use StreamIpc\Transport\MessageTransport;
use StreamIpc\Transport\NativeMessageTransport;
use RuntimeException;

/**
* IPC peer implementation that works with standard PHP stream resources.
Expand Down Expand Up @@ -142,6 +142,7 @@ public function tick(?float $timeout = null): void
$usec = (int)(($timeout - $sec) * 1e6);
}

$writes = $except = null;
if (@stream_select($reads, $writes, $except, $sec, $usec) <= 0) {
// no streams ready or error occurred
return;
Expand Down
16 changes: 8 additions & 8 deletions src/Serialization/JsonMessageSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
namespace StreamIpc\Serialization;

use JsonException;
use StreamIpc\Message\LogMessage;
use StreamIpc\Message\Message;
use ReflectionClass;
use StreamIpc\Message\ErrorMessage;
use StreamIpc\Message\Message;
use Throwable;
use function class_exists;

Expand Down Expand Up @@ -100,17 +100,17 @@ public function deserialize(string $data): Message
{
try {
$decoded = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
} catch (Throwable $e) {
return new LogMessage($data, 'error');
} catch (JsonException $e) {
return new ErrorMessage('json_decode failed:' . $data, $e);
}

if (!is_array($decoded) || !isset($decoded['__class'])) {
return new LogMessage($data, 'error');
return new ErrorMessage('Invalid data to unserialize: ' . $data);
}

$class = $decoded['__class'];
if (!class_exists($class)) {
return new LogMessage("Unknown class: {$class}", 'error');
return new ErrorMessage("Unknown class: {$class}");
}

try {
Expand All @@ -127,11 +127,11 @@ public function deserialize(string $data): Message
$prop->setValue($inst, $this->fromArray($value));
}
} catch (Throwable $e) {
return new LogMessage($data, 'error');
return new ErrorMessage('Failed to unserialize data:' . $data, $e);
}

if (!$inst instanceof Message) {
return new LogMessage("Decoded object is not a Message: {$data}", 'error');
return new ErrorMessage("Decoded object is not a Message: {$data}");
}

return $inst;
Expand Down
29 changes: 17 additions & 12 deletions src/Transport/FrameCodec.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,27 @@
namespace StreamIpc\Transport;

use RuntimeException;
use StreamIpc\Message\ErrorMessage;
use StreamIpc\Message\LogMessage;
use StreamIpc\Message\Message;
use StreamIpc\Serialization\MessageSerializer;
use Throwable;

final class FrameCodec
{
public const MAGIC = "\xF3\x4A\x9D\xE2";
public const MAGIC = "\xF3\x4A\x9D\xE2";
private const MAGIC_LEN = 4;
private const LEN_LEN = 4;
private const LEN_LEN = 4;

private string $buffer = '';
private int $scanPos = 0;

private int $scanPos = 0;

public function __construct(
private readonly MessageSerializer $serializer,
private readonly ?int $maxFrame = null
) {}
) {
}

public function pack(Message $message): string
{
Expand All @@ -48,7 +51,7 @@ public function feed(string $chunk): array
$junkLen = strlen($this->buffer) - $overlap;

if ($junkLen > 0) {
$messages[] = new LogMessage(substr($this->buffer, 0, $junkLen), 'error');
$messages[] = new LogMessage(substr($this->buffer, 0, $junkLen), LogMessage::LEVEL_JUNK);
$this->buffer = substr($this->buffer, $junkLen); // keep only overlap
}
$this->scanPos = 0;
Expand All @@ -57,7 +60,7 @@ public function feed(string $chunk): array

// ── 2. junk before header ──────────────────────────────────────────────
if ($pos > 0) {
$messages[] = new LogMessage(substr($this->buffer, 0, $pos), 'error');
$messages[] = new LogMessage(substr($this->buffer, 0, $pos), LogMessage::LEVEL_JUNK);
$this->buffer = substr($this->buffer, $pos);
$this->scanPos = 0;
}
Expand All @@ -69,10 +72,10 @@ public function feed(string $chunk): array

// ── 4. parse 32-bit BE length fast ─────────────────────────────────────
$lenOffset = self::MAGIC_LEN;
$length = (ord($this->buffer[$lenOffset ]) << 24)
$length = (ord($this->buffer[$lenOffset ]) << 24)
| (ord($this->buffer[$lenOffset + 1]) << 16)
| (ord($this->buffer[$lenOffset + 2]) << 8)
| ord($this->buffer[$lenOffset + 3]);
| (ord($this->buffer[$lenOffset + 2]) << 8)
| ord($this->buffer[$lenOffset + 3]);

if ($this->maxFrame !== null && $length > $this->maxFrame) {
throw new RuntimeException("Frame length $length exceeds max {$this->maxFrame}");
Expand All @@ -84,16 +87,18 @@ public function feed(string $chunk): array
}

// ── 5. extract payload & consume buffer ────────────────────────────────
$payload = substr($this->buffer,
$payload = substr(
$this->buffer,
self::MAGIC_LEN + self::LEN_LEN,
$length);
$length
);
$this->buffer = substr($this->buffer, $frameSize);
$this->scanPos = 0;

try {
$messages[] = $this->serializer->deserialize($payload);
} catch (Throwable $e) {
$messages[] = new LogMessage($payload, 'error');
$messages[] = new ErrorMessage('Failed to deserialize message payload', $e);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/Transport/NativeFrameReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ final class NativeFrameReader
* @param $serializer MessageSerializer used for incoming frames.
* @param $maxFrame ?int Maximum allowed size for a single message frame.
*/

/**
* @param resource $stream
*/
Expand Down
3 changes: 2 additions & 1 deletion tests/Unit/FrameCodecTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use StreamIpc\Serialization\NativeMessageSerializer;
use StreamIpc\Tests\Fixtures\SimpleMessage;
use StreamIpc\Message\LogMessage;
use StreamIpc\Message\ErrorMessage;
use RuntimeException;
use StreamIpc\Message\Message;
use Throwable;
Expand Down Expand Up @@ -183,7 +184,7 @@ public function deserialize(string $data): Message
$msgs = $codec->feed($frame);

self::assertCount(1, $msgs);
self::assertInstanceOf(LogMessage::class, $msgs[0]);
self::assertInstanceOf(ErrorMessage::class, $msgs[0]);
}

/** 8. Feeding an empty string must be a no-op ******************************/
Expand Down
6 changes: 4 additions & 2 deletions tests/Unit/IpcSessionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use StreamIpc\Tests\Fixtures\FakeTransport;
use StreamIpc\Tests\Fixtures\SimpleMessage;
use StreamIpc\Message\LogMessage;
use StreamIpc\Message\ErrorMessage;
use StreamIpc\Envelope\RequestEnvelope;
use StreamIpc\Envelope\ResponseEnvelope;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -58,7 +59,7 @@ public function testDispatchResponseStoresMessage(): void
unset($promise);
}

public function testDispatchErrorFromHandlerSendsLogMessage(): void
public function testDispatchErrorFromHandlerSendsErrorMessage(): void
{
$peer = new TestPeer();
$transport = new FakeTransport();
Expand All @@ -73,7 +74,8 @@ public function testDispatchErrorFromHandlerSendsLogMessage(): void
$this->assertCount(1, $transport->sent);
$this->assertInstanceOf(ResponseEnvelope::class, $transport->sent[0]);
$this->assertSame('123', $transport->sent[0]->id);
$this->assertStringContainsString('boom', $transport->sent[0]->response->message);
$this->assertInstanceOf(ErrorMessage::class, $transport->sent[0]->response);
$this->assertStringContainsString('boom', $transport->sent[0]->response->toString());
}

public function testRequestCreatesPromiseAndSendsEnvelope(): void
Expand Down
18 changes: 9 additions & 9 deletions tests/Unit/JsonMessageSerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
namespace StreamIpc\Tests\Unit;

use StreamIpc\Serialization\JsonMessageSerializer;
use StreamIpc\Message\LogMessage;
use StreamIpc\Message\ErrorMessage;
use StreamIpc\Tests\Fixtures\SimpleMessage;
use StreamIpc\Tests\Fixtures\ComplexMessage;
use PHPUnit\Framework\TestCase;
Expand All @@ -23,30 +23,30 @@ public function testRoundTripWithNestedObjects(): void
$this->assertSame([1, 2], $decoded->list);
}

public function testUnknownClassReturnsLogMessage(): void
public function testUnknownClassReturnsErrorMessage(): void
{
$serializer = new JsonMessageSerializer();
$json = json_encode(['__class' => 'NoSuchClass', 'foo' => 'bar']);
$decoded = $serializer->deserialize($json);

$this->assertInstanceOf(LogMessage::class, $decoded);
$this->assertSame('error', $decoded->level);
$this->assertInstanceOf(ErrorMessage::class, $decoded);
$this->assertStringContainsString('Unknown class', $decoded->toString());
}

public function testInvalidJsonProducesLogMessage(): void
public function testInvalidJsonProducesErrorMessage(): void
{
$serializer = new JsonMessageSerializer();
$decoded = $serializer->deserialize('{invalid json');
$this->assertInstanceOf(LogMessage::class, $decoded);
$this->assertSame('{invalid json', $decoded->message);
$this->assertInstanceOf(ErrorMessage::class, $decoded);
$this->assertStringContainsString('json_decode failed', $decoded->toString());
}

public function testDeserializedObjectNotImplementingMessage(): void
{
$serializer = new JsonMessageSerializer();
$objData = json_encode(['__class' => \stdClass::class]);
$decoded = $serializer->deserialize($objData);
$this->assertInstanceOf(LogMessage::class, $decoded);
$this->assertSame('error', $decoded->level);
$this->assertInstanceOf(ErrorMessage::class, $decoded);
$this->assertStringContainsString('Decoded object is not a Message', $decoded->toString());
}
}