pax_global_header00006660000000000000000000000064132301323220014501gustar00rootroot0000000000000052 comment=aae49d7f1340bafb695b9af3ce4421ea41a39620 child-process-0.5.2/000077500000000000000000000000001323013232200142445ustar00rootroot00000000000000child-process-0.5.2/.gitignore000066400000000000000000000000251323013232200162310ustar00rootroot00000000000000composer.lock vendor child-process-0.5.2/.travis.yml000066400000000000000000000007121323013232200163550ustar00rootroot00000000000000language: php php: # - 5.3 # requires old distro, see below - 5.4 - 5.5 - 5.6 - 7.0 - 7.1 - hhvm # ignore errors, see below # lock distro so new future defaults will not break the build dist: trusty matrix: include: - php: 5.3 dist: precise allow_failures: - php: hhvm sudo: false install: - composer install --no-interaction script: - vendor/bin/phpunit --coverage-text - php examples/13-benchmark-throughput.php child-process-0.5.2/CHANGELOG.md000066400000000000000000000040511323013232200160550ustar00rootroot00000000000000# Changelog ## 0.5.2 (2018-01-18) * Feature: Detect "exit" immediately if last process pipe is closed (#58 by @clue) This introduces a simple check to see if the program is already known to be closed when the last process pipe is closed instead of relying on a periodic timer. This simple change improves "exit" detection significantly for most programs and does not cause a noticeable penalty for more advanced use cases. * Fix forward compatibility with upcoming EventLoop releases (#56 by @clue) ## 0.5.1 (2017-12-22) * Fix: Update Stream dependency to work around SEGFAULT in legacy PHP < 5.4.28 and PHP < 5.5.12 (#50 and #52 by @clue) * Improve test suite by simplifying test bootstrapping logic via Composer and adding forward compatibility with PHPUnit 6 (#53, #54 and #55 by @clue) ## 0.5.0 (2017-08-15) * Forward compatibility: react/event-loop 1.0 and 0.5, react/stream 0.7.2 and 1.0, and Événement 3.0 (#38 and #44 by @WyriHaximus, and #46 by @clue) * Windows compatibility: Documentate that windows isn't supported in 0.5 unless used from within WSL (#41 and #47 by @WyriHaximus) * Documentation: Termination examples (#42 by @clue) * BC: Throw LogicException in Process instanciating when on Windows or when proc_open is missing (was `RuntimeException`) (#49 by @mdrost) ## 0.4.3 (2017-03-14) * Ease getting started by improving documentation and adding examples (#33 and #34 by @clue) * First class support for PHP 5.3 through PHP 7.1 and HHVM (#29 by @clue and #32 by @WyriHaximus) ## 0.4.2 (2017-03-10) * Feature: Forward compatibility with Stream v0.5 (#26 by @clue) * Improve test suite by removing AppVeyor and adding PHPUnit to `require-dev` (#27 and #28 by @clue) ## 0.4.1 (2016-08-01) * Standalone component * Test against PHP 7 and HHVM, report test coverage, AppVeyor tests * Wait for stdout and stderr to close before watching for process exit (#18 by @mbonneau) ## 0.4.0 (2014-02-02) * Feature: Added ChildProcess to run async child processes within the event loop (@jmikola) child-process-0.5.2/LICENSE000066400000000000000000000020551323013232200152530ustar00rootroot00000000000000Copyright (c) 2012 Igor Wiedler, Chris Boden Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. child-process-0.5.2/README.md000066400000000000000000000270131323013232200155260ustar00rootroot00000000000000# Child Process [![Build Status](https://travis-ci.org/reactphp/child-process.svg?branch=master)](https://travis-ci.org/reactphp/child-process) Event-driven library for executing child processes with [ReactPHP](https://reactphp.org/). This library integrates [Program Execution](http://php.net/manual/en/book.exec.php) with the [EventLoop](https://github.com/reactphp/event-loop). Child processes launched may be signaled and will emit an `exit` event upon termination. Additionally, process I/O streams (i.e. STDIN, STDOUT, STDERR) are exposed as [Streams](https://github.com/reactphp/stream). **Table of contents** * [Quickstart example](#quickstart-example) * [Process](#process) * [Stream Properties](#stream-properties) * [Command](#command) * [Termination](#termination) * [Sigchild Compatibility](#sigchild-compatibility) * [Windows Compatibility](#windows-compatibility) * [Install](#install) * [Tests](#tests) * [License](#license) ## Quickstart example ```php $loop = React\EventLoop\Factory::create(); $process = new React\ChildProcess\Process('echo foo'); $process->start($loop); $process->stdout->on('data', function ($chunk) { echo $chunk; }); $process->on('exit', function($exitCode, $termSignal) { echo 'Process exited with code ' . $exitCode . PHP_EOL; }); $loop->run(); ``` See also the [examples](examples). ## Process ### Stream Properties Once a process is started, its I/O streams will be constructed as instances of `React\Stream\ReadableStreamInterface` and `React\Stream\WritableStreamInterface`. Before `start()` is called, these properties are `null`.Once a process terminates, the streams will become closed but not unset. * `$stdin` * `$stdout` * `$stderr` Each of these implement the underlying [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) or [`WritableStreamInterface`](https://github.com/reactphp/stream#writablestreaminterface) and you can use any of their events and methods as usual: ```php $process->stdout->on('data', function ($chunk) { echo $chunk; }); $process->stdout->on('end', function () { echo 'ended'; }); $process->stdout->on('error', function (Exception $e) { echo 'error: ' . $e->getMessage(); }); $process->stdout->on('close', function () { echo 'closed'; }); $process->stdin->write($data); $process->stdin->end($data = null); // … ``` For more details, see the [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) and [`WritableStreamInterface`](https://github.com/reactphp/stream#writablestreaminterface). ### Command The `Process` class allows you to pass any kind of command line string: ```php $process = new Process('echo test'); $process->start($loop); ``` By default, PHP will launch processes by wrapping the given command line string in a `sh` command, so that the above example will actually execute `sh -c echo test` under the hood. This is a very useful feature because it does not only allow you to pass single commands, but actually allows you to pass any kind of shell command line and launch multiple sub-commands using command chains (with `&&`, `||`, `;` and others) and allows you to redirect STDIO streams (with `2>&1` and family). This can be used to pass complete command lines and receive the resulting STDIO streams from the wrapping shell command like this: ```php $process = new Process('echo run && demo || echo failed'); $process->start($loop); ``` In other words, the underlying shell is responsible for managing this command line and launching the individual sub-commands and connecting their STDIO streams as appropriate. This implies that the `Process` class will only receive the resulting STDIO streams from the wrapping shell, which will thus contain the complete input/output with no way to discern the input/output of single sub-commands. If you want to discern the output of single sub-commands, you may want to implement some higher-level protocol logic, such as printing an explicit boundary between each sub-command like this: ```php $process = new Process('cat first && echo --- && cat second'); $process->start($loop); ``` As an alternative, considering launching one process at a time and listening on its `exit` event to conditionally start the next process in the chain. This will give you an opportunity to configure the subsequent process I/O streams: ```php $first = new Process('cat first'); $first->start($loop); $first->on('exit', function () use ($loop) { $second = new Process('cat second'); $second->start($loop); }); ``` Keep in mind that PHP uses the shell wrapper for ALL command lines. While this may seem reasonable for more complex command lines, this actually also applies to running the most simple single command: ```php $process = new Process('yes'); $process->start($loop); ``` This will actually spawn a command hierarchy similar to this: ``` 5480 … \_ php example.php 5481 … \_ sh -c yes 5482 … \_ yes ``` This means that trying to get the underlying process PID or sending signals will actually target the wrapping shell, which may not be the desired result in many cases. If you do not want this wrapping shell process to show up, you can simply prepend the command string with `exec`, which will cause the wrapping shell process to be replaced by our process: ```php $process = new Process('exec yes'); $process->start($loop); ``` This will show a resulting command hierarchy similar to this: ``` 5480 … \_ php example.php 5481 … \_ yes ``` This means that trying to get the underlying process PID and sending signals will now target the actual command as expected. Note that in this case, the command line will not be run in a wrapping shell. This implies that when using `exec`, there's no way to pass command lines such as those containing command chains or redirected STDIO streams. As a rule of thumb, most commands will likely run just fine with the wrapping shell. If you pass a complete command line (or are unsure), you SHOULD most likely keep the wrapping shell. If you want to pass an invidual command only, you MAY want to consider prepending the command string with `exec` to avoid the wrapping shell. ### Termination The `exit` event will be emitted whenever the process is no longer running. Event listeners will receive the exit code and termination signal as two arguments: ```php $process = new Process('sleep 10'); $process->start($loop); $process->on('exit', function ($code, $term) { if ($term === null) { echo 'exit with code ' . $code . PHP_EOL; } else { echo 'terminated with signal ' . $term . PHP_EOL; } }); ``` Note that `$code` is `null` if the process has terminated, but the exit code could not be determined (for example [sigchild compatibility](#sigchild-compatibility) was disabled). Similarly, `$term` is `null` unless the process has terminated in response to an uncaught signal sent to it. This is not a limitation of this project, but actual how exit codes and signals are exposed on POSIX systems, for more details see also [here](https://unix.stackexchange.com/questions/99112/default-exit-code-when-process-is-terminated). It's also worth noting that process termination depends on all file descriptors being closed beforehand. This means that all [process pipes](#stream-properties) will emit a `close` event before the `exit` event and that no more `data` events will arrive after the `exit` event. Accordingly, if either of these pipes is in a paused state (`pause()` method or internally due to a `pipe()` call), this detection may not trigger. The `terminate(?int $signal = null): bool` method can be used to send the process a signal (SIGTERM by default). Depending on which signal you send to the process and whether it has a signal handler registered, this can be used to either merely signal a process or even forcefully terminate it. ```php $process->terminate(SIGUSR1); ``` Keep the above section in mind if you want to forcefully terminate a process. If your process spawn sub-processes or implicitly uses the [wrapping shell mentioned above](#command), its file descriptors may be inherited to child processes and terminating the main process may not necessarily terminate the whole process tree. It is highly suggested that you explicitly `close()` all process pipes accordingly when terminating a process: ```php $process = new Process('sleep 10'); $process->start($loop); $loop->addTimer(2.0, function () use ($process) { $process->stdin->close(); $process->stout->close(); $process->stderr->close(); $process->terminate(SIGKILL); }); ``` For many simple programs these seamingly complicated steps can also be avoided by prefixing the command line with `exec` to avoid the wrapping shell and its inherited process pipes as [mentioned above](#command). ```php $process = new Process('exec sleep 10'); $process->start($loop); $loop->addTimer(2.0, function () use ($process) { $process->terminate(); }); ``` Many command line programs also wait for data on `STDIN` and terminate cleanly when this pipe is closed. For example, the following can be used to "soft-close" a `cat` process: ```php $process = new Process('cat'); $process->start($loop); $loop->addTimer(2.0, function () use ($process) { $process->stdin->end(); }); ``` While process pipes and termination may seem confusing to newcomers, the above properties actually allow some fine grained control over process termination, such as first trying a soft-close and then applying a force-close after a timeout. ### Sigchild Compatibility When PHP has been compiled with the `--enabled-sigchild` option, a child process' exit code cannot be reliably determined via `proc_close()` or `proc_get_status()`. Instead, we execute the child process with a fourth pipe and use that to retrieve its exit code. This behavior is used by default and only when necessary. It may be manually disabled by calling `setEnhanceSigchildCompatibility(false)` on the Process before it is started, in which case the `exit` event may receive `null` instead of the actual exit code. **Note:** This functionality was taken from Symfony's [Process](https://github.com/symfony/process) compoment. ### Windows Compatibility Due to the blocking nature of `STDIN`/`STDOUT`/`STDERR` pipes on Windows we can not guarantee this package works as expected on Windows directly. As such when instantiating `Process` it throws an exception when on native Windows. However this package does work on [`Windows Subsystem for Linux`](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux) (or WSL) without issues. We suggest [installing WSL](https://msdn.microsoft.com/en-us/commandline/wsl/install_guide) when you want to run this package on Windows. ## Install The recommended way to install this library is [through Composer](https://getcomposer.org). [New to Composer?](https://getcomposer.org/doc/00-intro.md) This will install the latest supported version: ```bash $ composer require react/child-process:^0.5.2 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP extensions and supports running on legacy PHP 5.3 through current PHP 7+ and HHVM. It's *highly recommended to use PHP 7+* for this project. See above note for limited [Windows Compatibility](#windows-compatibility). ## Tests To run the test suite, you first need to clone this repo and then install all dependencies [through Composer](https://getcomposer.org): ```bash $ composer install ``` To run the test suite, go to the project root and run: ```bash $ php vendor/bin/phpunit ``` ## License MIT, see [LICENSE file](LICENSE). child-process-0.5.2/composer.json000066400000000000000000000013161323013232200167670ustar00rootroot00000000000000{ "name": "react/child-process", "description": "Event-driven library for executing child processes with ReactPHP.", "keywords": ["process", "event-driven", "ReactPHP"], "license": "MIT", "require": { "php": ">=5.3.0", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", "react/stream": "^1.0 || ^0.7.6" }, "require-dev": { "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35", "sebastian/environment": "^3.0 || ^2.0 || ^1.0" }, "autoload": { "psr-4": { "React\\ChildProcess\\": "src" } }, "autoload-dev": { "psr-4": { "React\\Tests\\ChildProcess\\": "tests" } } } child-process-0.5.2/examples/000077500000000000000000000000001323013232200160625ustar00rootroot00000000000000child-process-0.5.2/examples/01-stdio.php000066400000000000000000000012741323013232200201370ustar00rootroot00000000000000start($loop); $process->stdout->on('data', function ($chunk) { echo $chunk; }); $process->on('exit', function ($code) { echo 'EXIT with code ' . $code . PHP_EOL; }); // periodically send something to stream $periodic = $loop->addPeriodicTimer(0.2, function () use ($process) { $process->stdin->write('hello'); }); // stop sending after a few seconds $loop->addTimer(2.0, function () use ($periodic, $loop, $process) { $loop->cancelTimer($periodic); $process->stdin->end(); }); $loop->run(); child-process-0.5.2/examples/02-race.php000066400000000000000000000006621323013232200177300ustar00rootroot00000000000000start($loop); $second = new Process('sleep 1; echo hallo'); $second->start($loop); $first->stdout->on('data', function ($chunk) { echo $chunk; }); $second->stdout->on('data', function ($chunk) { echo $chunk; }); $loop->run(); child-process-0.5.2/examples/03-stdout-stderr.php000066400000000000000000000010231323013232200216320ustar00rootroot00000000000000&2;sleep 1;echo error;sleep 1;nope'); $process->start($loop); $process->stdout->on('data', function ($chunk) { echo '(' . $chunk . ')'; }); $process->stderr->on('data', function ($chunk) { echo '[' . $chunk . ']'; }); $process->on('exit', function ($code) { echo 'EXIT with code ' . $code . PHP_EOL; }); $loop->run(); child-process-0.5.2/examples/04-terminate.php000066400000000000000000000011151323013232200210020ustar00rootroot00000000000000start($loop); // report when process exits $process->on('exit', function ($exit, $term) { var_dump($exit, $term); }); // forcefully terminate process after 2s $loop->addTimer(2.0, function () use ($process) { $process->stdin->close(); $process->stdout->close(); $process->stderr->close(); $process->terminate(); }); $loop->run(); child-process-0.5.2/examples/11-benchmark-read.php000066400000000000000000000030761323013232200216630ustar00rootroot00000000000000write('Counts number of chunks/bytes received from process STDOUT' . PHP_EOL); $info->write('Command: ' . $cmd . PHP_EOL); if (extension_loaded('xdebug')) { $info->write('NOTICE: The "xdebug" extension is loaded, this has a major impact on performance.' . PHP_EOL); } $process = new Process($cmd); $process->start($loop); $start = microtime(true); $chunks = 0; $bytes = 0; $process->stdout->on('data', function ($chunk) use (&$chunks, &$bytes) { ++$chunks; $bytes += strlen($chunk); }); // print stream position once stream closes $process->on('exit', function () use (&$chunks, &$bytes, $start, $info) { $t = microtime(true) - $start; $info->write('read ' . $chunks . ' chunks with ' . $bytes . ' byte(s) in ' . round($t, 3) . ' second(s) => ' . round($bytes / 1024 / 1024 / $t, 1) . ' MiB/s' . PHP_EOL); $info->write('peak memory usage of ' . round(memory_get_peak_usage(true) / 1024 / 1024, 1) . ' MiB' . PHP_EOL); }); // report any other output/errors $process->stdout->on('error', array($info, 'write')); $process->stderr->on('data', 'printf'); $process->stdout->on('error', 'printf'); $loop->run(); child-process-0.5.2/examples/12-benchmark-write.php000066400000000000000000000021701323013232200220750ustar00rootroot00000000000000write('Pipes data to process STDIN' . PHP_EOL); if (extension_loaded('xdebug')) { $info->write('NOTICE: The "xdebug" extension is loaded, this has a major impact on performance.' . PHP_EOL); } $process = new Process('dd of=/dev/zero'); $process->start($loop); // 10000 * 100 KB => 1 GB $i = 10000; $chunk = str_repeat("\0", 100 * 1000); $write = function () use ($chunk, $process, &$i, &$write) { do { --$i; $continue = $process->stdin->write($chunk); } while ($i && $continue); if ($i > 0) { // buffer full => wait for drain to continue $process->stdin->once('drain', $write); } else { $process->stdin->end(); } }; $write(); // report any other output/errors $process->stdout->on('data', 'printf'); $process->stdout->on('error', 'printf'); $process->stderr->on('data', 'printf'); $process->stdout->on('error', 'printf'); $loop->run(); child-process-0.5.2/examples/13-benchmark-throughput.php000066400000000000000000000033171323013232200231610ustar00rootroot00000000000000write('Pipes data through process STDIN and reads STDOUT again' . PHP_EOL); if (extension_loaded('xdebug')) { $info->write('NOTICE: The "xdebug" extension is loaded, this has a major impact on performance.' . PHP_EOL); } $process = new Process('cat'); $process->start($loop); $start = microtime(true); $chunks = 0; $bytes = 0; $process->stdout->on('data', function ($chunk) use (&$chunks, &$bytes) { ++$chunks; $bytes += strlen($chunk); }); // 10000 * 100 KB => 1 GB $i = 10000; $chunk = str_repeat("\0", 100 * 1000); $write = function () use ($chunk, $process, &$i, &$write) { do { --$i; $continue = $process->stdin->write($chunk); } while ($i && $continue); if ($i > 0) { // buffer full => wait for drain to continue $process->stdin->once('drain', $write); } else { $process->stdin->end(); } }; $write(); // print stream position once process exits $process->on('exit', function () use (&$chunks, &$bytes, $start, $info) { $t = microtime(true) - $start; $info->write('read ' . $chunks . ' chunks with ' . $bytes . ' byte(s) in ' . round($t, 3) . ' second(s) => ' . round($bytes / 1024 / 1024 / $t, 1) . ' MiB/s' . PHP_EOL); $info->write('peak memory usage of ' . round(memory_get_peak_usage(true) / 1024 / 1024, 1) . ' MiB' . PHP_EOL); }); // report any other output/errors $process->stdout->on('error', 'printf'); $process->stderr->on('data', 'printf'); $process->stdout->on('error', 'printf'); $loop->run(); child-process-0.5.2/phpunit.xml.dist000066400000000000000000000012321323013232200174150ustar00rootroot00000000000000 ./tests/ ./src/ child-process-0.5.2/src/000077500000000000000000000000001323013232200150335ustar00rootroot00000000000000child-process-0.5.2/src/Process.php000066400000000000000000000301441323013232200171640ustar00rootroot00000000000000cmd = $cmd; $this->cwd = $cwd; if (null !== $env) { $this->env = array(); foreach ($env as $key => $value) { $this->env[(binary) $key] = (binary) $value; } } $this->options = $options; $this->enhanceSigchildCompatibility = $this->isSigchildEnabled(); } /** * Start the process. * * After the process is started, the standard IO streams will be constructed * and available via public properties. STDIN will be paused upon creation. * * @param LoopInterface $loop Loop interface for stream construction * @param float $interval Interval to periodically monitor process state (seconds) * @throws RuntimeException If the process is already running or fails to start */ public function start(LoopInterface $loop, $interval = 0.1) { if ($this->isRunning()) { throw new \RuntimeException('Process is already running'); } $cmd = $this->cmd; $fdSpec = array( array('pipe', 'r'), // stdin array('pipe', 'w'), // stdout array('pipe', 'w'), // stderr ); // Read exit code through fourth pipe to work around --enable-sigchild if ($this->isSigchildEnabled() && $this->enhanceSigchildCompatibility) { $fdSpec[] = array('pipe', 'w'); $cmd = sprintf('(%s) 3>/dev/null; code=$?; echo $code >&3; exit $code', $cmd); } $this->process = proc_open($cmd, $fdSpec, $this->pipes, $this->cwd, $this->env, $this->options); if (!is_resource($this->process)) { throw new \RuntimeException('Unable to launch a new process.'); } $closeCount = 0; $that = $this; $streamCloseHandler = function () use (&$closeCount, $loop, $interval, $that) { $closeCount++; if ($closeCount < 2) { return; } // process already closed => report immediately if (!$that->isRunning()) { $that->close(); $that->emit('exit', array($that->getExitCode(), $that->getTermSignal())); return; } // close not detected immediately => check regularly $loop->addPeriodicTimer($interval, function ($timer) use ($that, $loop) { if (!$that->isRunning()) { $loop->cancelTimer($timer); $that->close(); $that->emit('exit', array($that->getExitCode(), $that->getTermSignal())); } }); }; $this->stdin = new WritableResourceStream($this->pipes[0], $loop); $this->stdout = new ReadableResourceStream($this->pipes[1], $loop); $this->stdout->on('close', $streamCloseHandler); $this->stderr = new ReadableResourceStream($this->pipes[2], $loop); $this->stderr->on('close', $streamCloseHandler); } /** * Close the process. * * This method should only be invoked via the periodic timer that monitors * the process state. */ public function close() { if ($this->process === null) { return; } $this->stdin->close(); $this->stdout->close(); $this->stderr->close(); if ($this->isSigchildEnabled() && $this->enhanceSigchildCompatibility) { $this->pollExitCodePipe(); $this->closeExitCodePipe(); } $exitCode = proc_close($this->process); $this->process = null; if ($this->exitCode === null && $exitCode !== -1) { $this->exitCode = $exitCode; } if ($this->exitCode === null && $this->status['exitcode'] !== -1) { $this->exitCode = $this->status['exitcode']; } if ($this->exitCode === null && $this->fallbackExitCode !== null) { $this->exitCode = $this->fallbackExitCode; $this->fallbackExitCode = null; } } /** * Terminate the process with an optional signal. * * @param int $signal Optional signal (default: SIGTERM) * @return boolean Whether the signal was sent successfully */ public function terminate($signal = null) { if ($this->process === null) { return false; } if ($signal !== null) { return proc_terminate($this->process, $signal); } return proc_terminate($this->process); } /** * Get the command string used to launch the process. * * @return string */ public function getCommand() { return $this->cmd; } /** * Return whether sigchild compatibility is enabled. * * @return boolean */ public final function getEnhanceSigchildCompatibility() { return $this->enhanceSigchildCompatibility; } /** * Enable or disable sigchild compatibility mode. * * Sigchild compatibility mode is required to get the exit code and * determine the success of a process when PHP has been compiled with * the --enable-sigchild option. * * @param boolean $enhance * @return self * @throws RuntimeException If the process is already running */ public final function setEnhanceSigchildCompatibility($enhance) { if ($this->isRunning()) { throw new \RuntimeException('Process is already running'); } $this->enhanceSigchildCompatibility = (bool) $enhance; return $this; } /** * Get the exit code returned by the process. * * This value is only meaningful if isRunning() has returned false. Null * will be returned if the process is still running. * * Null may also be returned if the process has terminated, but the exit * code could not be determined (e.g. sigchild compatibility was disabled). * * @return int|null */ public function getExitCode() { return $this->exitCode; } /** * Get the process ID. * * @return int|null */ public function getPid() { $status = $this->getCachedStatus(); return $status !== null ? $status['pid'] : null; } /** * Get the signal that caused the process to stop its execution. * * This value is only meaningful if isStopped() has returned true. Null will * be returned if the process was never stopped. * * @return int|null */ public function getStopSignal() { return $this->stopSignal; } /** * Get the signal that caused the process to terminate its execution. * * This value is only meaningful if isTerminated() has returned true. Null * will be returned if the process was never terminated. * * @return int|null */ public function getTermSignal() { return $this->termSignal; } /** * Return whether the process is still running. * * @return boolean */ public function isRunning() { if ($this->process === null) { return false; } $status = $this->getFreshStatus(); return $status !== null ? $status['running'] : false; } /** * Return whether the process has been stopped by a signal. * * @return boolean */ public function isStopped() { $status = $this->getFreshStatus(); return $status !== null ? $status['stopped'] : false; } /** * Return whether the process has been terminated by an uncaught signal. * * @return boolean */ public function isTerminated() { $status = $this->getFreshStatus(); return $status !== null ? $status['signaled'] : false; } /** * Return whether PHP has been compiled with the '--enable-sigchild' option. * * @see \Symfony\Component\Process\Process::isSigchildEnabled() * @return bool */ public final static function isSigchildEnabled() { if (null !== self::$sigchild) { return self::$sigchild; } ob_start(); phpinfo(INFO_GENERAL); return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); } /** * Check the fourth pipe for an exit code. * * This should only be used if --enable-sigchild compatibility was enabled. */ private function pollExitCodePipe() { if ( ! isset($this->pipes[3])) { return; } $r = array($this->pipes[3]); $w = $e = null; $n = @stream_select($r, $w, $e, 0); if (1 !== $n) { return; } $data = fread($r[0], 8192); if (strlen($data) > 0) { $this->fallbackExitCode = (int) $data; } } /** * Close the fourth pipe used to relay an exit code. * * This should only be used if --enable-sigchild compatibility was enabled. */ private function closeExitCodePipe() { if ( ! isset($this->pipes[3])) { return; } fclose($this->pipes[3]); unset($this->pipes[3]); } /** * Return the cached process status. * * @return array */ private function getCachedStatus() { if ($this->status === null) { $this->updateStatus(); } return $this->status; } /** * Return the updated process status. * * @return array */ private function getFreshStatus() { $this->updateStatus(); return $this->status; } /** * Update the process status, stop/term signals, and exit code. * * Stop/term signals are only updated if the process is currently stopped or * signaled, respectively. Otherwise, signal values will remain as-is so the * corresponding getter methods may be used at a later point in time. */ private function updateStatus() { if ($this->process === null) { return; } $this->status = proc_get_status($this->process); if ($this->status === false) { throw new \UnexpectedValueException('proc_get_status() failed'); } if ($this->status['stopped']) { $this->stopSignal = $this->status['stopsig']; } if ($this->status['signaled']) { $this->termSignal = $this->status['termsig']; } if (!$this->status['running'] && -1 !== $this->status['exitcode']) { $this->exitCode = $this->status['exitcode']; } } } child-process-0.5.2/tests/000077500000000000000000000000001323013232200154065ustar00rootroot00000000000000child-process-0.5.2/tests/AbstractProcessTest.php000066400000000000000000000365541323013232200220760ustar00rootroot00000000000000assertSame($process, $process->setEnhanceSigchildCompatibility(true)); $this->assertTrue($process->getEnhanceSigchildCompatibility()); $this->assertSame($process, $process->setEnhanceSigchildCompatibility(false)); $this->assertFalse($process->getEnhanceSigchildCompatibility()); } /** * @expectedException RuntimeException */ public function testSetEnhanceSigchildCompatibilityCannotBeCalledIfProcessIsRunning() { $process = new Process('sleep 1'); $process->start($this->createLoop()); $process->setEnhanceSigchildCompatibility(false); } public function testGetCommand() { $process = new Process('echo foo'); $this->assertSame('echo foo', $process->getCommand()); } public function testIsRunning() { $process = new Process('sleep 1'); $this->assertFalse($process->isRunning()); $process->start($this->createLoop()); $this->assertTrue($process->isRunning()); return $process; } /** * @depends testIsRunning */ public function testGetExitCodeWhenRunning($process) { $this->assertNull($process->getExitCode()); } /** * @depends testIsRunning */ public function testGetTermSignalWhenRunning($process) { $this->assertNull($process->getTermSignal()); } public function testReceivesProcessStdoutFromEcho() { $cmd = 'echo test'; $loop = $this->createLoop(); $process = new Process($cmd); $process->start($loop); $buffer = ''; $process->stdout->on('data', function ($data) use (&$buffer) { $buffer .= $data; }); $loop->run(); $this->assertEquals('test', rtrim($buffer)); } public function testReceivesProcessStdoutFromDd() { if (!file_exists('/dev/zero')) { $this->markTestSkipped('Unable to read from /dev/zero, Windows?'); } $cmd = 'dd if=/dev/zero bs=12345 count=1234'; $loop = $this->createLoop(); $process = new Process($cmd); $process->start($loop); $bytes = 0; $process->stdout->on('data', function ($data) use (&$bytes) { $bytes += strlen($data); }); $loop->run(); $this->assertEquals(12345 * 1234, $bytes); } public function testProcessPidNotSameDueToShellWrapper() { $cmd = $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getmypid();'); $loop = $this->createLoop(); $process = new Process($cmd, '/'); $process->start($loop); $output = ''; $process->stdout->on('data', function ($data) use (&$output) { $output .= $data; }); $loop->run(); $this->assertNotEquals('', $output); $this->assertNotNull($process->getPid()); $this->assertNotEquals($process->getPid(), $output); } public function testProcessPidSameWithExec() { $cmd = 'exec ' . $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getmypid();'); $loop = $this->createLoop(); $process = new Process($cmd, '/'); $process->start($loop); $output = ''; $process->stdout->on('data', function ($data) use (&$output) { $output .= $data; }); $loop->run(); $this->assertNotNull($process->getPid()); $this->assertEquals($process->getPid(), $output); } public function testProcessWithDefaultCwdAndEnv() { $cmd = $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getcwd(), PHP_EOL, count($_SERVER), PHP_EOL;'); $loop = $this->createLoop(); $process = new Process($cmd); $process->start($loop); $output = ''; $process->stdout->on('data', function () use (&$output) { $output .= func_get_arg(0); }); $loop->run(); list($cwd, $envCount) = explode(PHP_EOL, $output); /* Child process should inherit the same current working directory and * existing environment variables; however, it may be missing a "_" * environment variable (i.e. current shell/script) on some platforms. */ $this->assertSame(getcwd(), $cwd); $this->assertLessThanOrEqual(1, (count($_SERVER) - (integer) $envCount)); } public function testProcessWithCwd() { $cmd = $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getcwd(), PHP_EOL;'); $loop = $this->createLoop(); $process = new Process($cmd, '/'); $process->start($loop); $output = ''; $process->stdout->on('data', function () use (&$output) { $output .= func_get_arg(0); }); $loop->run(); $this->assertSame('/' . PHP_EOL, $output); } public function testProcessWithEnv() { if (getenv('TRAVIS')) { $this->markTestSkipped('Cannot execute PHP processes with custom environments on Travis CI.'); } $cmd = $this->getPhpBinary() . ' -r ' . escapeshellarg('echo getenv("foo"), PHP_EOL;'); $loop = $this->createLoop(); $process = new Process($cmd, null, array('foo' => 'bar')); $process->start($loop); $output = ''; $process->stdout->on('data', function () use (&$output) { $output .= func_get_arg(0); }); $loop->run(); $this->assertSame('bar' . PHP_EOL, $output); } public function testStartAndAllowProcessToExitSuccessfullyUsingEventLoop() { $loop = $this->createLoop(); $process = new Process('exit 0'); $called = false; $exitCode = 'initial'; $termSignal = 'initial'; $process->on('exit', function () use (&$called, &$exitCode, &$termSignal) { $called = true; $exitCode = func_get_arg(0); $termSignal = func_get_arg(1); }); $process->start($loop); $loop->run(); $this->assertTrue($called); $this->assertSame(0, $exitCode); $this->assertNull($termSignal); $this->assertFalse($process->isRunning()); $this->assertSame(0, $process->getExitCode()); $this->assertNull($process->getTermSignal()); $this->assertFalse($process->isTerminated()); } public function testProcessWillExitFasterThanExitInterval() { $loop = $this->createLoop(); $process = new Process('echo hi'); $process->start($loop, 2); $time = microtime(true); $loop->run(); $time = microtime(true) - $time; $this->assertLessThan(0.1, $time); } public function testDetectsClosingStdoutWithoutHavingToWaitForExit() { $cmd = 'exec ' . $this->getPhpBinary() . ' -r ' . escapeshellarg('fclose(STDOUT); sleep(1);'); $loop = $this->createLoop(); $process = new Process($cmd); $process->start($loop); $closed = false; $process->stdout->on('close', function () use (&$closed) { $closed = true; }); // run loop for 0.1s only $loop->addTimer(0.1, function () use ($loop) { $loop->stop(); }); $loop->run(); $this->assertTrue($closed); } public function testKeepsRunningEvenWhenAllStdioPipesHaveBeenClosed() { $cmd = 'exec ' . $this->getPhpBinary() . ' -r ' . escapeshellarg('fclose(STDIN);fclose(STDOUT);fclose(STDERR);sleep(1);'); $loop = $this->createLoop(); $process = new Process($cmd); $process->start($loop); $closed = 0; $process->stdout->on('close', function () use (&$closed) { ++$closed; }); $process->stderr->on('close', function () use (&$closed) { ++$closed; }); // run loop for 0.1s only $loop->addTimer(0.1, function () use ($loop) { $loop->stop(); }); $loop->run(); $this->assertEquals(2, $closed); $this->assertTrue($process->isRunning()); } public function testDetectsClosingProcessEvenWhenAllStdioPipesHaveBeenClosed() { $cmd = 'exec ' . $this->getPhpBinary() . ' -r ' . escapeshellarg('fclose(STDIN);fclose(STDOUT);fclose(STDERR);usleep(10000);'); $loop = $this->createLoop(); $process = new Process($cmd); $process->start($loop, 0.001); $time = microtime(true); $loop->run(); $time = microtime(true) - $time; $this->assertLessThan(0.1, $time); } public function testStartInvalidProcess() { $cmd = tempnam(sys_get_temp_dir(), 'react'); $loop = $this->createLoop(); $process = new Process($cmd); $process->start($loop); $output = ''; $process->stderr->on('data', function () use (&$output) { $output .= func_get_arg(0); }); $loop->run(); unlink($cmd); $this->assertNotEmpty($output); } /** * @expectedException RuntimeException */ public function testStartAlreadyRunningProcess() { $process = new Process('sleep 1'); $process->start($this->createLoop()); $process->start($this->createLoop()); } public function testTerminateProcesWithoutStartingReturnsFalse() { $process = new Process('sleep 1'); $this->assertFalse($process->terminate()); } public function testTerminateWillExit() { $loop = $this->createloop(); $process = new Process('sleep 10'); $process->start($loop); $called = false; $process->on('exit', function () use (&$called) { $called = true; }); $process->stdin->close(); $process->stdout->close(); $process->stderr->close(); $process->terminate(); $loop->run(); $this->assertTrue($called); } public function testTerminateWithDefaultTermSignalUsingEventLoop() { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Windows does not report signals via proc_get_status()'); } if (!defined('SIGTERM')) { $this->markTestSkipped('SIGTERM is not defined'); } $loop = $this->createloop(); $process = new Process('sleep 1; exit 0'); $called = false; $exitCode = 'initial'; $termSignal = 'initial'; $process->on('exit', function () use (&$called, &$exitCode, &$termSignal) { $called = true; $exitCode = func_get_arg(0); $termSignal = func_get_arg(1); }); $process->start($loop); $process->terminate(); $loop->run(); $this->assertTrue($called); $this->assertNull($exitCode); $this->assertEquals(SIGTERM, $termSignal); $this->assertFalse($process->isRunning()); $this->assertNull($process->getExitCode()); $this->assertEquals(SIGTERM, $process->getTermSignal()); $this->assertTrue($process->isTerminated()); } public function testTerminateWithStopAndContinueSignalsUsingEventLoop() { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Windows does not report signals via proc_get_status()'); } if (!defined('SIGSTOP') && !defined('SIGCONT')) { $this->markTestSkipped('SIGSTOP and/or SIGCONT is not defined'); } $loop = $this->createloop(); $process = new Process('sleep 1; exit 0'); $called = false; $exitCode = 'initial'; $termSignal = 'initial'; $process->on('exit', function () use (&$called, &$exitCode, &$termSignal) { $called = true; $exitCode = func_get_arg(0); $termSignal = func_get_arg(1); }); $that = $this; $process->start($loop); $process->terminate(SIGSTOP); $that->assertSoon(function () use ($process, $that) { $that->assertTrue($process->isStopped()); $that->assertTrue($process->isRunning()); $that->assertEquals(SIGSTOP, $process->getStopSignal()); }); $process->terminate(SIGCONT); $that->assertSoon(function () use ($process, $that) { $that->assertFalse($process->isStopped()); $that->assertEquals(SIGSTOP, $process->getStopSignal()); }); $loop->run(); $this->assertTrue($called); $this->assertSame(0, $exitCode); $this->assertNull($termSignal); $this->assertFalse($process->isRunning()); $this->assertSame(0, $process->getExitCode()); $this->assertNull($process->getTermSignal()); $this->assertFalse($process->isTerminated()); } public function testIssue18() { $loop = $this->createLoop(); $testString = 'x'; $process = new Process($this->getPhpBinary() . " -r 'echo \"$testString\";'"); $stdOut = ''; $stdErr = ''; $that = $this; $process->on( 'exit', function ($exitCode) use (&$stdOut, &$stdErr, $testString, $that) { $that->assertEquals(0, $exitCode, "Exit code is 0"); $that->assertEquals($testString, $stdOut); } ); $process->start($loop); $process->stdout->on( 'data', function ($output) use (&$stdOut) { $stdOut .= $output; } ); $process->stderr->on( 'data', function ($output) use (&$stdErr) { $stdErr .= $output; } ); // tick loop once $loop->addTimer(0, function () use ($loop) { $loop->stop(); }); $loop->run(); sleep(1); // comment this line out and it works fine $loop->run(); } /** * Execute a callback at regular intervals until it returns successfully or * a timeout is reached. * * @param Closure $callback Callback with one or more assertions * @param integer $timeout Time limit for callback to succeed (milliseconds) * @param integer $interval Interval for retrying the callback (milliseconds) * @throws PHPUnit_Framework_ExpectationFailedException Last exception raised by the callback */ public function assertSoon(\Closure $callback, $timeout = 20000, $interval = 200) { $start = microtime(true); $timeout /= 1000; // convert to seconds $interval *= 1000; // convert to microseconds while (1) { try { call_user_func($callback); return; } catch (ExpectationFailedException $e) { // namespaced PHPUnit exception } catch (\PHPUnit_Framework_ExpectationFailedException $e) { // legacy PHPUnit exception } if ((microtime(true) - $start) > $timeout) { throw $e; } usleep($interval); } } private function getPhpBinary() { $runtime = new Runtime(); return $runtime->getBinary(); } } child-process-0.5.2/tests/ExtEventLoopProcessTest.php000066400000000000000000000010031323013232200227040ustar00rootroot00000000000000markTestSkipped('ext-event is not installed.'); } if (!class_exists('React\EventLoop\ExtEventLoop')) { $this->markTestSkipped('ext-event not supported by this legacy react/event-loop version'); } return new ExtEventLoop(); } } child-process-0.5.2/tests/ExtLibevLoopProcessTest.php000066400000000000000000000006731323013232200227000ustar00rootroot00000000000000markTestSkipped('ext-libev is not installed.'); } return class_exists('React\EventLoop\ExtLibevLoop') ? new ExtLibevLoop() : new LibEvLoop(); } } child-process-0.5.2/tests/ExtLibeventLoopProcessTest.php000066400000000000000000000007221323013232200234020ustar00rootroot00000000000000markTestSkipped('ext-libevent is not installed.'); } return class_exists('React\EventLoop\ExtLibeventLoop') ? new ExtLibeventLoop() : new LibEventLoop(); } } child-process-0.5.2/tests/StreamSelectLoopProcessTest.php000066400000000000000000000003511323013232200235420ustar00rootroot00000000000000