GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — fix(attach): handle TTY and upgraded attach responses · docker-php/docker-php@8024e95

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • docs/reference
    • streams.mdx
  • src
    • Docker.php
    • Endpoint
      • ContainerAttach.php
      • ContainerAttachWebsocket.php
    • Stream
      • AttachWebsocketStream.php
  • tests
    • Endpoint
      • ContainerAttachTest.php
    • Resource
      • ContainerResourceTest.php
    • Stream
      • AttachWebsocketStreamTest.php
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎docs/reference/streams.mdx‎

Copy file name to clipboardExpand all lines: docs/reference/streams.mdx
+45-11Lines changed: 45 additions & 11 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,51 @@ for status checks and raw-fetch error handling.
129129
`containerAttach()` attaches to the container's existing process; it does not
130130
create a new command like `containerExec()` followed by `execStart()`.
131131

132-
The current attach wrapper recognizes HTTP 200 with the exact
133-
`application/vnd.docker.raw-stream` content type and creates a multiplexed
134-
`DockerRawStream`. Unlike the log and exec wrappers, it does not select a
135-
TTY-aware decoder or handle all upgraded-response variants. Do not assume
136-
that terminal output or HTTP 101 will work through this wrapper.
137-
138-
`containerAttachWebsocket()` uses a separate `AttachWebsocketStream` with
139-
`read()`, `write()` and `close()`, not `onStdout()`/`wait()`. Its response
140-
handling is also narrow, and the existing WebSocket integration test is
141-
skipped. Treat it as a feature requiring validation with your daemon and
142-
transport, not a tested interactive-terminal recipe.
132+
`containerAttach()` returns a `DockerRawStream` for plain and upgraded
133+
responses. Docker labels TTY output and framed non-TTY output on plain
134+
responses with the same content type, so the client inspects the container's
135+
TTY setting to choose the decoder. TTY output arrives through `onStdout()`.
136+
Since `3.3.0`, attaching to a TTY container no longer loses its output.
137+
138+
`containerAttachWebsocket()` returns an `AttachWebsocketStream` with `read()`,
139+
`write()` and `close()`, not `onStdout()`/`wait()`. It is useful when you need
140+
to write to the container's stdin over the attach connection:
141+
142+
```php
143+
<?php
144+
145+
require __DIR__ . '/vendor/autoload.php';
146+
147+
use Docker\Docker;
148+
149+
$docker = Docker::create();
150+
$attach = $docker->containerAttachWebsocket('my-container', [
151+
'stream' => true,
152+
'stdin' => true,
153+
'stdout' => true,
154+
'stderr' => true,
155+
]);
156+
157+
$attach->write("echo hello\n");
158+
// Print output until a second passes without any.
159+
while (is_string($output = $attach->read(1))) {
160+
echo $output;
161+
}
162+
$attach->close();
163+
```
164+
165+
`read()` waits up to the given seconds and microseconds for one frame. It
166+
returns the frame's bytes, `false` when nothing arrived in time, or `null` once
167+
Docker has closed the connection. A frame is a chunk of output, not a line.
168+
Docker mixes stdout and stderr in the same frames, with no marker for which
169+
stream a chunk came from. The stream answers pings and closes itself when
170+
Docker sends a close frame. `write()` sends one binary frame.
171+
172+
WebSocket attach needs `3.3.0` or later; earlier clients returned `null`
173+
instead of a stream. Docker Desktop's socket proxy upgrades the connection but
174+
does not forward attach output; a Linux daemon or a remote engine works. For
175+
driving commands, [interactive exec](/guides/interactive-exec) gives separate
176+
stdout and stderr, exit codes and deadlines.
143177

144178
`DockerRawStream::onStdin()` is an output-channel callback; it is not a method
145179
for writing commands into a container. Prefer the [exec recipe](/guides/exec)
Collapse file

‎src/Docker.php‎

