GeezerVison is showing github.com — feat(websocket): add close() to attach streams · docker-php/docker-php@459a1f2
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: docs/reference/streams.mdx +4-4Lines changed: 4 additions & 4 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -132,10 +132,10 @@ TTY-aware decoder or handle all upgraded-response variants. Do not assume | 132 | 132 | that terminal output or HTTP 101 will work through this wrapper. | 133 | 133 |
| 134 | 134 | `containerAttachWebsocket()` uses a separate `AttachWebsocketStream` with | 135 | | -`read()` and `write()`, not `onStdout()`/`wait()`. Its response handling is also | 136 | | -narrow, and the existing WebSocket integration test is skipped. Treat it as | 137 | | -a feature requiring validation with your daemon and transport, not a tested | 138 | | -interactive-terminal recipe. | | 135 | +`read()`, `write()` and `close()`, not `onStdout()`/`wait()`. Its response | | 136 | +handling is also narrow, and the existing WebSocket integration test is | | 137 | +skipped. Treat it as a feature requiring validation with your daemon and | | 138 | +transport, not a tested interactive-terminal recipe. | 139 | 139 |
| 140 | 140 | `DockerRawStream::onStdin()` is an output-channel callback; it is not a method | 141 | 141 | for writing commands into a container. Prefer the [exec recipe](/guides/exec) | |
Collapse file Copy file name to clipboardExpand all lines: src/Stream/AttachWebsocketStream.php +21Lines changed: 21 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -30,6 +30,10 @@ public function __construct(StreamInterface $stream) | 30 | 30 | */ | 31 | 31 | public function write($data): void | 32 | 32 | { | | 33 | + if (!\is_resource($this->socket)) { | | 34 | + throw new \RuntimeException('The WebSocket stream is closed.'); | | 35 | + } | | 36 | + | 33 | 37 | $rand = random_int(0, 28); | 34 | 38 | $frame = [ | 35 | 39 | 'fin' => 1, | @@ -80,6 +84,23 @@ public function write($data): void | 80 | 84 | $this->socketWrite($frame['data']); | 81 | 85 | } | 82 | 86 |
| | 87 | + /** | | 88 | + * Send a close frame and close the connection. | | 89 | + * | | 90 | + * Later read() calls return null. Calling close() again does nothing. | | 91 | + */ | | 92 | + public function close(): void | | 93 | + { | | 94 | + if (!\is_resource($this->socket)) { | | 95 | + return; | | 96 | + } | | 97 | + | | 98 | + // Masked close frame without a status code (RFC 6455 section 5.5.1). | | 99 | + // The daemon may already have closed its side, so a failed write is ignored. | | 100 | + @$this->socketWrite("\x88\x80".random_bytes(4)); | | 101 | + fclose($this->socket); | | 102 | + } | | 103 | + | 83 | 104 | /** | 84 | 105 | * Block until it receive a frame from websocket or return null if no more connexion. | 85 | 106 | * | |
Collapse file Copy file name to clipboardExpand all lines: tests/Stream/AttachWebsocketStreamTest.php +39Lines changed: 39 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -22,4 +22,43 @@ public function testReadsEmptyFrameBeforePayload(): void | 22 | 22 | $this->assertSame('hi', $stream->read()); | 23 | 23 | fclose($server); | 24 | 24 | } | | 25 | + | | 26 | + public function testCloseSendsCloseFrameAndClosesSocket(): void | | 27 | + { | | 28 | + [$client, $server] = stream_socket_pair(\STREAM_PF_UNIX, \STREAM_SOCK_STREAM, \STREAM_IPPROTO_IP); | | 29 | + $stream = new AttachWebsocketStream(Stream::create($client)); | | 30 | + | | 31 | + $stream->close(); | | 32 | + $stream->close(); | | 33 | + | | 34 | + $frame = fread($server, 16); | | 35 | + $this->assertSame("\x88\x80", substr($frame, 0, 2)); | | 36 | + $this->assertSame(6, \strlen($frame)); | | 37 | + $this->assertSame('', fread($server, 1)); | | 38 | + $this->assertTrue(feof($server)); | | 39 | + $this->assertNull($stream->read()); | | 40 | + fclose($server); | | 41 | + } | | 42 | + | | 43 | + public function testCloseIgnoresPeerThatAlreadyClosed(): void | | 44 | + { | | 45 | + [$client, $server] = stream_socket_pair(\STREAM_PF_UNIX, \STREAM_SOCK_STREAM, \STREAM_IPPROTO_IP); | | 46 | + fclose($server); | | 47 | + $stream = new AttachWebsocketStream(Stream::create($client)); | | 48 | + | | 49 | + $stream->close(); | | 50 | + | | 51 | + $this->assertNull($stream->read()); | | 52 | + } | | 53 | + | | 54 | + public function testWriteAfterCloseThrows(): void | | 55 | + { | | 56 | + [$client, $server] = stream_socket_pair(\STREAM_PF_UNIX, \STREAM_SOCK_STREAM, \STREAM_IPPROTO_IP); | | 57 | + $stream = new AttachWebsocketStream(Stream::create($client)); | | 58 | + $stream->close(); | | 59 | + fclose($server); | | 60 | + | | 61 | + $this->expectException(\RuntimeException::class); | | 62 | + $stream->write('exit'); | | 63 | + } | 25 | 64 | } |
|