Skip to content

Commit 015717b

Browse files
committed
Do not launch command line in a shell (cmd.exe) by default on Windows
1 parent 09390e2 commit 015717b

File tree

3 files changed

+106
-18
lines changed

3 files changed

+106
-18
lines changed

README.md

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,30 @@ For more details, see the
112112
The `Process` class allows you to pass any kind of command line string:
113113

114114
```php
115-
$process = new Process('echo test');
115+
$process = new Process('echo test', …);
116+
$process->start($loop);
117+
```
118+
119+
The command line string usually consists of a whitespace-separated list with
120+
your main executable bin and any number of arguments. Special care should be
121+
taken to quote any arguments via `escapeshellarg()` if you pass any user input
122+
along. Likewise, keep in mind that especially on Windows, it is rather common to
123+
have path names containing spaces and other special characters. If you want to
124+
run a binary like this, you will have to ensure this is quoted as a single
125+
argument like this:
126+
127+
```php
128+
$bin = 'C:\\Program files (x86)\\PHP\\php.exe';
129+
$file = 'C:\\Users\\me\\Desktop\\Application\\main.php';
130+
131+
$process = new Process(escapeshellarg($bin) . ' ' . escapeshellarg($file), …);
116132
$process->start($loop);
117133
```
118134

119135
By default, PHP will launch processes by wrapping the given command line string
120-
in a `sh` command on Unix, so that the above example will actually execute
121-
`sh -c echo test` under the hood on Unix. On Windows, it will launch processes
122-
by wrapping it in a `cmd` shell like `cmd /C echo test`.
136+
in a `sh` command on Unix, so that the first example will actually execute
137+
`sh -c echo test` under the hood on Unix. On Windows, it will not launch
138+
processes by wrapping them in a shell.
123139

124140
This is a very useful feature because it does not only allow you to pass single
125141
commands, but actually allows you to pass any kind of shell command line and
@@ -133,6 +149,12 @@ $process = new Process('echo run && demo || echo failed');
133149
$process->start($loop);
134150
```
135151

152+
> Note that [Windows support](#windows-compatibility) is limited in that it
153+
doesn't support STDIO streams at all and also that processes will not be run
154+
in a wrapping shell by default. If you want to run a shell built-in function
155+
such as `echo hello` or `sleep 10`, you may have to prefix your command line
156+
with an explicit shell like `cmd /c echo hello`.
157+
136158
In other words, the underlying shell is responsible for managing this command
137159
line and launching the individual sub-commands and connecting their STDIO
138160
streams as appropriate.
@@ -145,7 +167,7 @@ implement some higher-level protocol logic, such as printing an explicit
145167
boundary between each sub-command like this:
146168

147169
```php
148-
$process = new Process('cat first && echo --- && cat second');
170+
$process = new Process('cat first && echo --- && cat second', …);
149171
$process->start($loop);
150172
```
151173

@@ -154,7 +176,7 @@ its `exit` event to conditionally start the next process in the chain.
154176
This will give you an opportunity to configure the subsequent process I/O streams:
155177

156178
```php
157-
$first = new Process('cat first');
179+
$first = new Process('cat first', …);
158180
$first->start($loop);
159181

