Skip to content

Commit a2a04fa

Browse files
authored
Merge pull request #34 from clue-labs/command
Improve documentation for command passed to Process, plus tests
2 parents 9b06ecc + 2054522 commit a2a04fa

2 files changed

Lines changed: 139 additions & 19 deletions

File tree

README.md

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ as [Streams](https://github.com/reactphp/stream).
1818
* [EventEmitter Events](#eventemitter-events)
1919
* [Methods](#methods)
2020
* [Stream Properties](#stream-properties)
21-
* [Prepending Commands with `exec`](#prepending-commands-with-exec)
21+
* [Command](#command)
2222
* [Sigchild Compatibility](#sigchild-compatibility)
23-
* [Command Chaining](#command-chaining)
2423
* [Install](#install)
2524
* [Tests](#tests)
2625
* [License](#license)
@@ -102,15 +101,106 @@ $process->stdin->close();
102101
For more details, see the
103102
[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface).
104103

105-
### Prepending Commands with `exec`
104+
### Command
106105

107-
Symfony pull request [#5759](https://github.com/symfony/symfony/issues/5759)
108-
documents a caveat with the
109-
[Program Execution](http://php.net/manual/en/book.exec.php) extension. PHP will
110-
launch processes via `sh`, which obfuscates the underlying process' PID and
111-
complicates signaling (our process becomes a child of `sh`). As a work-around,
112-
prepend the command string with `exec`, which will cause the `sh` process to be
113-
replaced by our process.
106+
The `Process` class allows you to pass any kind of command line string:
107+
108+
```php
109+
$process = new Process('echo test');
110+
```
111+
112+
By default, PHP will launch processes by wrapping the given command line string
113+
in a `sh` command, so that the above example will actually execute
114+
`sh -c echo test` under the hood.
115+
116+
This is a very useful feature because it does not only allow you to pass single
117+
commands, but actually allows you to pass any kind of shell command line and
118+
launch multiple sub-commands using command chains (with `&&`, `||`, `;` and
119+
others) and allows you to redirect STDIO streams (with `2>&1` and family).
120+
This can be used to pass complete command lines and receive the resulting STDIO
121+
streams from the wrapping shell command like this:
122+
123+
```php
124+
$process = new Process('echo run && demo || echo failed');
125+
```
126+
127+
In other words, the underlying shell is responsible for managing this command
128+
line and launching the individual sub-commands and connecting their STDIO
129+
streams as appropriate.
130+
This implies that the `Process` class will only receive the resulting STDIO
131+
streams from the wrapping shell, which will thus contain the complete
132+
input/output with no way to discern the input/output of single sub-commands.
133+
134+
If you want to discern the output of single sub-commands, you may want to
135+
implement some higher-level protocol logic, such as printing an explicit
136+
boundary between each sub-command like this:
137+
138+
```php
139+
$process = new Process('cat first && echo --- && cat second');
140+
```
141+
142+
As an alternative, considering launching one process at a time and listening on
143+
its `exit` event to conditionally start the next process in the chain.
144+
This will give you an opportunity to configure the subsequent process I/O streams:
145+
146+
```php
147+
$first = new Process('cat first');
148+
$first->start($loop);
149+
150+
$first->on('exit', function () use ($loop) {
151+
$second = new Process('cat second');
152+
$second->start($loop);
153+
});
154+
```
155+
156+
Keep in mind that PHP uses the shell wrapper for ALL command lines.
157+
While this may seem reasonable for more complex command lines, this actually
158+
also applies to running the most simple single command:
159+
160+
```php
161+
$process = new Process('yes');
162+
```
163+
164+
This will actually spawn a command hierarchy similar to this:
165+
166+
```
167+
5480 … \_ php example.php
168+
5481 … \_ sh -c yes
169+
5482 … \_ yes
170+
```
171+
172+
This means that trying to get the underlying process PID or sending signals
173+
will actually target the wrapping shell, which may not be the desired result
174+
in many cases.
175+
176+
If you do not want this wrapping shell process to show up, you can simply
177+
prepend the command string with `exec`, which will cause the wrapping shell
178+
process to be replaced by our process:
179+
180+
```php
181+
$process = new Process('exec yes');
182+
```
183+
184+
This will show a resulting command hierarchy similar to this:
185+
186+
```
187+
5480 … \_ php example.php
188+
5481 … \_ yes
189+
```
190+
191+
This means that trying to get the underlying process PID and sending signals
192+
will now target the actual command as expected.
193+
194+
Note that in this case, the command line will not be run in a wrapping shell.
195+
This implies that when using `exec`, there's no way to pass command lines such
196+
as those containing command chains or redirected STDIO streams.
197+
198+
As a rule of thumb, most commands will likely run just fine with the wrapping
199+
shell.
200+
If you pass a complete command line (or are unsure), you SHOULD most likely keep
201+
the wrapping shell.
202+
If you want to pass an invidual command only, you MAY want to consider
203+
prepending the command string with `exec` to avoid the wrapping shell.
114204

115205
### Sigchild Compatibility
116206

@@ -127,15 +217,6 @@ of the actual exit code.
127217
**Note:** This functionality was taken from Symfony's
128218
[Process](https://github.com/symfony/process) compoment.
129219

130-
### Command Chaining
131-
132-
Command chaning with `&&` or `;`, while possible with `proc_open()`, should not
133-
be used with this component. There is currently no way to discern when each
134-
process in a chain ends, which would complicate working with I/O streams. As an
135-
alternative, considering launching one process at a time and listening on its
136-
`exit` event to conditionally start the next process in the chain. This will
137-
give you an opportunity to configure the subsequent process' I/O streams.
138-
139220
## Install
140221

141222
The recommended way to install this library is [through Composer](http://getcomposer.org).

tests/AbstractProcessTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,45 @@ public function testReceivesProcessStdoutFromDd()
106106
$this->assertEquals(12345 * 1234, $bytes);
107107
}
108108

109+
public function testProcessPidNotSameDueToShellWrapper()
110+
{
111+
$cmd = $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getmypid();');
112+
113+
$loop = $this->createLoop();
114+
$process = new Process($cmd, '/');
115+
$process->start($loop);
116+
117+
$output = '';
118+
$process->stdout->on('data', function ($data) use (&$output) {
119+
$output .= $data;
120+
});
121+
122+
$loop->run();
123+
124+
$this->assertNotEquals('', $output);
125+
$this->assertNotNull($process->getPid());
126+
$this->assertNotEquals($process->getPid(), $output);
127+
}
128+
129+
public function testProcessPidSameWithExec()
130+
{
131+
$cmd = 'exec ' . $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getmypid();');
132+
133+
$loop = $this->createLoop();
134+
$process = new Process($cmd, '/');
135+
$process->start($loop);
136+
137+
$output = '';
138+
$process->stdout->on('data', function ($data) use (&$output) {
139+
$output .= $data;
140+
});
141+
142+
$loop->run();
143+
144+
$this->assertNotNull($process->getPid());
145+
$this->assertEquals($process->getPid(), $output);
146+
}
147+
109148
public function testProcessWithDefaultCwdAndEnv()
110149
{
111150
$cmd = $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getcwd(), PHP_EOL, count($_SERVER), PHP_EOL;');

0 commit comments

Comments
 (0)