Codebase list php-react-child-process / f6014e6
Update upstream source from tag 'upstream/0.6.1' Update to upstream version '0.6.1' with Debian dir 91b0a54f75589bd03b5d045e93e122dba5a23644 Dominik George 4 years ago
17 changed file(s) with 1119 addition(s) and 287 deletion(s). Raw diff Collapse all Expand all
66 - 5.6
77 - 7.0
88 - 7.1
9 - hhvm # ignore errors, see below
9 - 7.2
10 - 7.3
11 # - hhvm # requires legacy phpunit & ignore errors, see below
1012
1113 # lock distro so new future defaults will not break the build
1214 dist: trusty
1517 include:
1618 - php: 5.3
1719 dist: precise
20 - php: hhvm
21 install: composer require phpunit/phpunit:^5 --dev --no-interaction
22 - name: "Windows"
23 os: windows
24 language: shell # no built-in php support
25 before_install:
26 - choco install php
27 - choco install composer
28 - export PATH="$(powershell -Command '("Process", "Machine" | % { [Environment]::GetEnvironmentVariable("PATH", $_) -Split ";" -Replace "\\$", "" } | Select -Unique | % { cygpath $_ }) -Join ":"')"
1829 allow_failures:
1930 - php: hhvm
31 - os: windows
2032
2133 sudo: false
2234
2335 install:
2436 - composer install --no-interaction
25
37
2638 script:
2739 - vendor/bin/phpunit --coverage-text
2840 - php examples/13-benchmark-throughput.php
00 # Changelog
1
2 ## 0.6.1 (2019-02-15)
3
4 * Feature / Fix: Improve error reporting when spawning child process fails.
5 (#73 by @clue)
6
7 ## 0.6.0 (2019-01-14)
8
9 A major feature release with some minor API improvements!
10 This project now has limited Windows support and supports passing custom pipes
11 and file descriptors to the child process.
12
13 This update involves a few minor BC breaks. We've tried hard to avoid BC breaks
14 where possible and minimize impact otherwise. We expect that most consumers of
15 this package will actually not be affected by any BC breaks, see below for more
16 details.
17
18 * Feature / BC break: Support passing custom pipes and file descriptors to child process,
19 expose all standard I/O pipes in an array and remove unused Windows-only options.
20 (#62, #64 and #65 by @clue)
21
22 > BC note: The optional `$options` parameter in the `Process` constructor
23 has been removed and a new `$fds` parameter has been added instead. The
24 previous `$options` parameter was Windows-only, available options were not
25 documented or referenced anywhere else in this library, so its actual
26 impact is expected to be relatively small. See the documentation and the
27 following changelog entry if you're looking for Windows support.
28
29 * Feature: Support spawning child process on Windows without process I/O pipes.
30 (#67 by @clue)
31
32 * Feature / BC break: Improve sigchild compatibility and support explicit configuration.
33 (#63 by @clue)
34
35 ```php
36 // advanced: not recommended by default
37 Process::setSigchildEnabled(true);
38 ```
39
40 > BC note: The old public sigchild methods have been removed, but its
41 practical impact is believed to be relatively small due to the automatic detection.
42
43 * Improve performance by prefixing all global functions calls with \ to skip
44 the look up and resolve process and go straight to the global function.
45 (#68 by @WyriHaximus)
46
47 * Minor documentation improvements and docblock updates.
48 (#59 by @iamluc and #69 by @CharlotteDunois)
49
50 * Improve test suite to test against PHP7.2 and PHP 7.3, improve HHVM compatibility,
51 add forward compatibility with PHPUnit 7 and run tests on Windows via Travis CI.
52 (#66 and #71 by @clue)
153
254 ## 0.5.2 (2018-01-18)
355
1818 * [Stream Properties](#stream-properties)
1919 * [Command](#command)
2020 * [Termination](#termination)
21 * [Custom pipes](#custom-pipes)
2122 * [Sigchild Compatibility](#sigchild-compatibility)
2223 * [Windows Compatibility](#windows-compatibility)
2324 * [Install](#install)
5152
5253 Once a process is started, its I/O streams will be constructed as instances of
5354 `React\Stream\ReadableStreamInterface` and `React\Stream\WritableStreamInterface`.
54 Before `start()` is called, these properties are `null`.Once a process terminates,
55 Before `start()` is called, these properties are not set. Once a process terminates,
5556 the streams will become closed but not unset.
5657
57 * `$stdin`
58 * `$stdout`
59 * `$stderr`
60
61 Each of these implement the underlying
58 Following common Unix conventions, this library will start each child process
59 with the three pipes matching the standard I/O streams as given below by default.
60 You can use the named references for common use cases or access these as an
61 array with all three pipes.
62
63 * `$stdin` or `$pipes[0]` is a `WritableStreamInterface`
64 * `$stdout` or `$pipes[1]` is a `ReadableStreamInterface`
65 * `$stderr` or `$pipes[2]` is a `ReadableStreamInterface`
66
67 Note that this default configuration may be overridden by explicitly passing
68 [custom pipes](#custom-pipes), in which case they may not be set or be assigned
69 different values. In particular, note that [Windows support](#windows-compatibility)
70 is limited in that it doesn't support non-blocking STDIO pipes. The `$pipes`
71 array will always contain references to all pipes as configured and the standard
72 I/O references will always be set to reference the pipes matching the above
73 conventions. See [custom pipes](#custom-pipes) for more details.
74
75 Because each of these implement the underlying
6276 [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) or
63 [`WritableStreamInterface`](https://github.com/reactphp/stream#writablestreaminterface) and
77 [`WritableStreamInterface`](https://github.com/reactphp/stream#writablestreaminterface),
6478 you can use any of their events and methods as usual:
6579
6680 ```php
81 $process = new Process($command);
82 $process->start($loop);
83
6784 $process->stdout->on('data', function ($chunk) {
6885 echo $chunk;
6986 });
98115 $process->start($loop);
99116 ```
100117
118 The command line string usually consists of a whitespace-separated list with
119 your main executable bin and any number of arguments. Special care should be
120 taken to escape or quote any arguments, escpecially if you pass any user input
121 along. Likewise, keep in mind that especially on Windows, it is rather common to
122 have path names containing spaces and other special characters. If you want to
123 run a binary like this, you will have to ensure this is quoted as a single
124 argument using `escapeshellarg()` like this:
125
126 ```php
127 $bin = 'C:\\Program files (x86)\\PHP\\php.exe';
128 $file = 'C:\\Users\\me\\Desktop\\Application\\main.php';
129
130 $process = new Process(escapeshellarg($bin) . ' ' . escapeshellarg($file));
131 $process->start($loop);
132 ```
133
101134 By default, PHP will launch processes by wrapping the given command line string
102 in a `sh` command, so that the above example will actually execute
103 `sh -c echo test` under the hood.
135 in a `sh` command on Unix, so that the first example will actually execute
136 `sh -c echo test` under the hood on Unix. On Windows, it will not launch
137 processes by wrapping them in a shell.
104138
105139 This is a very useful feature because it does not only allow you to pass single
106140 commands, but actually allows you to pass any kind of shell command line and
114148 $process->start($loop);
115149 ```
116150
151 > Note that [Windows support](#windows-compatibility) is limited in that it
152 doesn't support STDIO streams at all and also that processes will not be run
153 in a wrapping shell by default. If you want to run a shell built-in function
154 such as `echo hello` or `sleep 10`, you may have to prefix your command line
155 with an explicit shell like `cmd /c echo hello`.
156
117157 In other words, the underlying shell is responsible for managing this command
118158 line and launching the individual sub-commands and connecting their STDIO
119159 streams as appropriate.
144184 });
145185 ```
146186
147 Keep in mind that PHP uses the shell wrapper for ALL command lines.
187 Keep in mind that PHP uses the shell wrapper for ALL command lines on Unix.
148188 While this may seem reasonable for more complex command lines, this actually
149189 also applies to running the most simple single command:
150190
153193 $process->start($loop);
154194 ```
155195
156 This will actually spawn a command hierarchy similar to this:
196 This will actually spawn a command hierarchy similar to this on Unix:
157197
158198 ```
159199 5480 … \_ php example.php
166206 in many cases.
167207
168208 If you do not want this wrapping shell process to show up, you can simply
169 prepend the command string with `exec`, which will cause the wrapping shell
170 process to be replaced by our process:
209 prepend the command string with `exec` on Unix platforms, which will cause the
210 wrapping shell process to be replaced by our process:
171211
172212 ```php
173213 $process = new Process('exec yes');
192232 shell.
193233 If you pass a complete command line (or are unsure), you SHOULD most likely keep
194234 the wrapping shell.
195 If you want to pass an invidual command only, you MAY want to consider
196 prepending the command string with `exec` to avoid the wrapping shell.
235 If you're running on Unix and you want to pass an invidual command only, you MAY
236 want to consider prepending the command string with `exec` to avoid the wrapping shell.
197237
198238 ### Termination
199239
254294 $process->start($loop);
255295
256296 $loop->addTimer(2.0, function () use ($process) {
257 $process->stdin->close();
258 $process->stout->close();
259 $process->stderr->close();
260 $process->terminate(SIGKILL);
297 foreach ($process->pipes as $pipe) {
298 $pipe->close();
299 }
300 $process->terminate();
261301 });
262302 ```
263303
292332 such as first trying a soft-close and then applying a force-close after a
293333 timeout.
294334
335 ### Custom pipes
336
337 Following common Unix conventions, this library will start each child process
338 with the three pipes matching the standard I/O streams by default. For more
339 advanced use cases it may be useful to pass in custom pipes, such as explicitly
340 passing additional file descriptors (FDs) or overriding default process pipes.
341
342 Note that passing custom pipes is considered advanced usage and requires a
343 more in-depth understanding of Unix file descriptors and how they are inherited
344 to child processes and shared in multi-processing applications.
345
346 If you do not want to use the default standard I/O pipes, you can explicitly
347 pass an array containing the file descriptor specification to the constructor
348 like this:
349
350 ```php
351 $fds = array(
352 // standard I/O pipes for stdin/stdout/stderr
353 0 => array('pipe', 'r'),
354 1 => array('pipe', 'w'),
355 2 => array('pipe', 'w'),
356
357 // example FDs for files or open resources
358 4 => array('file', '/dev/null', 'r'),
359 6 => fopen('log.txt','a'),
360 8 => STDERR,
361
362 // example FDs for sockets
363 10 => fsockopen('localhost', 8080),
364 12 => stream_socket_server('tcp://0.0.0.0:4711')
365 );
366
367 $process = new Process($cmd, null, null, $fds);
368 $process->start($loop);
369 ```
370
371 Unless your use case has special requirements that demand otherwise, you're
372 highly recommended to (at least) pass in the standard I/O pipes as given above.
373 The file descriptor specification accepts arguments in the exact same format
374 as the underlying [`proc_open()`](http://php.net/proc_open) function.
375
376 Once the process is started, the `$pipes` array will always contain references to
377 all pipes as configured and the standard I/O references will always be set to
378 reference the pipes matching common Unix conventions. This library supports any
379 number of pipes and additional file descriptors, but many common applications
380 being run as a child process will expect that the parent process properly
381 assigns these file descriptors.
382
295383 ### Sigchild Compatibility
296384
297 When PHP has been compiled with the `--enabled-sigchild` option, a child
298 process' exit code cannot be reliably determined via `proc_close()` or
299 `proc_get_status()`. Instead, we execute the child process with a fourth pipe
300 and use that to retrieve its exit code.
301
302 This behavior is used by default and only when necessary. It may be manually
303 disabled by calling `setEnhanceSigchildCompatibility(false)` on the Process
304 before it is started, in which case the `exit` event may receive `null` instead
305 of the actual exit code.
306
307 **Note:** This functionality was taken from Symfony's
385 Internally, this project uses a work-around to improve compatibility when PHP
386 has been compiled with the `--enable-sigchild` option. This should not affect most
387 installations as this configure option is not used by default and many
388 distributions (such as Debian and Ubuntu) are known to not use this by default.
389 Some installations that use [Oracle OCI8](http://php.net/manual/en/book.oci8.php)
390 may use this configure option to circumvent `defunct` processes.
391
392 When PHP has been compiled with the `--enable-sigchild` option, a child process'
393 exit code cannot be reliably determined via `proc_close()` or `proc_get_status()`.
394 To work around this, we execute the child process with an additional pipe and
395 use that to retrieve its exit code.
396
397 This work-around incurs some overhead, so we only trigger this when necessary
398 and when we detect that PHP has been compiled with the `--enable-sigchild` option.
399 Because PHP does not provide a way to reliably detect this option, we try to
400 inspect output of PHP's configure options from the `phpinfo()` function.
401
402 The static `setSigchildEnabled(bool $sigchild): void` method can be used to
403 explicitly enable or disable this behavior like this:
404
405 ```php
406 // advanced: not recommended by default
407 Process::setSigchildEnabled(true);
408 ```
409
410 Note that all processes instantiated after this method call will be affected.
411 If this work-around is disabled on an affected PHP installation, the `exit`
412 event may receive `null` instead of the actual exit code as described above.
413 Similarly, some distributions are known to omit the configure options from
414 `phpinfo()`, so automatic detection may fail to enable this work-around in some
415 cases. You may then enable this explicitly as given above.
416
417 **Note:** The original functionality was taken from Symfony's
308418 [Process](https://github.com/symfony/process) compoment.
309419
310420 ### Windows Compatibility
311421
312 Due to the blocking nature of `STDIN`/`STDOUT`/`STDERR` pipes on Windows we can
313 not guarantee this package works as expected on Windows directly. As such when
314 instantiating `Process` it throws an exception when on native Windows.
315 However this package does work on [`Windows Subsystem for Linux`](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux)
316 (or WSL) without issues. We suggest [installing WSL](https://msdn.microsoft.com/en-us/commandline/wsl/install_guide)
317 when you want to run this package on Windows.
422 Due to platform constraints, this library provides only limited support for
423 spawning child processes on Windows. In particular, PHP does not allow accessing
424 standard I/O pipes without blocking. As such, this project will not allow
425 constructing a child process with the default process pipes and will instead
426 throw a `LogicException` on Windows by default:
427
428 ```php
429 // throws LogicException on Windows
430 $process = new Process('ping example.com');
431 $process->start($loop);
432 ```
433
434 There are a number of alternatives and workarounds as detailed below if you want
435 to run a child process on Windows, each with its own set of pros and cons:
436
437 * This package does work on
438 [`Windows Subsystem for Linux`](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux)
439 (or WSL) without issues. When you are in control over how your application is
440 deployed, we recommend [installing WSL](https://msdn.microsoft.com/en-us/commandline/wsl/install_guide)
441 when you want to run this package on Windows.
442
443 * If you only care about the exit code of a child process to check if its
444 execution was successful, you can use [custom pipes](#custom-pipes) to omit
445 any standard I/O pipes like this:
446
447 ```php
448 $process = new Process('ping example.com', null, null, array());
449 $process->start($loop);
450
451 $process->on('exit', function ($exitcode) {
452 echo 'exit with ' . $exitcode . PHP_EOL;
453 });
454 ```
455
456 Similarly, this is also useful if your child process communicates over
457 sockets with remote servers or even your parent process using the
458 [Socket component](https://github.com/reactphp/socket). This is usually
459 considered the best alternative if you have control over how your child
460 process communicates with the parent process.
461
462 * If you only care about command output after the child process has been
463 executed, you can use [custom pipes](#custom-pipes) to configure file
464 handles to be passed to the child process instead of pipes like this:
465
466 ```php
467 $process = new Process('ping example.com', null, null, array(
468 array('file', 'nul', 'r'),
469 $stdout = tmpfile(),
470 array('file', 'nul', 'w')
471 ));
472 $process->start($loop);
473
474 $process->on('exit', function ($exitcode) use ($stdout) {
475 echo 'exit with ' . $exitcode . PHP_EOL;
476
477 // rewind to start and then read full file (demo only, this is blocking).
478 // reading from shared file is only safe if you have some synchronization in place
479 // or after the child process has terminated.
480 rewind($stdout);
481 echo stream_get_contents($stdout);
482 fclose($stdout);
483 });
484 ```
485
486 Note that this example uses `tmpfile()`/`fopen()` for illustration purposes only.
487 This should not be used in a truly async program because the filesystem is
488 inherently blocking and each call could potentially take several seconds.
489 See also the [Filesystem component](https://github.com/reactphp/filesystem) as an
490 alternative.
491
492 * If you want to access command output as it happens in a streaming fashion,
493 you can use redirection to spawn an additional process to forward your
494 standard I/O streams to a socket and use [custom pipes](#custom-pipes) to
495 omit any actual standard I/O pipes like this:
496
497 ```php
498 $server = new React\Socket\Server('127.0.0.1:0', $loop);
499 $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
500 $connection->on('data', function ($chunk) {
501 echo $chunk;
502 });
503 });
504
505 $command = 'ping example.com | foobar ' . escapeshellarg($server->getAddress());
506 $process = new Process($command, null, null, array());
507 $process->start($loop);
508
509 $process->on('exit', function ($exitcode) use ($server) {
510 $server->close();
511 echo 'exit with ' . $exitcode . PHP_EOL;
512 });
513 ```
514
515 Note how this will spawn another fictional `foobar` helper program to consume
516 the standard output from the actual child process. This is in fact similar
517 to the above recommendation of using socket connections in the child process,
518 but in this case does not require modification of the actual child process.
519
520 In this example, the fictional `foobar` helper program can be implemented by
521 simply consuming all data from standard input and forwarding it to a socket
522 connection like this:
523
524 ```php
525 $socket = stream_socket_client($argv[1]);
526 do {
527 fwrite($socket, $data = fread(STDIN, 8192));
528 } while (isset($data[0]));
529 ```
530
531 Accordingly, this example can also be run with plain PHP without having to
532 rely on any external helper program like this:
533
534 ```php
535 $code = '$s=stream_socket_client($argv[1]);do{fwrite($s,$d=fread(STDIN, 8192));}while(isset($d[0]));';
536 $command = 'ping example.com | php -r ' . escapeshellarg($code) . ' ' . escapeshellarg($server->getAddress());
537 $process = new Process($command, null, null, array());
538 $process->start($loop);
539 ```
540
541 See also [example #23](examples/23-forward-socket.php).
542
543 Note that this is for illustration purposes only and you may want to implement
544 some proper error checks and/or socket verification in actual production use
545 if you do not want to risk other processes connecting to the server socket.
546 In this case, we suggest looking at the excellent
547 [createprocess-windows](https://github.com/cubiclesoft/createprocess-windows).
548
549 Additionally, note that the [command](#command) given to the `Process` will be
550 passed to the underlying Windows-API
551 ([`CreateProcess`](https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-createprocessa))
552 as-is and the process will not be launched in a wrapping shell by default. In
553 particular, this means that shell built-in functions such as `echo hello` or
554 `sleep 10` may have to be prefixed with an explicit shell command like this:
555
556 ```php
557 $process = new Process('cmd /c echo hello', null, null, $pipes);
558 $process->start($loop);
559 ```
318560
319561 ## Install
320562
324566 This will install the latest supported version:
325567
326568 ```bash
327 $ composer require react/child-process:^0.5.2
569 $ composer require react/child-process:^0.6.1
328570 ```
329571
330572 See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
99 "react/stream": "^1.0 || ^0.7.6"
1010 },
1111 "require-dev": {
12 "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35",
12 "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35",
13 "react/socket": "^1.0",
1314 "sebastian/environment": "^3.0 || ^2.0 || ^1.0"
1415 },
1516 "autoload": {
33 use React\ChildProcess\Process;
44
55 require __DIR__ . '/../vendor/autoload.php';
6
7 if (DIRECTORY_SEPARATOR === '\\') {
8 exit('Process pipes not supported on Windows' . PHP_EOL);
9 }
610
711 $loop = Factory::create();
812
33 use React\ChildProcess\Process;
44
55 require __DIR__ . '/../vendor/autoload.php';
6
7 if (DIRECTORY_SEPARATOR === '\\') {
8 exit('Process pipes not supported on Windows' . PHP_EOL);
9 }
610
711 $loop = Factory::create();
812
33 use React\ChildProcess\Process;
44
55 require __DIR__ . '/../vendor/autoload.php';
6
7 if (DIRECTORY_SEPARATOR === '\\') {
8 exit('Process pipes not supported on Windows' . PHP_EOL);
9 }
610
711 $loop = Factory::create();
812
77 $loop = Factory::create();
88
99 // start a process that takes 10s to terminate
10 $process = new Process('sleep 10');
10 $process = new Process('php -r "sleep(10);"', null, null, array());
1111 $process->start($loop);
1212
1313 // report when process exits
1717
1818 // forcefully terminate process after 2s
1919 $loop->addTimer(2.0, function () use ($process) {
20 $process->stdin->close();
21 $process->stdout->close();
22 $process->stderr->close();
20 foreach ($process->pipes as $pipe) {
21 $pipe->close();
22 }
2323 $process->terminate();
2424 });
2525
77 use React\ChildProcess\Process;
88
99 require __DIR__ . '/../vendor/autoload.php';
10
11 if (DIRECTORY_SEPARATOR === '\\') {
12 exit('Process pipes not supported on Windows' . PHP_EOL);
13 }
1014
1115 $cmd = isset($argv[1]) ? implode(' ', array_slice($argv, 1)) : 'dd if=/dev/zero bs=1M count=1000';
1216
11
22 use React\EventLoop\Factory;
33 use React\ChildProcess\Process;
4 use React\Stream\Stream;
54
65 require __DIR__ . '/../vendor/autoload.php';
6
7 if (DIRECTORY_SEPARATOR === '\\') {
8 exit('Process pipes not supported on Windows' . PHP_EOL);
9 }
710
811 $loop = Factory::create();
912
11
22 use React\EventLoop\Factory;
33 use React\ChildProcess\Process;
4 use React\Stream\Stream;
54
65 require __DIR__ . '/../vendor/autoload.php';
6
7 if (DIRECTORY_SEPARATOR === '\\') {
8 exit('Process pipes not supported on Windows' . PHP_EOL);
9 }
710
811 $loop = Factory::create();
912
0 <?php
1
2 use React\EventLoop\Factory;
3 use React\ChildProcess\Process;
4
5 require __DIR__ . '/../vendor/autoload.php';
6
7 if (DIRECTORY_SEPARATOR === '\\') {
8 exit('Process pipes not supported on Windows' . PHP_EOL);
9 }
10
11 $loop = Factory::create();
12
13 $process = new Process('exec 0>&- 2>&-;exec ls -la /proc/self/fd', null, null, array(
14 1 => array('pipe', 'w')
15 ));
16 $process->start($loop);
17
18 $process->stdout->on('data', function ($chunk) {
19 echo $chunk;
20 });
21
22 $process->on('exit', function ($code) {
23 echo 'EXIT with code ' . $code . PHP_EOL;
24 });
25
26 $loop->run();
0 <?php
1
2 use React\EventLoop\Factory;
3 use React\ChildProcess\Process;
4
5 require __DIR__ . '/../vendor/autoload.php';
6
7 $loop = Factory::create();
8
9 $first = new Process('php -r "sleep(2);"', null, null, array());
10 $first->start($loop);
11
12 $first->on('exit', function ($code) {
13 echo 'First closed ' . $code . PHP_EOL;
14 });
15
16 $second = new Process('php -r "sleep(1);"', null, null, array());
17 $second->start($loop);
18
19 $second->on('exit', function ($code) {
20 echo 'Second closed ' . $code . PHP_EOL;
21 });
22
23 $loop->run();
0 <?php
1
2 use React\EventLoop\Factory;
3 use React\ChildProcess\Process;
4
5 require __DIR__ . '/../vendor/autoload.php';
6
7 $loop = Factory::create();
8
9 $server = new React\Socket\Server('127.0.0.1:0', $loop);
10 $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
11 $connection->on('data', function ($chunk) {
12 // escape control codes (useful in case encoding or binary data is not working as expected)
13 // $chunk = addcslashes($chunk,"\0..\37!@\177..\377");
14
15 // convert default code page 850 to UTF-8 (German Windows in this case)
16 $chunk = iconv('CP850','UTF-8', $chunk);
17
18 echo $chunk;
19 });
20 });
21
22 $command = 'php -r "echo 1;sleep(1);echo 2;sleep(1);echo 3;"';
23 // $command = 'ping google.com';
24 // $command = 'C:\Windows\System32\ping google.com';
25
26 // use stream redirections to consume output of child process in another helper process and forward to socket
27 $code = '$s=stream_socket_client($argv[1]);do{fwrite($s,$d=fread(STDIN, 8192));}while(isset($d[0]));';
28 $process = new Process(
29 $command . ' | php -r ' . escapeshellarg($code) . ' ' . $server->getAddress(),
30 null,
31 null,
32 array()
33 );
34 $process->start($loop);
35
36 $process->on('exit', function ($code) use ($server) {
37 $server->close();
38 echo PHP_EOL . 'Process closed ' . $code . PHP_EOL;
39 });
40
41 $loop->run();
77 convertWarningsToExceptions="true"
88 processIsolation="false"
99 stopOnFailure="false"
10 syntaxCheck="false"
1110 bootstrap="vendor/autoload.php"
1211 >
1312 <testsuites>
44 use Evenement\EventEmitter;
55 use React\EventLoop\LoopInterface;
66 use React\Stream\ReadableResourceStream;
7 use React\Stream\ReadableStreamInterface;
78 use React\Stream\WritableResourceStream;
9 use React\Stream\WritableStreamInterface;
810
911 /**
1012 * Process component.
1214 * This class borrows logic from Symfony's Process component for ensuring
1315 * compatibility when PHP is compiled with the --enable-sigchild option.
1416 *
15 * @event exit
17 * This class also implements the `EventEmitterInterface`
18 * which allows you to react to certain events:
19 *
20 * exit event:
21 * The `exit` event will be emitted whenever the process is no longer running.
22 * Event listeners will receive the exit code and termination signal as two
23 * arguments:
24 *
25 * ```php
26 * $process = new Process('sleep 10');
27 * $process->start($loop);
28 *
29 * $process->on('exit', function ($code, $term) {
30 * if ($term === null) {
31 * echo 'exit with code ' . $code . PHP_EOL;
32 * } else {
33 * echo 'terminated with signal ' . $term . PHP_EOL;
34 * }
35 * });
36 * ```
37 *
38 * Note that `$code` is `null` if the process has terminated, but the exit
39 * code could not be determined (for example
40 * [sigchild compatibility](#sigchild-compatibility) was disabled).
41 * Similarly, `$term` is `null` unless the process has terminated in response to
42 * an uncaught signal sent to it.
43 * This is not a limitation of this project, but actual how exit codes and signals
44 * are exposed on POSIX systems, for more details see also
45 * [here](https://unix.stackexchange.com/questions/99112/default-exit-code-when-process-is-terminated).
46 *
47 * It's also worth noting that process termination depends on all file descriptors
48 * being closed beforehand.
49 * This means that all [process pipes](#stream-properties) will emit a `close`
50 * event before the `exit` event and that no more `data` events will arrive after
51 * the `exit` event.
52 * Accordingly, if either of these pipes is in a paused state (`pause()` method
53 * or internally due to a `pipe()` call), this detection may not trigger.
1654 */
1755 class Process extends EventEmitter
1856 {
57 /**
58 * @var WritableStreamInterface|null|ReadableStreamInterface
59 */
1960 public $stdin;
61
62 /**
63 * @var ReadableStreamInterface|null|WritableStreamInterface
64 */
2065 public $stdout;
66
67 /**
68 * @var ReadableStreamInterface|null|WritableStreamInterface
69 */
2170 public $stderr;
71
72 /**
73 * Array with all process pipes (once started)
74 *
75 * Unless explicitly configured otherwise during construction, the following
76 * standard I/O pipes will be assigned by default:
77 * - 0: STDIN (`WritableStreamInterface`)
78 * - 1: STDOUT (`ReadableStreamInterface`)
79 * - 2: STDERR (`ReadableStreamInterface`)
80 *
81 * @var ReadableStreamInterface|WritableStreamInterface
82 */
83 public $pipes = array();
2284
2385 private $cmd;
2486 private $cwd;
2587 private $env;
26 private $options;
88 private $fds;
89
2790 private $enhanceSigchildCompatibility;
28 private $pipes;
91 private $sigchildPipe;
2992
3093 private $process;
3194 private $status;
39102 /**
40103 * Constructor.
41104 *
42 * @param string $cmd Command line to run
43 * @param string $cwd Current working directory or null to inherit
44 * @param array $env Environment variables or null to inherit
45 * @param array $options Options for proc_open()
46 * @throws LogicException On windows or when proc_open() is not installed
105 * @param string $cmd Command line to run
106 * @param null|string $cwd Current working directory or null to inherit
107 * @param null|array $env Environment variables or null to inherit
108 * @param null|array $fds File descriptors to allocate for this process (or null = default STDIO streams)
109 * @throws \LogicException On windows or when proc_open() is not installed
47110 */
48 public function __construct($cmd, $cwd = null, array $env = null, array $options = array())
49 {
50 if (substr(strtolower(PHP_OS), 0, 3) === 'win') {
51 throw new \LogicException('Windows isn\'t supported due to the blocking nature of STDIN/STDOUT/STDERR pipes.');
52 }
53
54 if (!function_exists('proc_open')) {
111 public function __construct($cmd, $cwd = null, array $env = null, array $fds = null)
112 {
113 if (!\function_exists('proc_open')) {
55114 throw new \LogicException('The Process class relies on proc_open(), which is not available on your PHP installation.');
56115 }
57116
65124 }
66125 }
67126
68 $this->options = $options;
69 $this->enhanceSigchildCompatibility = $this->isSigchildEnabled();
127 if ($fds === null) {
128 $fds = array(
129 array('pipe', 'r'), // stdin
130 array('pipe', 'w'), // stdout
131 array('pipe', 'w'), // stderr
132 );
133 }
134
135 if (\DIRECTORY_SEPARATOR === '\\') {
136 foreach ($fds as $fd) {
137 if (isset($fd[0]) && $fd[0] === 'pipe') {
138 throw new \LogicException('Process pipes are not supported on Windows due to their blocking nature on Windows');
139 }
140 }
141 }
142
143 $this->fds = $fds;
144 $this->enhanceSigchildCompatibility = self::isSigchildEnabled();
70145 }
71146
72147 /**
73148 * Start the process.
74149 *
75 * After the process is started, the standard IO streams will be constructed
76 * and available via public properties. STDIN will be paused upon creation.
150 * After the process is started, the standard I/O streams will be constructed
151 * and available via public properties.
77152 *
78153 * @param LoopInterface $loop Loop interface for stream construction
79154 * @param float $interval Interval to periodically monitor process state (seconds)
80 * @throws RuntimeException If the process is already running or fails to start
155 * @throws \RuntimeException If the process is already running or fails to start
81156 */
82157 public function start(LoopInterface $loop, $interval = 0.1)
83158 {
86161 }
87162
88163 $cmd = $this->cmd;
89 $fdSpec = array(
90 array('pipe', 'r'), // stdin
91 array('pipe', 'w'), // stdout
92 array('pipe', 'w'), // stderr
93 );
164 $fdSpec = $this->fds;
165 $sigchild = null;
94166
95167 // Read exit code through fourth pipe to work around --enable-sigchild
96 if ($this->isSigchildEnabled() && $this->enhanceSigchildCompatibility) {
168 if ($this->enhanceSigchildCompatibility) {
97169 $fdSpec[] = array('pipe', 'w');
98 $cmd = sprintf('(%s) 3>/dev/null; code=$?; echo $code >&3; exit $code', $cmd);
99 }
100
101 $this->process = proc_open($cmd, $fdSpec, $this->pipes, $this->cwd, $this->env, $this->options);
102
103 if (!is_resource($this->process)) {
104 throw new \RuntimeException('Unable to launch a new process.');
105 }
106
170 \end($fdSpec);
171 $sigchild = \key($fdSpec);
172
173 // make sure this is fourth or higher (do not mess with STDIO)
174 if ($sigchild < 3) {
175 $fdSpec[3] = $fdSpec[$sigchild];
176 unset($fdSpec[$sigchild]);
177 $sigchild = 3;
178 }
179
180 $cmd = \sprintf('(%s) ' . $sigchild . '>/dev/null; code=$?; echo $code >&' . $sigchild . '; exit $code', $cmd);
181 }
182
183 // on Windows, we do not launch the given command line in a shell (cmd.exe) by default and omit any error dialogs
184 // the cmd.exe shell can explicitly be given as part of the command as detailed in both documentation and tests
185 $options = array();
186 if (\DIRECTORY_SEPARATOR === '\\') {
187 $options['bypass_shell'] = true;
188 $options['suppress_errors'] = true;
189 }
190
191 $this->process = @\proc_open($cmd, $fdSpec, $pipes, $this->cwd, $this->env, $options);
192
193 if (!\is_resource($this->process)) {
194 $error = \error_get_last();
195 throw new \RuntimeException('Unable to launch a new process: ' . $error['message']);
196 }
197
198 // count open process pipes and await close event for each to drain buffers before detecting exit
199 $that = $this;
107200 $closeCount = 0;
108
109 $that = $this;
110201 $streamCloseHandler = function () use (&$closeCount, $loop, $interval, $that) {
111 $closeCount++;
112
113 if ($closeCount < 2) {
202 $closeCount--;
203
204 if ($closeCount > 0) {
114205 return;
115206 }
116207
131222 });
132223 };
133224
134 $this->stdin = new WritableResourceStream($this->pipes[0], $loop);
135 $this->stdout = new ReadableResourceStream($this->pipes[1], $loop);
136 $this->stdout->on('close', $streamCloseHandler);
137 $this->stderr = new ReadableResourceStream($this->pipes[2], $loop);
138 $this->stderr->on('close', $streamCloseHandler);
225 if ($sigchild !== null) {
226 $this->sigchildPipe = $pipes[$sigchild];
227 unset($pipes[$sigchild]);
228 }
229
230 foreach ($pipes as $n => $fd) {
231 if (\strpos($this->fds[$n][1], 'w') === false) {
232 $stream = new WritableResourceStream($fd, $loop);
233 } else {
234 $stream = new ReadableResourceStream($fd, $loop);
235 $stream->on('close', $streamCloseHandler);
236 $closeCount++;
237 }
238 $this->pipes[$n] = $stream;
239 }
240
241 $this->stdin = isset($this->pipes[0]) ? $this->pipes[0] : null;
242 $this->stdout = isset($this->pipes[1]) ? $this->pipes[1] : null;
243 $this->stderr = isset($this->pipes[2]) ? $this->pipes[2] : null;
244
245 // immediately start checking for process exit when started without any I/O pipes
246 if (!$closeCount) {
247 $streamCloseHandler();
248 }
139249 }
140250
141251 /**
150260 return;
151261 }
152262
153 $this->stdin->close();
154 $this->stdout->close();
155 $this->stderr->close();
156
157 if ($this->isSigchildEnabled() && $this->enhanceSigchildCompatibility) {
263 foreach ($this->pipes as $pipe) {
264 $pipe->close();
265 }
266
267 if ($this->enhanceSigchildCompatibility) {
158268 $this->pollExitCodePipe();
159269 $this->closeExitCodePipe();
160270 }
161271
162 $exitCode = proc_close($this->process);
272 $exitCode = \proc_close($this->process);
163273 $this->process = null;
164274
165275 if ($this->exitCode === null && $exitCode !== -1) {
180290 * Terminate the process with an optional signal.
181291 *
182292 * @param int $signal Optional signal (default: SIGTERM)
183 * @return boolean Whether the signal was sent successfully
293 * @return bool Whether the signal was sent successfully
184294 */
185295 public function terminate($signal = null)
186296 {
189299 }
190300
191301 if ($signal !== null) {
192 return proc_terminate($this->process, $signal);
193 }
194
195 return proc_terminate($this->process);
302 return \proc_terminate($this->process, $signal);
303 }
304
305 return \proc_terminate($this->process);
196306 }
197307
198308 /**
206316 }
207317
208318 /**
209 * Return whether sigchild compatibility is enabled.
210 *
211 * @return boolean
212 */
213 public final function getEnhanceSigchildCompatibility()
214 {
215 return $this->enhanceSigchildCompatibility;
319 * Get the exit code returned by the process.
320 *
321 * This value is only meaningful if isRunning() has returned false. Null
322 * will be returned if the process is still running.
323 *
324 * Null may also be returned if the process has terminated, but the exit
325 * code could not be determined (e.g. sigchild compatibility was disabled).
326 *
327 * @return int|null
328 */
329 public function getExitCode()
330 {
331 return $this->exitCode;
332 }
333
334 /**
335 * Get the process ID.
336 *
337 * @return int|null
338 */
339 public function getPid()
340 {
341 $status = $this->getCachedStatus();
342
343 return $status !== null ? $status['pid'] : null;
344 }
345
346 /**
347 * Get the signal that caused the process to stop its execution.
348 *
349 * This value is only meaningful if isStopped() has returned true. Null will
350 * be returned if the process was never stopped.
351 *
352 * @return int|null
353 */
354 public function getStopSignal()
355 {
356 return $this->stopSignal;
357 }
358
359 /**
360 * Get the signal that caused the process to terminate its execution.
361 *
362 * This value is only meaningful if isTerminated() has returned true. Null
363 * will be returned if the process was never terminated.
364 *
365 * @return int|null
366 */
367 public function getTermSignal()
368 {
369 return $this->termSignal;
370 }
371
372 /**
373 * Return whether the process is still running.
374 *
375 * @return bool
376 */
377 public function isRunning()
378 {
379 if ($this->process === null) {
380 return false;
381 }
382
383 $status = $this->getFreshStatus();
384
385 return $status !== null ? $status['running'] : false;
386 }
387
388 /**
389 * Return whether the process has been stopped by a signal.
390 *
391 * @return bool
392 */
393 public function isStopped()
394 {
395 $status = $this->getFreshStatus();
396
397 return $status !== null ? $status['stopped'] : false;
398 }
399
400 /**
401 * Return whether the process has been terminated by an uncaught signal.
402 *
403 * @return bool
404 */
405 public function isTerminated()
406 {
407 $status = $this->getFreshStatus();
408
409 return $status !== null ? $status['signaled'] : false;
410 }
411
412 /**
413 * Return whether PHP has been compiled with the '--enable-sigchild' option.
414 *
415 * @see \Symfony\Component\Process\Process::isSigchildEnabled()
416 * @return bool
417 */
418 public final static function isSigchildEnabled()
419 {
420 if (null !== self::$sigchild) {
421 return self::$sigchild;
422 }
423
424 \ob_start();
425 \phpinfo(INFO_GENERAL);
426
427 return self::$sigchild = false !== \strpos(\ob_get_clean(), '--enable-sigchild');
216428 }
217429
218430 /**
222434 * determine the success of a process when PHP has been compiled with
223435 * the --enable-sigchild option.
224436 *
225 * @param boolean $enhance
226 * @return self
227 * @throws RuntimeException If the process is already running
228 */
229 public final function setEnhanceSigchildCompatibility($enhance)
230 {
231 if ($this->isRunning()) {
232 throw new \RuntimeException('Process is already running');
233 }
234
235 $this->enhanceSigchildCompatibility = (bool) $enhance;
236
237 return $this;
238 }
239
240 /**
241 * Get the exit code returned by the process.
242 *
243 * This value is only meaningful if isRunning() has returned false. Null
244 * will be returned if the process is still running.
245 *
246 * Null may also be returned if the process has terminated, but the exit
247 * code could not be determined (e.g. sigchild compatibility was disabled).
248 *
249 * @return int|null
250 */
251 public function getExitCode()
252 {
253 return $this->exitCode;
254 }
255
256 /**
257 * Get the process ID.
258 *
259 * @return int|null
260 */
261 public function getPid()
262 {
263 $status = $this->getCachedStatus();
264
265 return $status !== null ? $status['pid'] : null;
266 }
267
268 /**
269 * Get the signal that caused the process to stop its execution.
270 *
271 * This value is only meaningful if isStopped() has returned true. Null will
272 * be returned if the process was never stopped.
273 *
274 * @return int|null
275 */
276 public function getStopSignal()
277 {
278 return $this->stopSignal;
279 }
280
281 /**
282 * Get the signal that caused the process to terminate its execution.
283 *
284 * This value is only meaningful if isTerminated() has returned true. Null
285 * will be returned if the process was never terminated.
286 *
287 * @return int|null
288 */
289 public function getTermSignal()
290 {
291 return $this->termSignal;
292 }
293
294 /**
295 * Return whether the process is still running.
296 *
297 * @return boolean
298 */
299 public function isRunning()
300 {
301 if ($this->process === null) {
302 return false;
303 }
304
305 $status = $this->getFreshStatus();
306
307 return $status !== null ? $status['running'] : false;
308 }
309
310 /**
311 * Return whether the process has been stopped by a signal.
312 *
313 * @return boolean
314 */
315 public function isStopped()
316 {
317 $status = $this->getFreshStatus();
318
319 return $status !== null ? $status['stopped'] : false;
320 }
321
322 /**
323 * Return whether the process has been terminated by an uncaught signal.
324 *
325 * @return boolean
326 */
327 public function isTerminated()
328 {
329 $status = $this->getFreshStatus();
330
331 return $status !== null ? $status['signaled'] : false;
332 }
333
334 /**
335 * Return whether PHP has been compiled with the '--enable-sigchild' option.
336 *
337 * @see \Symfony\Component\Process\Process::isSigchildEnabled()
338 * @return bool
339 */
340 public final static function isSigchildEnabled()
341 {
342 if (null !== self::$sigchild) {
343 return self::$sigchild;
344 }
345
346 ob_start();
347 phpinfo(INFO_GENERAL);
348
349 return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
437 * @param bool $sigchild
438 * @return void
439 */
440 public final static function setSigchildEnabled($sigchild)
441 {
442 self::$sigchild = (bool) $sigchild;
350443 }
351444
352445 /**
356449 */
357450 private function pollExitCodePipe()
358451 {
359 if ( ! isset($this->pipes[3])) {
452 if ($this->sigchildPipe === null) {
360453 return;
361454 }
362455
363 $r = array($this->pipes[3]);
456 $r = array($this->sigchildPipe);
364457 $w = $e = null;
365458
366 $n = @stream_select($r, $w, $e, 0);
459 $n = @\stream_select($r, $w, $e, 0);
367460
368461 if (1 !== $n) {
369462 return;
370463 }
371464
372 $data = fread($r[0], 8192);
373
374 if (strlen($data) > 0) {
465 $data = \fread($r[0], 8192);
466
467 if (\strlen($data) > 0) {
375468 $this->fallbackExitCode = (int) $data;
376469 }
377470 }
383476 */
384477 private function closeExitCodePipe()
385478 {
386 if ( ! isset($this->pipes[3])) {
479 if ($this->sigchildPipe === null) {
387480 return;
388481 }
389482
390 fclose($this->pipes[3]);
391 unset($this->pipes[3]);
483 \fclose($this->sigchildPipe);
484 $this->sigchildPipe = null;
392485 }
393486
394487 /**
430523 return;
431524 }
432525
433 $this->status = proc_get_status($this->process);
526 $this->status = \proc_get_status($this->process);
434527
435528 if ($this->status === false) {
436529 throw new \UnexpectedValueException('proc_get_status() failed');
1010 {
1111 abstract public function createLoop();
1212
13 public function testGetEnhanceSigchildCompatibility()
14 {
13 public function testGetCommand()
14 {
15 $process = new Process('echo foo', null, null, array());
16
17 $this->assertSame('echo foo', $process->getCommand());
18 }
19
20 public function testPipesWillBeUnsetBeforeStarting()
21 {
22 if (DIRECTORY_SEPARATOR === '\\') {
23 $this->markTestSkipped('Process pipes not supported on Windows');
24 }
25
1526 $process = new Process('echo foo');
1627
17 $this->assertSame($process, $process->setEnhanceSigchildCompatibility(true));
18 $this->assertTrue($process->getEnhanceSigchildCompatibility());
19
20 $this->assertSame($process, $process->setEnhanceSigchildCompatibility(false));
21 $this->assertFalse($process->getEnhanceSigchildCompatibility());
28 $this->assertNull($process->stdin);
29 $this->assertNull($process->stdout);
30 $this->assertNull($process->stderr);
31 $this->assertEquals(array(), $process->pipes);
32 }
33
34 /**
35 * @depends testPipesWillBeUnsetBeforeStarting
36 */
37 public function testStartWillAssignPipes()
38 {
39 $process = new Process('echo foo');
40 $process->start($this->createLoop());
41
42 $this->assertInstanceOf('React\Stream\WritableStreamInterface', $process->stdin);
43 $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $process->stdout);
44 $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $process->stderr);
45 $this->assertCount(3, $process->pipes);
46 $this->assertSame($process->stdin, $process->pipes[0]);
47 $this->assertSame($process->stdout, $process->pipes[1]);
48 $this->assertSame($process->stderr, $process->pipes[2]);
49 }
50
51 public function testStartWithoutAnyPipesWillNotAssignPipes()
52 {
53 if (DIRECTORY_SEPARATOR === '\\') {
54 $process = new Process('cmd /c exit 0', null, null, array());
55 } else {
56 $process = new Process('exit 0', null, null, array());
57 }
58 $process->start($this->createLoop());
59
60 $this->assertNull($process->stdin);
61 $this->assertNull($process->stdout);
62 $this->assertNull($process->stderr);
63 $this->assertEquals(array(), $process->pipes);
64 }
65
66 public function testStartWithCustomPipesWillAssignPipes()
67 {
68 if (DIRECTORY_SEPARATOR === '\\') {
69 $this->markTestSkipped('Process pipes not supported on Windows');
70 }
71
72 $process = new Process('exit 0', null, null, array(
73 0 => array('pipe', 'w'),
74 3 => array('pipe', 'r')
75 ));
76 $process->start($this->createLoop());
77
78 $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $process->stdin);
79 $this->assertNull($process->stdout);
80 $this->assertNull($process->stderr);
81 $this->assertCount(2, $process->pipes);
82 $this->assertSame($process->stdin, $process->pipes[0]);
83 $this->assertInstanceOf('React\Stream\WritableStreamInterface', $process->pipes[3]);
2284 }
2385
2486 /**
2587 * @expectedException RuntimeException
88 * @expectedExceptionMessage No such file or directory
2689 */
27 public function testSetEnhanceSigchildCompatibilityCannotBeCalledIfProcessIsRunning()
28 {
29 $process = new Process('sleep 1');
30
90 public function testStartWithInvalidFileDescriptorPathWillThrow()
91 {
92 $fds = array(
93 4 => array('file', '/dev/does-not-exist', 'r')
94 );
95
96 $process = new Process('exit 0', null, null, $fds);
3197 $process->start($this->createLoop());
32 $process->setEnhanceSigchildCompatibility(false);
33 }
34
35 public function testGetCommand()
36 {
37 $process = new Process('echo foo');
38
39 $this->assertSame('echo foo', $process->getCommand());
98 }
99
100 public function testStartWithExcessiveNumberOfFileDescriptorsWillThrow()
101 {
102 if (PHP_VERSION_ID < 70000) {
103 $this->markTestSkipped('PHP 7+ only, causes memory overflow on legacy PHP 5');
104 }
105
106 $ulimit = exec('ulimit -n 2>&1');
107 if ($ulimit < 1) {
108 $this->markTestSkipped('Unable to determine limit of open files (ulimit not available?)');
109 }
110
111 $loop = $this->createLoop();
112
113 // create 70% (usually ~700) dummy file handles in this parent dummy
114 $limit = (int)($ulimit * 0.7);
115 $fds = array();
116 for ($i = 0; $i < $limit; ++$i) {
117 $fds[$i] = fopen('/dev/null', 'r');
118 }
119
120 // try to create child process with another ~700 dummy file handles
121 $new = array_fill(0, $limit, array('file', '/dev/null', 'r'));
122 $process = new Process('ping example.com', null, null, $new);
123
124 try {
125 $process->start($loop);
126
127 $this->fail('Did not expect to reach this point');
128 } catch (\RuntimeException $e) {
129 // clear dummy files handles to make some room again (avoid fatal errors for autoloader)
130 $fds = array();
131
132 $this->assertContains('Too many open files', $e->getMessage());
133 }
40134 }
41135
42136 public function testIsRunning()
43137 {
44 $process = new Process('sleep 1');
138 if (DIRECTORY_SEPARATOR === '\\') {
139 // Windows doesn't have a sleep command and also does not support process pipes
140 $process = new Process($this->getPhpBinary() . ' -r ' . escapeshellarg('sleep(1);'), null, null, array());
141 } else {
142 $process = new Process('sleep 1');
143 }
45144
46145 $this->assertFalse($process->isRunning());
47146 $process->start($this->createLoop());
66165 $this->assertNull($process->getTermSignal());
67166 }
68167
168 public function testCommandWithEnhancedSigchildCompatibilityReceivesExitCode()
169 {
170 if (DIRECTORY_SEPARATOR === '\\') {
171 $this->markTestSkipped('Process pipes not supported on Windows');
172 }
173
174 $loop = $this->createLoop();
175 $old = Process::isSigchildEnabled();
176 Process::setSigchildEnabled(true);
177 $process = new Process('echo foo');
178 $process->start($loop);
179 Process::setSigchildEnabled($old);
180
181 $loop->run();
182
183 $this->assertEquals(0, $process->getExitCode());
184 }
185
69186 public function testReceivesProcessStdoutFromEcho()
70187 {
188 if (DIRECTORY_SEPARATOR === '\\') {
189 $this->markTestSkipped('Process pipes not supported on Windows');
190 }
191
71192 $cmd = 'echo test';
72193
73194 $loop = $this->createLoop();
84205 $this->assertEquals('test', rtrim($buffer));
85206 }
86207
208 public function testReceivesProcessOutputFromStdoutRedirectedToFile()
209 {
210 $tmp = tmpfile();
211
212 if (DIRECTORY_SEPARATOR === '\\') {
213 $cmd = 'cmd /c echo test';
214 } else {
215 $cmd = 'echo test';
216 }
217
218 $loop = $this->createLoop();
219 $process = new Process($cmd, null, null, array(1 => $tmp));
220 $process->start($loop);
221
222 $loop->run();
223
224 rewind($tmp);
225 $this->assertEquals('test', rtrim(stream_get_contents($tmp)));
226 }
227
228 public function testReceivesProcessOutputFromTwoCommandsChainedStdoutRedirectedToFile()
229 {
230 $tmp = tmpfile();
231
232 if (DIRECTORY_SEPARATOR === '\\') {
233 // omit whitespace before "&&" and quotation marks as Windows will actually echo this otherwise
234 $cmd = 'cmd /c echo hello&& cmd /c echo world';
235 } else {
236 $cmd = 'echo "hello" && echo "world"';
237 }
238
239 $loop = $this->createLoop();
240 $process = new Process($cmd, null, null, array(1 => $tmp));
241 $process->start($loop);
242
243 $loop->run();
244
245 rewind($tmp);
246 $this->assertEquals("hello\nworld", str_replace("\r\n", "\n", rtrim(stream_get_contents($tmp))));
247 }
248
249 public function testReceivesProcessOutputFromStdoutAttachedToSocket()
250 {
251 if (DIRECTORY_SEPARATOR === '\\') {
252 $this->markTestSkipped('Sockets as STDIO handles not supported on Windows');
253 }
254
255 // create TCP/IP server on random port and create a client connection
256 $server = stream_socket_server('tcp://127.0.0.1:0');
257 $client = stream_socket_client(stream_socket_get_name($server, false));
258 $peer = stream_socket_accept($server, 0);
259 fclose($server);
260
261 $cmd = 'echo test';
262
263 $loop = $this->createLoop();
264
265 // spawn child process with $client socket as STDOUT, close local reference afterwards
266 $process = new Process($cmd, null, null, array(1 => $client));
267 $process->start($loop);
268 fclose($client);
269
270 $loop->run();
271
272 $this->assertEquals('test', rtrim(stream_get_contents($peer)));
273 }
274
275 public function testReceivesProcessOutputFromStdoutRedirectedToSocketProcess()
276 {
277 // create TCP/IP server on random port and wait for client connection
278 $server = stream_socket_server('tcp://127.0.0.1:0');
279
280 if (DIRECTORY_SEPARATOR === '\\') {
281 $cmd = 'cmd /c echo test';
282 } else {
283 $cmd = 'exec echo test';
284 }
285
286 $code = '$s=stream_socket_client($argv[1]);do{$d=fread(STDIN,8192);fwrite($s,$d);}while(!feof(STDIN));fclose($s);';
287 $cmd .= ' | ' . $this->getPhpBinary() . ' -r ' . escapeshellarg($code) . ' ' . escapeshellarg(stream_socket_get_name($server, false));
288
289 $loop = $this->createLoop();
290
291 // spawn child process without any STDIO streams
292 $process = new Process($cmd, null, null, array());
293 $process->start($loop);
294
295 $peer = stream_socket_accept($server, 10);
296
297 $loop->run();
298
299 $this->assertEquals('test', rtrim(stream_get_contents($peer)));
300 }
301
87302 public function testReceivesProcessStdoutFromDd()
88303 {
304 if (DIRECTORY_SEPARATOR === '\\') {
305 $this->markTestSkipped('Process pipes not supported on Windows');
306 }
307
89308 if (!file_exists('/dev/zero')) {
90309 $this->markTestSkipped('Unable to read from /dev/zero, Windows?');
91310 }
108327
109328 public function testProcessPidNotSameDueToShellWrapper()
110329 {
330 if (DIRECTORY_SEPARATOR === '\\') {
331 $this->markTestSkipped('Process pipes not supported on Windows');
332 }
333
111334 $cmd = $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getmypid();');
112335
113336 $loop = $this->createLoop();
128351
129352 public function testProcessPidSameWithExec()
130353 {
354 if (DIRECTORY_SEPARATOR === '\\') {
355 $this->markTestSkipped('Process pipes not supported on Windows');
356 }
357
131358 $cmd = 'exec ' . $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getmypid();');
132359
133360 $loop = $this->createLoop();
147374
148375 public function testProcessWithDefaultCwdAndEnv()
149376 {
377 if (DIRECTORY_SEPARATOR === '\\') {
378 $this->markTestSkipped('Process pipes not supported on Windows');
379 }
380
150381 $cmd = $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getcwd(), PHP_EOL, count($_SERVER), PHP_EOL;');
151382
152383 $loop = $this->createLoop();
173404
174405 public function testProcessWithCwd()
175406 {
407 if (DIRECTORY_SEPARATOR === '\\') {
408 $this->markTestSkipped('Process pipes not supported on Windows');
409 }
410
176411 $cmd = $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getcwd(), PHP_EOL;');
177412
178413 $loop = $this->createLoop();
192427
193428 public function testProcessWithEnv()
194429 {
430 if (DIRECTORY_SEPARATOR === '\\') {
431 $this->markTestSkipped('Process pipes not supported on Windows');
432 }
433
195434 if (getenv('TRAVIS')) {
196435 $this->markTestSkipped('Cannot execute PHP processes with custom environments on Travis CI.');
197436 }
215454
216455 public function testStartAndAllowProcessToExitSuccessfullyUsingEventLoop()
217456 {
457 if (DIRECTORY_SEPARATOR === '\\') {
458 $this->markTestSkipped('Process pipes not supported on Windows');
459 }
460
218461 $loop = $this->createLoop();
219462 $process = new Process('exit 0');
220463
244487
245488 public function testProcessWillExitFasterThanExitInterval()
246489 {
490 if (DIRECTORY_SEPARATOR === '\\') {
491 $this->markTestSkipped('Process pipes not supported on Windows');
492 }
493
247494 $loop = $this->createLoop();
248495 $process = new Process('echo hi');
249496 $process->start($loop, 2);
257504
258505 public function testDetectsClosingStdoutWithoutHavingToWaitForExit()
259506 {
507 if (DIRECTORY_SEPARATOR === '\\') {
508 $this->markTestSkipped('Process pipes not supported on Windows');
509 }
510
260511 $cmd = 'exec ' . $this->getPhpBinary() . ' -r ' . escapeshellarg('fclose(STDOUT); sleep(1);');
261512
262513 $loop = $this->createLoop();
264515 $process->start($loop);
265516
266517 $closed = false;
267 $process->stdout->on('close', function () use (&$closed) {
518 $process->stdout->on('close', function () use (&$closed, $loop) {
268519 $closed = true;
269 });
270
271 // run loop for 0.1s only
272 $loop->addTimer(0.1, function () use ($loop) {
273520 $loop->stop();
274521 });
522
523 // run loop for maximum of 0.5s only
524 $loop->addTimer(0.5, function () use ($loop) {
525 $loop->stop();
526 });
275527 $loop->run();
276528
277529 $this->assertTrue($closed);
279531
280532 public function testKeepsRunningEvenWhenAllStdioPipesHaveBeenClosed()
281533 {
534 if (DIRECTORY_SEPARATOR === '\\') {
535 $this->markTestSkipped('Process pipes not supported on Windows');
536 }
537
282538 $cmd = 'exec ' . $this->getPhpBinary() . ' -r ' . escapeshellarg('fclose(STDIN);fclose(STDOUT);fclose(STDERR);sleep(1);');
283539
284540 $loop = $this->createLoop();
286542 $process->start($loop);
287543
288544 $closed = 0;
289 $process->stdout->on('close', function () use (&$closed) {
545 $process->stdout->on('close', function () use (&$closed, $loop) {
290546 ++$closed;
291 });
292 $process->stderr->on('close', function () use (&$closed) {
547 if ($closed === 2) {
548 $loop->stop();
549 }
550 });
551 $process->stderr->on('close', function () use (&$closed, $loop) {
293552 ++$closed;
294 });
295
296 // run loop for 0.1s only
297 $loop->addTimer(0.1, function () use ($loop) {
553 if ($closed === 2) {
554 $loop->stop();
555 }
556 });
557
558 // run loop for maximum 0.5s only
559 $loop->addTimer(0.5, function () use ($loop) {
298560 $loop->stop();
299561 });
300562 $loop->run();
305567
306568 public function testDetectsClosingProcessEvenWhenAllStdioPipesHaveBeenClosed()
307569 {
570 if (DIRECTORY_SEPARATOR === '\\') {
571 $this->markTestSkipped('Process pipes not supported on Windows');
572 }
573
308574 $cmd = 'exec ' . $this->getPhpBinary() . ' -r ' . escapeshellarg('fclose(STDIN);fclose(STDOUT);fclose(STDERR);usleep(10000);');
309575
310576 $loop = $this->createLoop();
315581 $loop->run();
316582 $time = microtime(true) - $time;
317583
584 $this->assertLessThan(0.5, $time);
585 $this->assertSame(0, $process->getExitCode());
586 }
587
588 public function testDetectsClosingProcessEvenWhenStartedWithoutPipes()
589 {
590 $loop = $this->createLoop();
591
592 if (DIRECTORY_SEPARATOR === '\\') {
593 $process = new Process('cmd /c exit 0', null, null, array());
594 } else {
595 $process = new Process('exit 0', null, null, array());
596 }
597
598 $process->start($loop, 0.001);
599
600 $time = microtime(true);
601 $loop->run();
602 $time = microtime(true) - $time;
603
318604 $this->assertLessThan(0.1, $time);
605 $this->assertSame(0, $process->getExitCode());
319606 }
320607
321608 public function testStartInvalidProcess()
322609 {
610 if (DIRECTORY_SEPARATOR === '\\') {
611 $this->markTestSkipped('Process pipes not supported on Windows');
612 }
613
323614 $cmd = tempnam(sys_get_temp_dir(), 'react');
324615
325616 $loop = $this->createLoop();
344635 */
345636 public function testStartAlreadyRunningProcess()
346637 {
347 $process = new Process('sleep 1');
638 if (DIRECTORY_SEPARATOR === '\\') {
639 // Windows doesn't have a sleep command and also does not support process pipes
640 $process = new Process($this->getPhpBinary() . ' -r ' . escapeshellarg('sleep(1);'), null, null, array());
641 } else {
642 $process = new Process('sleep 1');
643 }
644 //var_dump($process);
348645
349646 $process->start($this->createLoop());
350647 $process->start($this->createLoop());
352649
353650 public function testTerminateProcesWithoutStartingReturnsFalse()
354651 {
355 $process = new Process('sleep 1');
652 if (DIRECTORY_SEPARATOR === '\\') {
653 // Windows doesn't have a sleep command and also does not support process pipes
654 $process = new Process($this->getPhpBinary() . ' -r ' . escapeshellarg('sleep(1);'), null, null, array());
655 } else {
656 $process = new Process('sleep 1');
657 }
356658
357659 $this->assertFalse($process->terminate());
358660 }
359661
360662 public function testTerminateWillExit()
361663 {
664 if (DIRECTORY_SEPARATOR === '\\') {
665 // Windows doesn't have a sleep command and also does not support process pipes
666 $process = new Process($this->getPhpBinary() . ' -r ' . escapeshellarg('sleep(10);'), null, null, array());
667 } else {
668 $process = new Process('sleep 10');
669 }
670
362671 $loop = $this->createloop();
363672
364 $process = new Process('sleep 10');
365673 $process->start($loop);
366674
367675 $called = false;
369677 $called = true;
370678 });
371679
372 $process->stdin->close();
373 $process->stdout->close();
374 $process->stderr->close();
680 foreach ($process->pipes as $pipe) {
681 $pipe->close();
682 }
375683 $process->terminate();
376684
377685 $loop->run();
381689
382690 public function testTerminateWithDefaultTermSignalUsingEventLoop()
383691 {
384 if (defined('PHP_WINDOWS_VERSION_BUILD')) {
692 if (DIRECTORY_SEPARATOR === '\\') {
385693 $this->markTestSkipped('Windows does not report signals via proc_get_status()');
386694 }
387695
419727
420728 public function testTerminateWithStopAndContinueSignalsUsingEventLoop()
421729 {
422 if (defined('PHP_WINDOWS_VERSION_BUILD')) {
730 if (DIRECTORY_SEPARATOR === '\\') {
423731 $this->markTestSkipped('Windows does not report signals via proc_get_status()');
424732 }
425733
469777 $this->assertFalse($process->isTerminated());
470778 }
471779
472 public function testIssue18() {
780 public function testIssue18()
781 {
782 if (DIRECTORY_SEPARATOR === '\\') {
783 $this->markTestSkipped('Process pipes not supported on Windows');
784 }
785
473786 $loop = $this->createLoop();
474787
475788 $testString = 'x';
519832 * Execute a callback at regular intervals until it returns successfully or
520833 * a timeout is reached.
521834 *
522 * @param Closure $callback Callback with one or more assertions
835 * @param \Closure $callback Callback with one or more assertions
523836 * @param integer $timeout Time limit for callback to succeed (milliseconds)
524837 * @param integer $interval Interval for retrying the callback (milliseconds)
525838 * @throws PHPUnit_Framework_ExpectationFailedException Last exception raised by the callback
548861 }
549862 }
550863
864 /**
865 * Returns the path to the PHP binary. This is already escapescaped via `escapeshellarg()`.
866 *
867 * @return string
868 */
551869 private function getPhpBinary()
552870 {
553871 $runtime = new Runtime();