Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
- ubuntu-20.04
- windows-2019
php:
- 8.0
- 7.4
- 7.3
- 7.2
Expand Down Expand Up @@ -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
32 changes: 28 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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).
Expand Down
38 changes: 38 additions & 0 deletions examples/05-stdio-sockets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

use React\EventLoop\Factory;
use React\ChildProcess\Process;

require __DIR__ . '/../vendor/autoload.php';

if (PHP_VERSION_ID < 80000) {
exit('Socket descriptors require PHP 8+' . PHP_EOL);
}

$loop = Factory::create();

$process = new Process(
'php -r ' . escapeshellarg('echo 1;sleep(1);fwrite(STDERR,2);exit(3);'),
null,
null,
[
['socket'],
['socket'],
['socket']
]
);
$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();
2 changes: 2 additions & 0 deletions examples/23-forward-socket.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

// see also 05-stdio-sockets.php

use React\EventLoop\Factory;
use React\ChildProcess\Process;

Expand Down
18 changes: 13 additions & 5 deletions src/Process.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use React\Stream\ReadableStreamInterface;
use React\Stream\WritableResourceStream;
use React\Stream\WritableStreamInterface;
use React\Stream\DuplexResourceStream;
use React\Stream\DuplexStreamInterface;

/**
* Process component.
Expand Down Expand Up @@ -56,17 +58,17 @@
class Process extends EventEmitter
{
/**
* @var WritableStreamInterface|null|ReadableStreamInterface
* @var WritableStreamInterface|null|DuplexStreamInterface|ReadableStreamInterface
*/
public $stdin;

/**
* @var ReadableStreamInterface|null|WritableStreamInterface
* @var ReadableStreamInterface|null|DuplexStreamInterface|WritableStreamInterface
*/
public $stdout;

/**
* @var ReadableStreamInterface|null|WritableStreamInterface
* @var ReadableStreamInterface|null|DuplexStreamInterface|WritableStreamInterface
*/
public $stderr;

Expand All @@ -79,7 +81,7 @@ class Process extends EventEmitter
* - 1: STDOUT (`ReadableStreamInterface`)
* - 2: STDERR (`ReadableStreamInterface`)
*
* @var array<ReadableStreamInterface|WritableStreamInterface>
* @var array<ReadableStreamInterface|WritableStreamInterface|DuplexStreamInterface>
*/
public $pipes = array();

Expand Down Expand Up @@ -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);
Expand Down
60 changes: 58 additions & 2 deletions tests/AbstractProcessTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 === '\\') {
Expand Down Expand Up @@ -109,15 +136,15 @@ 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?)');
}

$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');
Expand Down Expand Up @@ -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();
Expand Down