close
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Support socket descriptors on PHP 8+
  • Loading branch information
clue committed Jan 31, 2021
commit 1ae63b2f5bb7b3f2e9ea6b01d05b494bad87b943
30 changes: 27 additions & 3 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
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
15 changes: 10 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,10 @@ public function start(LoopInterface $loop, $interval = 0.1)
}

foreach ($pipes as $n => $fd) {
if (\strpos($this->fds[$n][1], 'w') === false) {
$meta = \stream_get_meta_data($fd);
if ($meta['mode'] === 'r+') {
$stream = new DuplexResourceStream($fd, $loop);
} elseif ($meta['mode'] === 'w') {
$stream = new WritableResourceStream($fd, $loop);
} else {
$stream = new ReadableResourceStream($fd, $loop);
Expand Down
56 changes: 56 additions & 0 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 @@ -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