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
6 | 6 | - 5.6 |
7 | 7 | - 7.0 |
8 | 8 | - 7.1 |
9 | - hhvm # ignore errors, see below | |
9 | - 7.2 | |
10 | - 7.3 | |
11 | # - hhvm # requires legacy phpunit & ignore errors, see below | |
10 | 12 | |
11 | 13 | # lock distro so new future defaults will not break the build |
12 | 14 | dist: trusty |
15 | 17 | include: |
16 | 18 | - php: 5.3 |
17 | 19 | 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 ":"')" | |
18 | 29 | allow_failures: |
19 | 30 | - php: hhvm |
31 | - os: windows | |
20 | 32 | |
21 | 33 | sudo: false |
22 | 34 | |
23 | 35 | install: |
24 | 36 | - composer install --no-interaction |
25 | ||
37 | ||
26 | 38 | script: |
27 | 39 | - vendor/bin/phpunit --coverage-text |
28 | 40 | - php examples/13-benchmark-throughput.php |
0 | 0 | # 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) | |
1 | 53 | |
2 | 54 | ## 0.5.2 (2018-01-18) |
3 | 55 |
18 | 18 | * [Stream Properties](#stream-properties) |
19 | 19 | * [Command](#command) |
20 | 20 | * [Termination](#termination) |
21 | * [Custom pipes](#custom-pipes) | |
21 | 22 | * [Sigchild Compatibility](#sigchild-compatibility) |
22 | 23 | * [Windows Compatibility](#windows-compatibility) |
23 | 24 | * [Install](#install) |
51 | 52 | |
52 | 53 | Once a process is started, its I/O streams will be constructed as instances of |
53 | 54 | `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, | |
55 | 56 | the streams will become closed but not unset. |
56 | 57 | |
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 | |
62 | 76 | [`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), | |
64 | 78 | you can use any of their events and methods as usual: |
65 | 79 | |
66 | 80 | ```php |
81 | $process = new Process($command); | |
82 | $process->start($loop); | |
83 | ||
67 | 84 | $process->stdout->on('data', function ($chunk) { |
68 | 85 | echo $chunk; |
69 | 86 | }); |
98 | 115 | $process->start($loop); |
99 | 116 | ``` |
100 | 117 | |
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 | ||
101 | 134 | 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. | |
104 | 138 | |
105 | 139 | This is a very useful feature because it does not only allow you to pass single |
106 | 140 | commands, but actually allows you to pass any kind of shell command line and |
114 | 148 | $process->start($loop); |
115 | 149 | ``` |
116 | 150 | |
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 | ||
117 | 157 | In other words, the underlying shell is responsible for managing this command |
118 | 158 | line and launching the individual sub-commands and connecting their STDIO |
119 | 159 | streams as appropriate. |
144 | 184 | }); |
145 | 185 | ``` |
146 | 186 | |
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. | |
148 | 188 | While this may seem reasonable for more complex command lines, this actually |
149 | 189 | also applies to running the most simple single command: |
150 | 190 | |
153 | 193 | $process->start($loop); |
154 | 194 | ``` |
155 | 195 | |
156 | This will actually spawn a command hierarchy similar to this: | |
196 | This will actually spawn a command hierarchy similar to this on Unix: | |
157 | 197 | |
158 | 198 | ``` |
159 | 199 | 5480 … \_ php example.php |
166 | 206 | in many cases. |
167 | 207 | |
168 | 208 | 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: | |
171 | 211 | |
172 | 212 | ```php |
173 | 213 | $process = new Process('exec yes'); |
192 | 232 | shell. |
193 | 233 | If you pass a complete command line (or are unsure), you SHOULD most likely keep |
194 | 234 | 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. | |
197 | 237 | |
198 | 238 | ### Termination |
199 | 239 | |
254 | 294 | $process->start($loop); |
255 | 295 | |
256 | 296 | $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(); | |
261 | 301 | }); |
262 | 302 | ``` |
263 | 303 | |
292 | 332 | such as first trying a soft-close and then applying a force-close after a |
293 | 333 | timeout. |
294 | 334 | |
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 | ||
295 | 383 | ### Sigchild Compatibility |
296 | 384 | |
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 | |
308 | 418 | [Process](https://github.com/symfony/process) compoment. |
309 | 419 | |
310 | 420 | ### Windows Compatibility |
311 | 421 | |
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 | ``` | |
318 | 560 | |
319 | 561 | ## Install |
320 | 562 | |
324 | 566 | This will install the latest supported version: |
325 | 567 | |
326 | 568 | ```bash |
327 | $ composer require react/child-process:^0.5.2 | |
569 | $ composer require react/child-process:^0.6.1 | |
328 | 570 | ``` |
329 | 571 | |
330 | 572 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. |
9 | 9 | "react/stream": "^1.0 || ^0.7.6" |
10 | 10 | }, |
11 | 11 | "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", | |
13 | 14 | "sebastian/environment": "^3.0 || ^2.0 || ^1.0" |
14 | 15 | }, |
15 | 16 | "autoload": { |
3 | 3 | use React\ChildProcess\Process; |
4 | 4 | |
5 | 5 | require __DIR__ . '/../vendor/autoload.php'; |
6 | ||
7 | if (DIRECTORY_SEPARATOR === '\\') { | |
8 | exit('Process pipes not supported on Windows' . PHP_EOL); | |
9 | } | |
6 | 10 | |
7 | 11 | $loop = Factory::create(); |
8 | 12 |
3 | 3 | use React\ChildProcess\Process; |
4 | 4 | |
5 | 5 | require __DIR__ . '/../vendor/autoload.php'; |
6 | ||
7 | if (DIRECTORY_SEPARATOR === '\\') { | |
8 | exit('Process pipes not supported on Windows' . PHP_EOL); | |
9 | } | |
6 | 10 | |
7 | 11 | $loop = Factory::create(); |
8 | 12 |
3 | 3 | use React\ChildProcess\Process; |
4 | 4 | |
5 | 5 | require __DIR__ . '/../vendor/autoload.php'; |
6 | ||
7 | if (DIRECTORY_SEPARATOR === '\\') { | |
8 | exit('Process pipes not supported on Windows' . PHP_EOL); | |
9 | } | |
6 | 10 | |
7 | 11 | $loop = Factory::create(); |
8 | 12 |
7 | 7 | $loop = Factory::create(); |
8 | 8 | |
9 | 9 | // 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()); | |
11 | 11 | $process->start($loop); |
12 | 12 | |
13 | 13 | // report when process exits |
17 | 17 | |
18 | 18 | // forcefully terminate process after 2s |
19 | 19 | $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 | } | |
23 | 23 | $process->terminate(); |
24 | 24 | }); |
25 | 25 |
7 | 7 | use React\ChildProcess\Process; |
8 | 8 | |
9 | 9 | require __DIR__ . '/../vendor/autoload.php'; |
10 | ||
11 | if (DIRECTORY_SEPARATOR === '\\') { | |
12 | exit('Process pipes not supported on Windows' . PHP_EOL); | |
13 | } | |
10 | 14 | |
11 | 15 | $cmd = isset($argv[1]) ? implode(' ', array_slice($argv, 1)) : 'dd if=/dev/zero bs=1M count=1000'; |
12 | 16 |
1 | 1 | |
2 | 2 | use React\EventLoop\Factory; |
3 | 3 | use React\ChildProcess\Process; |
4 | use React\Stream\Stream; | |
5 | 4 | |
6 | 5 | require __DIR__ . '/../vendor/autoload.php'; |
6 | ||
7 | if (DIRECTORY_SEPARATOR === '\\') { | |
8 | exit('Process pipes not supported on Windows' . PHP_EOL); | |
9 | } | |
7 | 10 | |
8 | 11 | $loop = Factory::create(); |
9 | 12 |
1 | 1 | |
2 | 2 | use React\EventLoop\Factory; |
3 | 3 | use React\ChildProcess\Process; |
4 | use React\Stream\Stream; | |
5 | 4 | |
6 | 5 | require __DIR__ . '/../vendor/autoload.php'; |
6 | ||
7 | if (DIRECTORY_SEPARATOR === '\\') { | |
8 | exit('Process pipes not supported on Windows' . PHP_EOL); | |
9 | } | |
7 | 10 | |
8 | 11 | $loop = Factory::create(); |
9 | 12 |
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(); |
7 | 7 | convertWarningsToExceptions="true" |
8 | 8 | processIsolation="false" |
9 | 9 | stopOnFailure="false" |
10 | syntaxCheck="false" | |
11 | 10 | bootstrap="vendor/autoload.php" |
12 | 11 | > |
13 | 12 | <testsuites> |
4 | 4 | use Evenement\EventEmitter; |
5 | 5 | use React\EventLoop\LoopInterface; |
6 | 6 | use React\Stream\ReadableResourceStream; |
7 | use React\Stream\ReadableStreamInterface; | |
7 | 8 | use React\Stream\WritableResourceStream; |
9 | use React\Stream\WritableStreamInterface; | |
8 | 10 | |
9 | 11 | /** |
10 | 12 | * Process component. |
12 | 14 | * This class borrows logic from Symfony's Process component for ensuring |
13 | 15 | * compatibility when PHP is compiled with the --enable-sigchild option. |
14 | 16 | * |
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. | |
16 | 54 | */ |
17 | 55 | class Process extends EventEmitter |
18 | 56 | { |
57 | /** | |
58 | * @var WritableStreamInterface|null|ReadableStreamInterface | |
59 | */ | |
19 | 60 | public $stdin; |
61 | ||
62 | /** | |
63 | * @var ReadableStreamInterface|null|WritableStreamInterface | |
64 | */ | |
20 | 65 | public $stdout; |
66 | ||
67 | /** | |
68 | * @var ReadableStreamInterface|null|WritableStreamInterface | |
69 | */ | |
21 | 70 | 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(); | |
22 | 84 | |
23 | 85 | private $cmd; |
24 | 86 | private $cwd; |
25 | 87 | private $env; |
26 | private $options; | |
88 | private $fds; | |
89 | ||
27 | 90 | private $enhanceSigchildCompatibility; |
28 | private $pipes; | |
91 | private $sigchildPipe; | |
29 | 92 | |
30 | 93 | private $process; |
31 | 94 | private $status; |
39 | 102 | /** |
40 | 103 | * Constructor. |
41 | 104 | * |
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 | |
47 | 110 | */ |
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')) { | |
55 | 114 | throw new \LogicException('The Process class relies on proc_open(), which is not available on your PHP installation.'); |
56 | 115 | } |
57 | 116 | |
65 | 124 | } |
66 | 125 | } |
67 | 126 | |
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(); | |
70 | 145 | } |
71 | 146 | |
72 | 147 | /** |
73 | 148 | * Start the process. |
74 | 149 | * |
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. | |
77 | 152 | * |
78 | 153 | * @param LoopInterface $loop Loop interface for stream construction |
79 | 154 | * @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 | |
81 | 156 | */ |
82 | 157 | public function start(LoopInterface $loop, $interval = 0.1) |
83 | 158 | { |
86 | 161 | } |
87 | 162 | |
88 | 163 | $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; | |
94 | 166 | |
95 | 167 | // Read exit code through fourth pipe to work around --enable-sigchild |
96 | if ($this->isSigchildEnabled() && $this->enhanceSigchildCompatibility) { | |
168 | if ($this->enhanceSigchildCompatibility) { | |
97 | 169 | $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; | |
107 | 200 | $closeCount = 0; |
108 | ||
109 | $that = $this; | |
110 | 201 | $streamCloseHandler = function () use (&$closeCount, $loop, $interval, $that) { |
111 | $closeCount++; | |
112 | ||
113 | if ($closeCount < 2) { | |
202 | $closeCount--; | |
203 | ||
204 | if ($closeCount > 0) { | |
114 | 205 | return; |
115 | 206 | } |
116 | 207 | |
131 | 222 | }); |
132 | 223 | }; |
133 | 224 | |
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 | } | |
139 | 249 | } |
140 | 250 | |
141 | 251 | /** |
150 | 260 | return; |
151 | 261 | } |
152 | 262 | |
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) { | |
158 | 268 | $this->pollExitCodePipe(); |
159 | 269 | $this->closeExitCodePipe(); |
160 | 270 | } |
161 | 271 | |
162 | $exitCode = proc_close($this->process); | |
272 | $exitCode = \proc_close($this->process); | |
163 | 273 | $this->process = null; |
164 | 274 | |
165 | 275 | if ($this->exitCode === null && $exitCode !== -1) { |
180 | 290 | * Terminate the process with an optional signal. |
181 | 291 | * |
182 | 292 | * @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 | |
184 | 294 | */ |
185 | 295 | public function terminate($signal = null) |
186 | 296 | { |
189 | 299 | } |
190 | 300 | |
191 | 301 | 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); | |
196 | 306 | } |
197 | 307 | |
198 | 308 | /** |
206 | 316 | } |
207 | 317 | |
208 | 318 | /** |
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'); | |
216 | 428 | } |
217 | 429 | |
218 | 430 | /** |
222 | 434 | * determine the success of a process when PHP has been compiled with |
223 | 435 | * the --enable-sigchild option. |
224 | 436 | * |
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; | |
350 | 443 | } |
351 | 444 | |
352 | 445 | /** |
356 | 449 | */ |
357 | 450 | private function pollExitCodePipe() |
358 | 451 | { |
359 | if ( ! isset($this->pipes[3])) { | |
452 | if ($this->sigchildPipe === null) { | |
360 | 453 | return; |
361 | 454 | } |
362 | 455 | |
363 | $r = array($this->pipes[3]); | |
456 | $r = array($this->sigchildPipe); | |
364 | 457 | $w = $e = null; |
365 | 458 | |
366 | $n = @stream_select($r, $w, $e, 0); | |
459 | $n = @\stream_select($r, $w, $e, 0); | |
367 | 460 | |
368 | 461 | if (1 !== $n) { |
369 | 462 | return; |
370 | 463 | } |
371 | 464 | |
372 | $data = fread($r[0], 8192); | |
373 | ||
374 | if (strlen($data) > 0) { | |
465 | $data = \fread($r[0], 8192); | |
466 | ||
467 | if (\strlen($data) > 0) { | |
375 | 468 | $this->fallbackExitCode = (int) $data; |
376 | 469 | } |
377 | 470 | } |
383 | 476 | */ |
384 | 477 | private function closeExitCodePipe() |
385 | 478 | { |
386 | if ( ! isset($this->pipes[3])) { | |
479 | if ($this->sigchildPipe === null) { | |
387 | 480 | return; |
388 | 481 | } |
389 | 482 | |
390 | fclose($this->pipes[3]); | |
391 | unset($this->pipes[3]); | |
483 | \fclose($this->sigchildPipe); | |
484 | $this->sigchildPipe = null; | |
392 | 485 | } |
393 | 486 | |
394 | 487 | /** |
430 | 523 | return; |
431 | 524 | } |
432 | 525 | |
433 | $this->status = proc_get_status($this->process); | |
526 | $this->status = \proc_get_status($this->process); | |
434 | 527 | |
435 | 528 | if ($this->status === false) { |
436 | 529 | throw new \UnexpectedValueException('proc_get_status() failed'); |
10 | 10 | { |
11 | 11 | abstract public function createLoop(); |
12 | 12 | |
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 | ||
15 | 26 | $process = new Process('echo foo'); |
16 | 27 | |
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]); | |
22 | 84 | } |
23 | 85 | |
24 | 86 | /** |
25 | 87 | * @expectedException RuntimeException |
88 | * @expectedExceptionMessage No such file or directory | |
26 | 89 | */ |
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); | |
31 | 97 | $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 | } | |
40 | 134 | } |
41 | 135 | |
42 | 136 | public function testIsRunning() |
43 | 137 | { |
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 | } | |
45 | 144 | |
46 | 145 | $this->assertFalse($process->isRunning()); |
47 | 146 | $process->start($this->createLoop()); |
66 | 165 | $this->assertNull($process->getTermSignal()); |
67 | 166 | } |
68 | 167 | |
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 | ||
69 | 186 | public function testReceivesProcessStdoutFromEcho() |
70 | 187 | { |
188 | if (DIRECTORY_SEPARATOR === '\\') { | |
189 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
190 | } | |
191 | ||
71 | 192 | $cmd = 'echo test'; |
72 | 193 | |
73 | 194 | $loop = $this->createLoop(); |
84 | 205 | $this->assertEquals('test', rtrim($buffer)); |
85 | 206 | } |
86 | 207 | |
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 | ||
87 | 302 | public function testReceivesProcessStdoutFromDd() |
88 | 303 | { |
304 | if (DIRECTORY_SEPARATOR === '\\') { | |
305 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
306 | } | |
307 | ||
89 | 308 | if (!file_exists('/dev/zero')) { |
90 | 309 | $this->markTestSkipped('Unable to read from /dev/zero, Windows?'); |
91 | 310 | } |
108 | 327 | |
109 | 328 | public function testProcessPidNotSameDueToShellWrapper() |
110 | 329 | { |
330 | if (DIRECTORY_SEPARATOR === '\\') { | |
331 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
332 | } | |
333 | ||
111 | 334 | $cmd = $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getmypid();'); |
112 | 335 | |
113 | 336 | $loop = $this->createLoop(); |
128 | 351 | |
129 | 352 | public function testProcessPidSameWithExec() |
130 | 353 | { |
354 | if (DIRECTORY_SEPARATOR === '\\') { | |
355 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
356 | } | |
357 | ||
131 | 358 | $cmd = 'exec ' . $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getmypid();'); |
132 | 359 | |
133 | 360 | $loop = $this->createLoop(); |
147 | 374 | |
148 | 375 | public function testProcessWithDefaultCwdAndEnv() |
149 | 376 | { |
377 | if (DIRECTORY_SEPARATOR === '\\') { | |
378 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
379 | } | |
380 | ||
150 | 381 | $cmd = $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getcwd(), PHP_EOL, count($_SERVER), PHP_EOL;'); |
151 | 382 | |
152 | 383 | $loop = $this->createLoop(); |
173 | 404 | |
174 | 405 | public function testProcessWithCwd() |
175 | 406 | { |
407 | if (DIRECTORY_SEPARATOR === '\\') { | |
408 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
409 | } | |
410 | ||
176 | 411 | $cmd = $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getcwd(), PHP_EOL;'); |
177 | 412 | |
178 | 413 | $loop = $this->createLoop(); |
192 | 427 | |
193 | 428 | public function testProcessWithEnv() |
194 | 429 | { |
430 | if (DIRECTORY_SEPARATOR === '\\') { | |
431 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
432 | } | |
433 | ||
195 | 434 | if (getenv('TRAVIS')) { |
196 | 435 | $this->markTestSkipped('Cannot execute PHP processes with custom environments on Travis CI.'); |
197 | 436 | } |
215 | 454 | |
216 | 455 | public function testStartAndAllowProcessToExitSuccessfullyUsingEventLoop() |
217 | 456 | { |
457 | if (DIRECTORY_SEPARATOR === '\\') { | |
458 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
459 | } | |
460 | ||
218 | 461 | $loop = $this->createLoop(); |
219 | 462 | $process = new Process('exit 0'); |
220 | 463 | |
244 | 487 | |
245 | 488 | public function testProcessWillExitFasterThanExitInterval() |
246 | 489 | { |
490 | if (DIRECTORY_SEPARATOR === '\\') { | |
491 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
492 | } | |
493 | ||
247 | 494 | $loop = $this->createLoop(); |
248 | 495 | $process = new Process('echo hi'); |
249 | 496 | $process->start($loop, 2); |
257 | 504 | |
258 | 505 | public function testDetectsClosingStdoutWithoutHavingToWaitForExit() |
259 | 506 | { |
507 | if (DIRECTORY_SEPARATOR === '\\') { | |
508 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
509 | } | |
510 | ||
260 | 511 | $cmd = 'exec ' . $this->getPhpBinary() . ' -r ' . escapeshellarg('fclose(STDOUT); sleep(1);'); |
261 | 512 | |
262 | 513 | $loop = $this->createLoop(); |
264 | 515 | $process->start($loop); |
265 | 516 | |
266 | 517 | $closed = false; |
267 | $process->stdout->on('close', function () use (&$closed) { | |
518 | $process->stdout->on('close', function () use (&$closed, $loop) { | |
268 | 519 | $closed = true; |
269 | }); | |
270 | ||
271 | // run loop for 0.1s only | |
272 | $loop->addTimer(0.1, function () use ($loop) { | |
273 | 520 | $loop->stop(); |
274 | 521 | }); |
522 | ||
523 | // run loop for maximum of 0.5s only | |
524 | $loop->addTimer(0.5, function () use ($loop) { | |
525 | $loop->stop(); | |
526 | }); | |
275 | 527 | $loop->run(); |
276 | 528 | |
277 | 529 | $this->assertTrue($closed); |
279 | 531 | |
280 | 532 | public function testKeepsRunningEvenWhenAllStdioPipesHaveBeenClosed() |
281 | 533 | { |
534 | if (DIRECTORY_SEPARATOR === '\\') { | |
535 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
536 | } | |
537 | ||
282 | 538 | $cmd = 'exec ' . $this->getPhpBinary() . ' -r ' . escapeshellarg('fclose(STDIN);fclose(STDOUT);fclose(STDERR);sleep(1);'); |
283 | 539 | |
284 | 540 | $loop = $this->createLoop(); |
286 | 542 | $process->start($loop); |
287 | 543 | |
288 | 544 | $closed = 0; |
289 | $process->stdout->on('close', function () use (&$closed) { | |
545 | $process->stdout->on('close', function () use (&$closed, $loop) { | |
290 | 546 | ++$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) { | |
293 | 552 | ++$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) { | |
298 | 560 | $loop->stop(); |
299 | 561 | }); |
300 | 562 | $loop->run(); |
305 | 567 | |
306 | 568 | public function testDetectsClosingProcessEvenWhenAllStdioPipesHaveBeenClosed() |
307 | 569 | { |
570 | if (DIRECTORY_SEPARATOR === '\\') { | |
571 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
572 | } | |
573 | ||
308 | 574 | $cmd = 'exec ' . $this->getPhpBinary() . ' -r ' . escapeshellarg('fclose(STDIN);fclose(STDOUT);fclose(STDERR);usleep(10000);'); |
309 | 575 | |
310 | 576 | $loop = $this->createLoop(); |
315 | 581 | $loop->run(); |
316 | 582 | $time = microtime(true) - $time; |
317 | 583 | |
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 | ||
318 | 604 | $this->assertLessThan(0.1, $time); |
605 | $this->assertSame(0, $process->getExitCode()); | |
319 | 606 | } |
320 | 607 | |
321 | 608 | public function testStartInvalidProcess() |
322 | 609 | { |
610 | if (DIRECTORY_SEPARATOR === '\\') { | |
611 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
612 | } | |
613 | ||
323 | 614 | $cmd = tempnam(sys_get_temp_dir(), 'react'); |
324 | 615 | |
325 | 616 | $loop = $this->createLoop(); |
344 | 635 | */ |
345 | 636 | public function testStartAlreadyRunningProcess() |
346 | 637 | { |
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); | |
348 | 645 | |
349 | 646 | $process->start($this->createLoop()); |
350 | 647 | $process->start($this->createLoop()); |
352 | 649 | |
353 | 650 | public function testTerminateProcesWithoutStartingReturnsFalse() |
354 | 651 | { |
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 | } | |
356 | 658 | |
357 | 659 | $this->assertFalse($process->terminate()); |
358 | 660 | } |
359 | 661 | |
360 | 662 | public function testTerminateWillExit() |
361 | 663 | { |
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 | ||
362 | 671 | $loop = $this->createloop(); |
363 | 672 | |
364 | $process = new Process('sleep 10'); | |
365 | 673 | $process->start($loop); |
366 | 674 | |
367 | 675 | $called = false; |
369 | 677 | $called = true; |
370 | 678 | }); |
371 | 679 | |
372 | $process->stdin->close(); | |
373 | $process->stdout->close(); | |
374 | $process->stderr->close(); | |
680 | foreach ($process->pipes as $pipe) { | |
681 | $pipe->close(); | |
682 | } | |
375 | 683 | $process->terminate(); |
376 | 684 | |
377 | 685 | $loop->run(); |
381 | 689 | |
382 | 690 | public function testTerminateWithDefaultTermSignalUsingEventLoop() |
383 | 691 | { |
384 | if (defined('PHP_WINDOWS_VERSION_BUILD')) { | |
692 | if (DIRECTORY_SEPARATOR === '\\') { | |
385 | 693 | $this->markTestSkipped('Windows does not report signals via proc_get_status()'); |
386 | 694 | } |
387 | 695 | |
419 | 727 | |
420 | 728 | public function testTerminateWithStopAndContinueSignalsUsingEventLoop() |
421 | 729 | { |
422 | if (defined('PHP_WINDOWS_VERSION_BUILD')) { | |
730 | if (DIRECTORY_SEPARATOR === '\\') { | |
423 | 731 | $this->markTestSkipped('Windows does not report signals via proc_get_status()'); |
424 | 732 | } |
425 | 733 | |
469 | 777 | $this->assertFalse($process->isTerminated()); |
470 | 778 | } |
471 | 779 | |
472 | public function testIssue18() { | |
780 | public function testIssue18() | |
781 | { | |
782 | if (DIRECTORY_SEPARATOR === '\\') { | |
783 | $this->markTestSkipped('Process pipes not supported on Windows'); | |
784 | } | |
785 | ||
473 | 786 | $loop = $this->createLoop(); |
474 | 787 | |
475 | 788 | $testString = 'x'; |
519 | 832 | * Execute a callback at regular intervals until it returns successfully or |
520 | 833 | * a timeout is reached. |
521 | 834 | * |
522 | * @param Closure $callback Callback with one or more assertions | |
835 | * @param \Closure $callback Callback with one or more assertions | |
523 | 836 | * @param integer $timeout Time limit for callback to succeed (milliseconds) |
524 | 837 | * @param integer $interval Interval for retrying the callback (milliseconds) |
525 | 838 | * @throws PHPUnit_Framework_ExpectationFailedException Last exception raised by the callback |
548 | 861 | } |
549 | 862 | } |
550 | 863 | |
864 | /** | |
865 | * Returns the path to the PHP binary. This is already escapescaped via `escapeshellarg()`. | |
866 | * | |
867 | * @return string | |
868 | */ | |
551 | 869 | private function getPhpBinary() |
552 | 870 | { |
553 | 871 | $runtime = new Runtime(); |