diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58ca434..2b0b287 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ jobs: - ubuntu-20.04 - windows-2019 php: + - 8.0 - 7.4 - 7.3 - 7.2 @@ -45,6 +46,6 @@ jobs: - uses: azjezz/setup-hhvm@v1 with: version: lts-3.30 - - run: hhvm $(which composer) require phpunit/phpunit:^5 --dev # requires legacy phpunit + - run: hhvm $(which composer) install - run: hhvm vendor/bin/phpunit - run: hhvm examples/13-benchmark-throughput.php diff --git a/README.md b/README.md index f0ca147..963d3f9 100644 --- a/README.md +++ b/README.md @@ -422,9 +422,9 @@ cases. You may then enable this explicitly as given above. Due to platform constraints, this library provides only limited support for spawning child processes on Windows. In particular, PHP does not allow accessing -standard I/O pipes without blocking. As such, this project will not allow -constructing a child process with the default process pipes and will instead -throw a `LogicException` on Windows by default: +standard I/O pipes on Windows without blocking. As such, this project will not +allow constructing a child process with the default process pipes and will +instead throw a `LogicException` on Windows by default: ```php // throws LogicException on Windows @@ -435,6 +435,30 @@ $process->start($loop); There are a number of alternatives and workarounds as detailed below if you want to run a child process on Windows, each with its own set of pros and cons: +* As of PHP 8, you can start the child process with `socket` pair descriptors + in place of normal standard I/O pipes like this: + + ```php + $process = new Process( + 'ping example.com', + null, + null, + [ + ['socket'], + ['socket'], + ['socket'] + ] + ); + $process->start($loop); + + $process->stdout->on('data', function ($chunk) { + echo $chunk; + }); + ``` + + These `socket` pairs support non-blocking process I/O on any platform, + including Windows. However, not all programs accept stdio sockets. + * This package does work on [`Windows Subsystem for Linux`](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux) (or WSL) without issues. When you are in control over how your application is @@ -573,7 +597,7 @@ $ composer require react/child-process:^0.6.1 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. +extensions and supports running on legacy PHP 5.3 through current PHP 8+ and HHVM. It's *highly recommended to use PHP 7+* for this project. See above note for limited [Windows Compatibility](#windows-compatibility). diff --git a/examples/05-stdio-sockets.php b/examples/05-stdio-sockets.php new file mode 100644 index 0000000..291c00c --- /dev/null +++ b/examples/05-stdio-sockets.php @@ -0,0 +1,38 @@ +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(); diff --git a/examples/23-forward-socket.php b/examples/23-forward-socket.php index c052bf0..7d37ddd 100644 --- a/examples/23-forward-socket.php +++ b/examples/23-forward-socket.php @@ -1,5 +1,7 @@ + * @var array */ public $pipes = array(); @@ -229,7 +231,13 @@ public function start(LoopInterface $loop, $interval = 0.1) } foreach ($pipes as $n => $fd) { - if (\strpos($this->fds[$n][1], 'w') === false) { + // use open mode from stream meta data or fall back to pipe open mode for legacy HHVM + $meta = \stream_get_meta_data($fd); + $mode = $meta['mode'] === '' ? ($this->fds[$n][1] === 'r' ? 'w' : 'r') : $meta['mode']; + + if ($mode === 'r+') { + $stream = new DuplexResourceStream($fd, $loop); + } elseif ($mode === 'w') { $stream = new WritableResourceStream($fd, $loop); } else { $stream = new ReadableResourceStream($fd, $loop); diff --git a/tests/AbstractProcessTest.php b/tests/AbstractProcessTest.php index 1b37c32..f19df97 100644 --- a/tests/AbstractProcessTest.php +++ b/tests/AbstractProcessTest.php @@ -49,6 +49,33 @@ public function testStartWillAssignPipes() $this->assertSame($process->stderr, $process->pipes[2]); } + /** + * @depends testStartWillAssignPipes + * @requires PHP 8 + */ + public function testStartWithSocketDescriptorsWillAssignDuplexPipes() + { + $process = new Process( + (DIRECTORY_SEPARATOR === '\\' ? 'cmd /c ' : '') . 'echo foo', + null, + null, + array( + array('socket'), + array('socket'), + array('socket') + ) + ); + $process->start($this->createLoop()); + + $this->assertInstanceOf('React\Stream\DuplexStreamInterface', $process->stdin); + $this->assertInstanceOf('React\Stream\DuplexStreamInterface', $process->stdout); + $this->assertInstanceOf('React\Stream\DuplexStreamInterface', $process->stderr); + $this->assertCount(3, $process->pipes); + $this->assertSame($process->stdin, $process->pipes[0]); + $this->assertSame($process->stdout, $process->pipes[1]); + $this->assertSame($process->stderr, $process->pipes[2]); + } + public function testStartWithoutAnyPipesWillNotAssignPipes() { if (DIRECTORY_SEPARATOR === '\\') { @@ -109,7 +136,7 @@ public function testStartWithExcessiveNumberOfFileDescriptorsWillThrow() $this->markTestSkipped('Not supported on legacy HHVM'); } - $ulimit = exec('ulimit -n 2>&1'); + $ulimit = (int) exec('ulimit -n 2>&1'); if ($ulimit < 1) { $this->markTestSkipped('Unable to determine limit of open files (ulimit not available?)'); } @@ -117,7 +144,7 @@ public function testStartWithExcessiveNumberOfFileDescriptorsWillThrow() $loop = $this->createLoop(); // create 70% (usually ~700) dummy file handles in this parent dummy - $limit = (int)($ulimit * 0.7); + $limit = (int) ($ulimit * 0.7); $fds = array(); for ($i = 0; $i < $limit; ++$i) { $fds[$i] = fopen('/dev/null', 'r'); @@ -211,6 +238,35 @@ public function testReceivesProcessStdoutFromEcho() $this->assertEquals('test', rtrim($buffer)); } + /** + * @requires PHP 8 + */ + public function testReceivesProcessStdoutFromEchoViaSocketDescriptors() + { + $loop = $this->createLoop(); + $process = new Process( + $this->getPhpBinary() . ' -r ' . escapeshellarg('echo \'test\';'), + null, + null, + array( + array('socket'), + array('socket'), + array('socket') + ) + ); + $process->start($loop); + + $buffer = ''; + $process->stdout->on('data', function ($data) use (&$buffer) { + $buffer .= $data; + }); + $process->stderr->on('data', 'var_dump'); + + $loop->run(); + + $this->assertEquals('test', rtrim($buffer)); + } + public function testReceivesProcessOutputFromStdoutRedirectedToFile() { $tmp = tmpfile();