Skip to content

Commit 9e87c39

Browse files
committed
Add option to emit STDERR data as custom event
1 parent df52c40 commit 9e87c39

6 files changed

Lines changed: 113 additions & 10 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,23 @@ $stream->on('close', function () {
221221
});
222222
```
223223

224+
Note that by default the output of both STDOUT and STDERR will be emitted
225+
as normal `data` events. You can optionally pass a custom event name which
226+
will be used to emit STDERR data so that it can be handled separately.
227+
Note that the normal streaming primitives likely do not know about this
228+
event, so special care may have to be taken.
229+
Also note that this option has no effect if you execute with a TTY.
230+
231+
```php
232+
$stream = $client->execStartStream($exec, $tty, 'stderr');
233+
$stream->on('data', function ($data) {
234+
echo 'STDOUT data: ' . $data;
235+
});
236+
$stream->on('stderr', function ($data) {
237+
echo 'STDERR data: ' . $data;
238+
});
239+
```
240+
224241
See also the [streaming exec example](examples/exec-stream.php) and the [exec benchmark example](examples/benchmark-exec.php).
225242

226243
The TTY mode should be set depending on whether your command needs a TTY

examples/exec-stream.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,26 @@
2727
$out = new Stream(STDOUT, $loop);
2828
$out->pause();
2929

30+
$stderr = new Stream(STDERR, $loop);
31+
$stderr->pause();
32+
3033
// unkown exit code by default
3134
$exit = 1;
3235

33-
$client->execCreate($container, $cmd)->then(function ($info) use ($client, $out, &$exit) {
34-
$stream = $client->execStartStream($info['Id']);
36+
$client->execCreate($container, $cmd)->then(function ($info) use ($client, $out, $stderr, &$exit) {
37+
$stream = $client->execStartStream($info['Id'], false, 'stderr');
3538
$stream->pipe($out);
3639

40+
// forward custom stderr event to STDERR stream
41+
$stream->on('stderr', function ($data) use ($stderr, $stream) {
42+
if ($stderr->write($data) === false) {
43+
$stream->pause();
44+
$stderr->once('drain', function () use ($stream) {
45+
$stream->resume();
46+
});
47+
}
48+
});
49+
3750
$stream->on('error', 'printf');
3851

3952
// remember exit code of executed command once it closes

src/Client.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,14 +1008,22 @@ public function execStartDetached($exec, $tty = false)
10081008
* This works for command output of any size as only small chunks have to
10091009
* be kept in memory.
10101010
*
1011-
* @param string $exec exec ID
1012-
* @param boolean $tty tty mode
1011+
* Note that by default the output of both STDOUT and STDERR will be emitted
1012+
* as normal "data" events. You can optionally pass a custom event name which
1013+
* will be used to emit STDERR data so that it can be handled separately.
1014+
* Note that the normal streaming primitives likely do not know about this
1015+
* event, so special care may have to be taken.
1016+
* Also note that this option has no effect if you execute with a TTY.
1017+
*
1018+
* @param string $exec exec ID
1019+
* @param boolean $tty tty mode
1020+
* @param string $stderrEvent custom event to emit for STDERR data (otherwise emits as "data")
10131021
* @return ReadableStreamInterface stream of exec data
10141022
* @link https://docs.docker.com/reference/api/docker_remote_api_v1.15/#exec-start
10151023
* @see self::execStart()
10161024
* @see self::execStartDetached()
10171025
*/
1018-
public function execStartStream($exec, $tty = false)
1026+
public function execStartStream($exec, $tty = false, $stderrEvent = null)
10191027
{
10201028
$stream = $this->streamingParser->parsePlainStream(
10211029
$this->browser->withOptions(array('streaming' => true))->post(
@@ -1036,7 +1044,7 @@ public function execStartStream($exec, $tty = false)
10361044

10371045
// this is a multiplexed stream unless this is started with a TTY
10381046
if (!$tty) {
1039-
$stream = $this->streamingParser->demultiplexStream($stream);
1047+
$stream = $this->streamingParser->demultiplexStream($stream, $stderrEvent);
10401048
}
10411049

10421050
return $stream;

src/Io/StreamingParser.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,27 @@ public function parsePlainStream(PromiseInterface $promise)
8888
* Returns a readable plain text stream for the given multiplexed stream using Docker's "attach multiplexing protocol"
8989
*
9090
* @param ReadableStreamInterface $input
91+
* @param string $stderrEvent
9192
* @return ReadableStreamInterface
9293
*/
93-
public function demultiplexStream(ReadableStreamInterface $input)
94+
public function demultiplexStream(ReadableStreamInterface $input, $stderrEvent = null)
9495
{
96+
if ($stderrEvent === null) {
97+
$stderrEvent = 'data';
98+
}
99+
95100
$out = new ReadableStream();
96101
$parser = new MultiplexStreamParser();
97102

98103
// pass all input data chunks through the parser
99-
$input->on('data', function ($chunk) use ($parser, $out) {
104+
$input->on('data', function ($chunk) use ($parser, $out, $stderrEvent) {
100105
// once parser emits, forward to output stream
101-
$parser->push($chunk, function ($stream, $data) use ($out) {
102-
$out->emit('data', array($data));
106+
$parser->push($chunk, function ($stream, $data) use ($out, $stderrEvent) {
107+
if ($stream === 2) {
108+
$out->emit($stderrEvent, array($data));
109+
} else {
110+
$out->emit('data', array($data));
111+
}
103112
});
104113
});
105114

tests/ClientTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,18 @@ public function testExecStartStreamWithTtyWillNotDemultiplex()
438438
$this->assertSame($stream, $this->client->execStartStream(123, $config));
439439
}
440440

441+
public function testExecStartStreamWithCustomStderrEvent()
442+
{
443+
$config = array();
444+
$stream = $this->getMock('React\Stream\ReadableStreamInterface');
445+
446+
$this->expectRequest('POST', '/exec/123/start', $this->createResponse());
447+
$this->streamingParser->expects($this->once())->method('parsePlainStream')->will($this->returnValue($stream));
448+
$this->streamingParser->expects($this->once())->method('demultiplexStream')->with($stream, 'stderr')->willReturn($stream);
449+
450+
$this->assertSame($stream, $this->client->execStartStream(123, $config, 'stderr'));
451+
}
452+
441453
public function testExecResize()
442454
{
443455
$this->expectRequestFlow('POST', '/exec/123/resize?w=800&h=600', $this->createResponse(), 'expectEmpty');

tests/FunctionalClientTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,27 @@ public function testExecStringCommandWithOutputWhileRunning($container)
171171
$this->assertEquals('hello world', $output);
172172
}
173173

174+
/**
175+
* @depends testStartRunning
176+
* @param string $container
177+
*/
178+
public function testExecStreamOutputInMultipleChunksWhileRunning($container)
179+
{
180+
$promise = $this->client->execCreate($container, 'echo -n hello && sleep 0 && echo -n world');
181+
$exec = Block\await($promise, $this->loop);
182+
183+
$this->assertTrue(is_array($exec));
184+
$this->assertTrue(is_string($exec['Id']));
185+
186+
$stream = $this->client->execStartStream($exec['Id']);
187+
$stream->once('data', $this->expectCallableOnceWith('hello'));
188+
//$stream->on('end', $this->expectCallableOnce());
189+
190+
$output = Block\await(Stream\buffer($stream), $this->loop);
191+
192+
$this->assertEquals('helloworld', $output);
193+
}
194+
174195
/**
175196
* @depends testStartRunning
176197
* @param string $container
@@ -228,6 +249,29 @@ public function testExecStreamCommandWithTtyAndStderrOutputWhileRunning($contain
228249
$this->assertEquals('hello world', $output);
229250
}
230251

252+
/**
253+
* @depends testStartRunning
254+
* @param string $container
255+
*/
256+
public function testExecStreamStderrCustomEventWhileRunning($container)
257+
{
258+
$promise = $this->client->execCreate($container, 'echo -n hello world >&2');
259+
$exec = Block\await($promise, $this->loop);
260+
261+
$this->assertTrue(is_array($exec));
262+
$this->assertTrue(is_string($exec['Id']));
263+
264+
$stream = $this->client->execStartStream($exec['Id'], false, 'err');
265+
$stream->on('err', $this->expectCallableOnceWith('hello world'));
266+
$stream->on('data', $this->expectCallableNever());
267+
$stream->on('error', $this->expectCallableNever());
268+
//$stream->on('end', $this->expectCallableOnce());
269+
270+
$output = Block\await(Stream\buffer($stream), $this->loop);
271+
272+
$this->assertEquals('', $output);
273+
}
274+
231275
/**
232276
* @depends testStartRunning
233277
* @param string $container

0 commit comments

Comments
 (0)