160182
$first->on('exit', function () use ($loop) {
@@ -163,7 +185,7 @@ $first->on('exit', function () use ($loop) {
163185
});
164186
```
165187

166-
Keep in mind that PHP uses the shell wrapper for ALL command lines.
188+
Keep in mind that PHP uses the shell wrapper for ALL command lines on Unix.
167189
While this may seem reasonable for more complex command lines, this actually
168190
also applies to running the most simple single command:
169191

@@ -172,7 +194,7 @@ $process = new Process('yes');
172194
$process->start($loop);
173195
```
174196

175-
This will actually spawn a command hierarchy similar to this:
197+
This will actually spawn a command hierarchy similar to this on Unix:
176198

177199
```
178200
5480 … \_ php example.php
@@ -525,6 +547,18 @@ want to run a child process on Windows, each with its own set of pros and cons:
525547
In this case, we suggest looking at the excellent
526548
[createprocess-windows](https://github.com/cubiclesoft/createprocess-windows).
527549

550+
Additionally, note that the [command](#command) given to the `Process` will be
551+
passed to the underlying Windows-API
552+
([`CreateProcess`](https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-createprocessa))
553+
as-is and the process will not be launched in a wrapping shell by default. In
554+
particular, this means that shell built-in functions such as `echo hello` or
555+
`sleep 10` may have to be prefixed with an explicit shell command like this:
556+
557+
```php
558+
$process = new Process('cmd /c echo hello', null, null, $pipes);
559+
$process->start($loop);
560+
```
561+
528562
## Install
529563

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

src/Process.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,15 @@ public function start(LoopInterface $loop, $interval = 0.1)
145145
$cmd = sprintf('(%s) ' . $sigchild . '>/dev/null; code=$?; echo $code >&' . $sigchild . '; exit $code', $cmd);
146146
}
147147

148-
$this->process = proc_open($cmd, $fdSpec, $pipes, $this->cwd, $this->env);
148+
// on Windows, we do not launch the given command line in a shell (cmd.exe) by default and omit any error dialogs
149+
// the cmd.exe shell can explicitly be given as part of the command as detailed in both documentation and tests
150+
$options = array();
151+
if (DIRECTORY_SEPARATOR === '\\') {
152+
$options['bypass_shell'] = true;
153+
$options['suppress_errors'] = true;
154+
}
155+
156+
$this->process = proc_open($cmd, $fdSpec, $pipes, $this->cwd, $this->env, $options);
149157

150158
if (!is_resource($this->process)) {
151159
throw new \RuntimeException('Unable to launch a new process.');

tests/AbstractProcessTest.php

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ public function testStartWillAssignPipes()
5151

5252
public function testStartWithoutAnyPipesWillNotAssignPipes()
5353
{
54-
$process = new Process('exit 0', null, null, array());
54+
if (DIRECTORY_SEPARATOR === '\\') {
55+
$process = new Process('cmd /c exit 0', null, null, array());
56+
} else {
57+
$process = new Process('exit 0', null, null, array());
58+
}
5559
$process->start($this->createLoop());
5660

5761
$this->assertNull($process->stdin);
@@ -84,7 +88,7 @@ public function testIsRunning()
8488
{
8589
if (DIRECTORY_SEPARATOR === '\\') {
8690
// Windows doesn't have a sleep command and also does not support process pipes
87-
$process = new Process('php -r ' . escapeshellarg('sleep(1);'), null, null, array());
91+
$process = new Process($this->getPhpBinary() . ' -r ' . escapeshellarg('sleep(1);'), null, null, array());
8892
} else {
8993
$process = new Process('sleep 1');
9094
}
@@ -156,7 +160,11 @@ public function testReceivesProcessOutputFromStdoutRedirectedToFile()
156160
{
157161
$tmp = tmpfile();
158162

159-
$cmd = 'echo test';
163+
if (DIRECTORY_SEPARATOR === '\\') {
164+
$cmd = 'cmd /c echo test';
165+
} else {
166+
$cmd = 'echo test';
167+
}
160168

161169
$loop = $this->createLoop();
162170
$process = new Process($cmd, null, null, array(1 => $tmp));
@@ -168,6 +176,27 @@ public function testReceivesProcessOutputFromStdoutRedirectedToFile()
168176
$this->assertEquals('test', rtrim(stream_get_contents($tmp)));
169177
}
170178

179+
public function testReceivesProcessOutputFromTwoCommandsChainedStdoutRedirectedToFile()
180+
{
181+
$tmp = tmpfile();
182+
183+
if (DIRECTORY_SEPARATOR === '\\') {
184+
// omit whitespace before "&&" and quotation marks as Windows will actually echo this otherwise
185+
$cmd = 'cmd /c echo hello&& cmd /c echo world';
186+
} else {
187+
$cmd = 'echo "hello" && echo "world"';
188+
}
189+
190+
$loop = $this->createLoop();
191+
$process = new Process($cmd, null, null, array(1 => $tmp));
192+
$process->start($loop);
193+
194+
$loop->run();
195+
196+
rewind($tmp);
197+
$this->assertEquals("hello\nworld", str_replace("\r\n", "\n", rtrim(stream_get_contents($tmp))));
198+
}
199+
171200
public function testReceivesProcessOutputFromStdoutAttachedToSocket()
172201
{
173202
if (DIRECTORY_SEPARATOR === '\\') {
@@ -199,9 +228,14 @@ public function testReceivesProcessOutputFromStdoutRedirectedToSocketProcess()
199228
// create TCP/IP server on random port and wait for client connection
200229
$server = stream_socket_server('tcp://127.0.0.1:0');
201230

202-
$cmd = 'echo test';
231+
if (DIRECTORY_SEPARATOR === '\\') {
232+
$cmd = 'cmd /c echo test';
233+
} else {
234+
$cmd = 'exec echo test';
235+
}
236+
203237
$code = '$s=stream_socket_client($argv[1]);do{$d=fread(STDIN,8192);fwrite($s,$d);}while(!feof(STDIN));fclose($s);';
204-
$cmd .= ' | php -r ' . escapeshellarg($code) . ' ' . escapeshellarg(stream_socket_get_name($server, false));
238+
$cmd .= ' | ' . $this->getPhpBinary() . ' -r ' . escapeshellarg($code) . ' ' . escapeshellarg(stream_socket_get_name($server, false));
205239

206240
$loop = $this->createLoop();
207241

@@ -505,7 +539,13 @@ public function testDetectsClosingProcessEvenWhenAllStdioPipesHaveBeenClosed()
505539
public function testDetectsClosingProcessEvenWhenStartedWithoutPipes()
506540
{
507541
$loop = $this->createLoop();
508-
$process = new Process('exit 0', null, null, array());
542+
543+
if (DIRECTORY_SEPARATOR === '\\') {
544+
$process = new Process('cmd /c exit 0', null, null, array());
545+
} else {
546+
$process = new Process('exit 0', null, null, array());
547+
}
548+
509549
$process->start($loop, 0.001);
510550

511551
$time = microtime(true);
@@ -548,10 +588,11 @@ public function testStartAlreadyRunningProcess()
548588
{
549589
if (DIRECTORY_SEPARATOR === '\\') {
550590
// Windows doesn't have a sleep command and also does not support process pipes
551-
$process = new Process('php -r ' . escapeshellarg('sleep(1);'), null, null, array());
591+
$process = new Process($this->getPhpBinary() . ' -r ' . escapeshellarg('sleep(1);'), null, null, array());
552592
} else {
553593
$process = new Process('sleep 1');
554594
}
595+
//var_dump($process);
555596

556597
$process->start($this->createLoop());
557598
$process->start($this->createLoop());
@@ -561,7 +602,7 @@ public function testTerminateProcesWithoutStartingReturnsFalse()
561602
{
562603
if (DIRECTORY_SEPARATOR === '\\') {
563604
// Windows doesn't have a sleep command and also does not support process pipes
564-
$process = new Process('php -r ' . escapeshellarg('sleep(1);'), null, null, array());
605+
$process = new Process($this->getPhpBinary() . ' -r ' . escapeshellarg('sleep(1);'), null, null, array());
565606
} else {
566607
$process = new Process('sleep 1');
567608
}
@@ -573,7 +614,7 @@ public function testTerminateWillExit()
573614
{
574615
if (DIRECTORY_SEPARATOR === '\\') {
575616
// Windows doesn't have a sleep command and also does not support process pipes
576-
$process = new Process('php -r ' . escapeshellarg('sleep(10);'), null, null, array());
617+
$process = new Process($this->getPhpBinary() . ' -r ' . escapeshellarg('sleep(10);'), null, null, array());
577618
} else {
578619
$process = new Process('sleep 10');
579620
}
@@ -771,6 +812,11 @@ public function assertSoon(\Closure $callback, $timeout = 20000, $interval = 200
771812
}
772813
}
773814

815+
/**
816+
* Returns the path to the PHP binary. This is already escapescaped via `escapeshellarg()`.
817+
*
818+
* @return string
819+
*/
774820
private function getPhpBinary()
775821
{
776822
$runtime = new Runtime();

0 commit comments

Comments
 (0)