diff --git a/README.md b/README.md index 922a800..d8dd491 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,12 @@ $stream->on('close', function () { See also the [streaming exec example](examples/exec-stream.php) and the [exec benchmark example](examples/benchmark-exec.php). +The TTY mode should be set depending on whether your command needs a TTY +or not. Note that toggling the TTY mode affects how/whether you can access +the STDERR stream and also has a significant impact on performance for +larger streams (relevant for 100 MiB and above). See also the TTY mode +on the `execStart*()` call. + Running this benchmark on my personal (rather mediocre) VM setup reveals that the benchmark achieves a throughput of ~300 MiB/s while the (totally unfair) comparison script using the plain Docker client only yields ~100 MiB/s. diff --git a/examples/benchmark-exec.php b/examples/benchmark-exec.php index 178fc66..79b55ec 100644 --- a/examples/benchmark-exec.php +++ b/examples/benchmark-exec.php @@ -23,7 +23,7 @@ $factory = new Factory($loop); $client = $factory->createClient(); -$client->execCreate($container, array('Cmd' => $cmd, 'AttachStdout' => true, 'AttachStderr' => true))->then(function ($info) use ($client) { +$client->execCreate($container, $cmd)->then(function ($info) use ($client) { $stream = $client->execStartStream($info['Id'], true); $start = microtime(true); diff --git a/examples/exec-inspect.php b/examples/exec-inspect.php index 72b55f3..9d2f677 100644 --- a/examples/exec-inspect.php +++ b/examples/exec-inspect.php @@ -23,7 +23,7 @@ $factory = new Factory($loop); $client = $factory->createClient(); -$client->execCreate($container, array('Cmd' => $cmd, 'AttachStdout' => true, 'AttachStderr' => true, 'Tty' => true))->then(function ($info) use ($client) { +$client->execCreate($container, $cmd, true)->then(function ($info) use ($client) { echo 'Created with info: ' . json_encode($info) . PHP_EOL; return $client->execInspect($info['Id']); diff --git a/examples/exec-stream.php b/examples/exec-stream.php index bf66f91..9fb7a55 100644 --- a/examples/exec-stream.php +++ b/examples/exec-stream.php @@ -27,7 +27,7 @@ $out = new Stream(STDOUT, $loop); $out->pause(); -$client->execCreate($container, array('Cmd' => $cmd, 'AttachStdout' => true, 'AttachStderr' => true, 'Tty' => true))->then(function ($info) use ($client, $out) { +$client->execCreate($container, $cmd, true)->then(function ($info) use ($client, $out) { $stream = $client->execStartStream($info['Id'], true); $stream->pipe($out); diff --git a/src/Client.php b/src/Client.php index 50fa9e4..afb41a0 100644 --- a/src/Client.php +++ b/src/Client.php @@ -882,13 +882,45 @@ public function imageSearch($term) /** * Sets up an exec instance in a running container id * - * @param string $container container ID - * @param array $config `array('Cmd' => 'date')` (see link) + * The $command should be given as an array of strings (the command plus + * arguments). Alternatively, you can also pass a single command line string + * which will then be wrapped in a shell process. + * + * The TTY mode should be set depending on whether your command needs a TTY + * or not. Note that toggling the TTY mode affects how/whether you can access + * the STDERR stream and also has a significant impact on performance for + * larger streams (relevant for 100 MiB and above). See also the TTY mode + * on the `execStart*()` call: + * - create=false, start=false: + * STDOUT/STDERR are multiplexed into separate streams + quite fast. + * This is the default mode, also for `docker exec`. + * - create=true, start=true: + * STDOUT and STDERR are mixed into a single stream + relatively slow. + * This is how `docker exec -t` works internally. + * - create=false, start=true + * STDOUT is streamed, STDERR can not be accessed at all + fastest mode. + * This looks strange to you? It probably is. See also the benchmarking example. + * - create=true, start=false + * STDOUT/STDERR are multiplexed into separate streams + relatively slow + * This looks strange to you? It probably is. Consider using the first option instead. + * + * @param string $container container ID + * @param string|array $cmd Command to run specified as an array of strings or a single command string + * @param boolean $tty TTY mode + * @param boolean $stdin attaches to STDIN of the exec command + * @param boolean $stdout attaches to STDOUT of the exec command + * @param boolean $stderr attaches to STDERR of the exec command + * @param string|int $user user-specific exec, otherwise defaults to main container user (requires API v1.19+ / Docker v1.7+) + * @param boolean $privileged privileged exec with all capabilities enabled (requires API v1.19+ / Docker v1.7+) * @return PromiseInterface Promise with exec ID in the form of `array("Id" => $execId)` * @link https://docs.docker.com/reference/api/docker_remote_api_v1.15/#exec-create */ - public function execCreate($container, $config) + public function execCreate($container, $cmd, $tty = false, $stdin = false, $stdout = true, $stderr = true, $user = '', $privileged = false) { + if (!is_array($cmd)) { + $cmd = array('sh', '-c', (string)$cmd); + } + return $this->postJson( $this->uri->expand( '/containers/{container}/exec', @@ -896,7 +928,15 @@ public function execCreate($container, $config) 'container' => $container ) ), - $config + array( + 'Cmd' => $cmd, + 'Tty' => !!$tty, + 'AttachStdin' => !!$stdin, + 'AttachStdout' => !!$stdout, + 'AttachStderr' => !!$stderr, + 'User' => $user, + 'Privileged' => !!$privileged, + ) )->then(array($this->parser, 'expectJson')); } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index b9e7c8a..b0761df 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -379,10 +379,17 @@ public function testImageSearch() public function testExecCreate() { $json = array(); - $config = array(); $this->expectRequestFlow('post', '/containers/123/exec', $this->createResponseJson($json), 'expectJson'); - $this->expectPromiseResolveWith($json, $this->client->execCreate(123, $config)); + $this->expectPromiseResolveWith($json, $this->client->execCreate(123, array('env'))); + } + + public function testExecCreateStringCommand() + { + $json = array(); + $this->expectRequestFlow('post', '/containers/123/exec', $this->createResponseJson($json), 'expectJson'); + + $this->expectPromiseResolveWith($json, $this->client->execCreate(123, 'env')); } public function testExecDetached() diff --git a/tests/FunctionalClientTest.php b/tests/FunctionalClientTest.php index 74204e8..80e14d8 100644 --- a/tests/FunctionalClientTest.php +++ b/tests/FunctionalClientTest.php @@ -104,12 +104,7 @@ public function testStartRunning() */ public function testExecCreateWhileRunning($container) { - $promise = $this->client->execCreate($container, array( - 'Cmd' => array('echo', '-n', 'hello', 'world'), - 'AttachStdout' => true, - 'AttachStderr' => true, - 'Tty' => true - )); + $promise = $this->client->execCreate($container, array('echo', '-n', 'hello', 'world')); $exec = Block\await($promise, $this->loop); $this->assertTrue(is_array($exec)); @@ -158,18 +153,67 @@ public function testExecInspectAfterRunning($exec) $this->assertEquals(0, $info['ExitCode']); } + /** + * @depends testStartRunning + * @param string $container + */ + public function testExecStringCommandWithOutputWhileRunning($container) + { + $promise = $this->client->execCreate($container, 'echo -n hello world'); + $exec = Block\await($promise, $this->loop); + + $this->assertTrue(is_array($exec)); + $this->assertTrue(is_string($exec['Id'])); + + $promise = $this->client->execStart($exec['Id'], true); + $output = Block\await($promise, $this->loop); + + $this->assertEquals('hello world', $output); + } + + /** + * @depends testStartRunning + * @param string $container + */ + public function testExecUserSpecificCommandWithOutputWhileRunning($container) + { + $promise = $this->client->execCreate($container, 'whoami', true, false, true, true, 'nobody'); + $exec = Block\await($promise, $this->loop); + + $this->assertTrue(is_array($exec)); + $this->assertTrue(is_string($exec['Id'])); + + $promise = $this->client->execStart($exec['Id'], true); + $output = Block\await($promise, $this->loop); + + $this->assertEquals('nobody', rtrim($output)); + } + + /** + * @depends testStartRunning + * @param string $container + */ + public function testExecStringCommandWithStderrOutputWhileRunning($container) + { + $promise = $this->client->execCreate($container, 'echo -n hello world >&2', true); + $exec = Block\await($promise, $this->loop); + + $this->assertTrue(is_array($exec)); + $this->assertTrue(is_string($exec['Id'])); + + $promise = $this->client->execStart($exec['Id'], true); + $output = Block\await($promise, $this->loop); + + $this->assertEquals('hello world', $output); + } + /** * @depends testStartRunning * @param string $container */ public function testExecStreamEmptyOutputWhileRunning($container) { - $promise = $this->client->execCreate($container, array( - 'Cmd' => array('true'), - 'AttachStdout' => true, - 'AttachStderr' => true, - 'Tty' => true - )); + $promise = $this->client->execCreate($container, array('true')); $exec = Block\await($promise, $this->loop); $this->assertTrue(is_array($exec)); @@ -189,12 +233,7 @@ public function testExecStreamEmptyOutputWhileRunning($container) */ public function testExecDetachedWhileRunning($container) { - $promise = $this->client->execCreate($container, array( - 'Cmd' => array('sleep', '10'), - 'AttachStdout' => true, - 'AttachStderr' => true, - 'Tty' => true - )); + $promise = $this->client->execCreate($container, array('sleep', '10')); $exec = Block\await($promise, $this->loop); $this->assertTrue(is_array($exec));