Copy file name to clipboardExpand all lines: src/Docker.php
+6-1Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@ class Docker extends Client
4646
*/
4747
public function containerAttach(string $id, array $queryParameters = [], string $fetch = self::FETCH_OBJECT, array $accept = [])
4848
{
49-
return $this->executeEndpoint(new ContainerAttach($id, $queryParameters, $accept), $fetch);
49+
return $this->executeEndpoint(new ContainerAttach(
50+
$id,
51+
$queryParameters,
52+
$accept,
53+
fn (): bool => (bool) $this->containerInspect($id)->getConfig()?->getTty()
54+
), $fetch);
5055
}
5156

5257
/**
Collapse file

‎src/Endpoint/ContainerAttach.php‎

Copy file name to clipboardExpand all lines: src/Endpoint/ContainerAttach.php
+23-2Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,31 @@
1111

1212
class ContainerAttach extends BaseEndpoint
1313
{
14+
/** @var callable|null Resolves the container's TTY setting for ambiguous raw-stream responses. */
15+
private $ttyResolver;
16+
17+
public function __construct(string $id, array $queryParameters = [], array $accept = [], ?callable $ttyResolver = null)
18+
{
19+
parent::__construct($id, $queryParameters, $accept);
20+
$this->ttyResolver = $ttyResolver;
21+
}
22+
1423
protected function transformResponseBody(ResponseInterface $response, SerializerInterface $serializer, ?string $contentType = null)
1524
{
16-
if (200 === $response->getStatusCode() && DockerRawStream::HEADER === $contentType) {
17-
return new DockerRawStream($response->getBody());
25+
// 101 is an upgraded connection; its body is the same output stream.
26+
if (200 === $response->getStatusCode() || 101 === $response->getStatusCode()) {
27+
$mediaType = strtolower(trim(explode(';', $contentType ?? '')[0]));
28+
if (DockerRawStream::MULTIPLEXED_HEADER === $mediaType) {
29+
return new DockerRawStream($response->getBody());
30+
}
31+
if (DockerRawStream::HEADER === $mediaType) {
32+
// Docker uses this header for TTY output and for framed non-TTY
33+
// output on plain responses. Keep the historical framed behavior
34+
// when used without a resolver.
35+
$multiplexed = null === $this->ttyResolver || !($this->ttyResolver)();
36+
37+
return new DockerRawStream($response->getBody(), $multiplexed);
38+
}
1839
}
1940

2041
return parent::transformResponseBody($response, $serializer, $contentType);
Collapse file

‎src/Endpoint/ContainerAttachWebsocket.php‎

Copy file name to clipboardExpand all lines: src/Endpoint/ContainerAttachWebsocket.php
+4-5Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
use Docker\API\Endpoint\ContainerAttachWebsocket as BaseEndpoint;
88
use Docker\Stream\AttachWebsocketStream;
9-
use Docker\Stream\DockerRawStream;
109
use Psr\Http\Message\ResponseInterface;
1110
use Symfony\Component\Serializer\SerializerInterface;
1211

@@ -22,15 +21,15 @@ public function getExtraHeaders(): array
2221
'Upgrade' => 'websocket',
2322
'Connection' => 'Upgrade',
2423
'Sec-WebSocket-Version' => '13',
25-
'Sec-WebSocket-Key' => base64_encode(uniqid()),
24+
// RFC 6455 requires 16 random bytes.
25+
'Sec-WebSocket-Key' => base64_encode(random_bytes(16)),
2626
]
2727
);
2828
}
2929

30-
protected function transformResponseBody(ResponseInterface $response, SerializerInterface $serializer, ?string
31-
$contentType = null)
30+
protected function transformResponseBody(ResponseInterface $response, SerializerInterface $serializer, ?string $contentType = null)
3231
{
33-
if (200 === $response->getStatusCode() && DockerRawStream::HEADER === $contentType) {
32+
if (101 === $response->getStatusCode() && 'websocket' === strtolower(trim($response->getHeaderLine('Upgrade')))) {
3433
return new AttachWebsocketStream($response->getBody());
3534
}
3635

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.