Skip to content

Commit 1ae63b2

Browse files
committed
Support socket descriptors on PHP 8+
1 parent 1422f1e commit 1ae63b2

File tree

5 files changed

+133
-8
lines changed

5 files changed

+133
-8
lines changed

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -422,9 +422,9 @@ cases. You may then enable this explicitly as given above.
422422

423423
Due to platform constraints, this library provides only limited support for
424424
spawning child processes on Windows. In particular, PHP does not allow accessing
425-
standard I/O pipes without blocking. As such, this project will not allow
426-
constructing a child process with the default process pipes and will instead
427-
throw a `LogicException` on Windows by default:
425+
standard I/O pipes on Windows without blocking. As such, this project will not
426+
allow constructing a child process with the default process pipes and will
427+
instead throw a `LogicException` on Windows by default:
428428

429429
```php
430430
// throws LogicException on Windows
@@ -435,6 +435,30 @@ $process->start($loop);
435435
There are a number of alternatives and workarounds as detailed below if you want
436436
to run a child process on Windows, each with its own set of pros and cons:
437437

438+
* As of PHP 8, you can start the child process with `socket` pair descriptors
439+
in place of normal standard I/O pipes like this:
440+
441+
```php
442+
$process = new Process(
443+
'ping example.com',
444+
null,
445+
null,
446+
[
447+
['socket'],
448+
['socket'],
449+
['socket']
450+
]
451+
);
452+
$process->start($loop);
453+
454+
$process->stdout->on('data', function ($chunk) {
455+
echo $chunk;
456+
});
457+
```
458+
459+
These `socket` pairs support non-blocking process I/O on any platform,
460+
including Windows. However, not all programs accept stdio sockets.
461+
438462
* This package does work on
439463
[`Windows Subsystem for Linux`](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux)
440464
(or WSL) without issues. When you are in control over how your application is

examples/05-stdio-sockets.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
use React\EventLoop\Factory;
4+
use React\ChildProcess\Process;
5+
6+
require __DIR__ . '/../vendor/autoload.php';
7+
8+
if (PHP_VERSION_ID < 80000) {
9+
exit('Socket descriptors require PHP 8+' . PHP_EOL);
10+
}
11+
12+
$loop = Factory::create();
13+
14+
$process = new Process(
15+
'php -r ' . escapeshellarg('echo 1;sleep(1);fwrite(STDERR,2);exit(3);'),
16+
null,
17+
null,
18+
[
19+
['socket'],
20+
['socket'],
21+
['socket']
22+
]
23+
);
24+
$process->start($loop);
25+
26+
$process->stdout->on('data', function ($chunk) {
27+
echo '(' . $chunk . ')';
28+
});
29+
30+
$process->stderr->on('data', function ($chunk) {
31+
echo '[' . $chunk . ']';
32+
});
33+
34+
$process->on('exit', function ($code) {
35+
echo 'EXIT with code ' . $code . PHP_EOL;
36+
});
37+
38+
$loop->run();

examples/23-forward-socket.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
// see also 05-stdio-sockets.php
4+
35
use React\EventLoop\Factory;
46
use React\ChildProcess\Process;
57

src/Process.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use React\Stream\ReadableStreamInterface;
99
use React\Stream\WritableResourceStream;
1010
use React\Stream\WritableStreamInterface;
11+
use React\Stream\DuplexResourceStream;
12+
use React\Stream\DuplexStreamInterface;
1113

1214
/**
1315
* Process component.
@@ -56,17 +58,17 @@
5658
class Process extends EventEmitter
5759
{
5860
/**
59-
* @var WritableStreamInterface|null|ReadableStreamInterface
61+
* @var WritableStreamInterface|null|DuplexStreamInterface|ReadableStreamInterface
6062
*/
6163
public $stdin;
6264

6365
/**
64-
* @var ReadableStreamInterface|null|WritableStreamInterface
66+
* @var ReadableStreamInterface|null|DuplexStreamInterface|WritableStreamInterface
6567
*/
6668
public $stdout;
6769

6870
/**
69-
* @var ReadableStreamInterface|null|WritableStreamInterface
71+
* @var ReadableStreamInterface|null|DuplexStreamInterface|WritableStreamInterface
7072
*/
7173
public $stderr;
7274

@@ -79,7 +81,7 @@ class Process extends EventEmitter
7981
* - 1: STDOUT (`ReadableStreamInterface`)
8082
* - 2: STDERR (`ReadableStreamInterface`)
8183
*
82-
* @var array<ReadableStreamInterface|WritableStreamInterface>
84+
* @var array<ReadableStreamInterface|WritableStreamInterface|DuplexStreamInterface>
8385
*/
8486
public $pipes = array();
8587

@@ -229,7 +231,10 @@ public function start(LoopInterface $loop, $interval = 0.1)
229231
}
230232

231233
foreach ($pipes as $n => $fd) {
232-
if (\strpos($this->fds[$n][1], 'w') === false) {
234+
$meta = \stream_get_meta_data($fd);
235+
if ($meta['mode'] === 'r+') {
236+
$stream = new DuplexResourceStream($fd, $loop);
237+
} elseif ($meta['mode'] === 'w') {
233238
$stream = new WritableResourceStream($fd, $loop);
234239
} else {
235240
$stream = new ReadableResourceStream($fd, $loop);

tests/AbstractProcessTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,33 @@ public function testStartWillAssignPipes()
4949
$this->assertSame($process->stderr, $process->pipes[2]);
5050
}
5151

52+
/**
53+
* @depends testStartWillAssignPipes
54+
* @requires PHP 8
55+
*/
56+
public function testStartWithSocketDescriptorsWillAssignDuplexPipes()
57+
{
58+
$process = new Process(
59+
(DIRECTORY_SEPARATOR === '\\' ? 'cmd /c ' : '') . 'echo foo',
60+
null,
61+
null,
62+
array(
63+
array('socket'),
64+
array('socket'),
65+
array('socket')
66+
)
67+
);
68+
$process->start($this->createLoop());
69+
70+
$this->assertInstanceOf('React\Stream\DuplexStreamInterface', $process->stdin);
71+
$this->assertInstanceOf('React\Stream\DuplexStreamInterface', $process->stdout);
72+
$this->assertInstanceOf('React\Stream\DuplexStreamInterface', $process->stderr);
73+
$this->assertCount(3, $process->pipes);
74+
$this->assertSame($process->stdin, $process->pipes[0]);
75+
$this->assertSame($process->stdout, $process->pipes[1]);
76+
$this->assertSame($process->stderr, $process->pipes[2]);
77+
}
78+
5279
public function testStartWithoutAnyPipesWillNotAssignPipes()
5380
{
5481
if (DIRECTORY_SEPARATOR === '\\') {
@@ -211,6 +238,35 @@ public function testReceivesProcessStdoutFromEcho()
211238
$this->assertEquals('test', rtrim($buffer));
212239
}
213240

241+
/**
242+
* @requires PHP 8
243+
*/
244+
public function testReceivesProcessStdoutFromEchoViaSocketDescriptors()
245+
{
246+
$loop = $this->createLoop();
247+
$process = new Process(
248+
$this->getPhpBinary() . ' -r ' . escapeshellarg('echo \'test\';'),
249+
null,
250+
null,
251+
array(
252+
array('socket'),
253+
array('socket'),
254+
array('socket')
255+
)
256+
);
257+
$process->start($loop);
258+
259+
$buffer = '';
260+
$process->stdout->on('data', function ($data) use (&$buffer) {
261+
$buffer .= $data;
262+
});
263+
$process->stderr->on('data', 'var_dump');
264+
265+
$loop->run();
266+
267+
$this->assertEquals('test', rtrim($buffer));
268+
}
269+
214270
public function testReceivesProcessOutputFromStdoutRedirectedToFile()
215271
{
216272
$tmp = tmpfile();

0 commit comments

Comments
 (0)