-
-
Notifications
You must be signed in to change notification settings - Fork 47
Description
I ran into an edge case with child-process that I suspect may also affect stream. First of all, here is a reproduce case:
<?php
use React\ChildProcess\Process;
use React\EventLoop\Factory;
use React\EventLoop\LoopInterface;
require 'vendor/autoload.php';
function run(LoopInterface $loop, $cmd, callable $callback) {
$process = new Process($cmd);
$stdOut = '';
$stdErr = '';
$process->on(
'exit',
function ($exitCode) use ($callback, $cmd, &$stdOut, &$stdErr) {
if ($exitCode) {
throw new \Exception('Error running command ' . $cmd . ': ' . $exitCode . PHP_EOL . $stdOut . PHP_EOL . $stdErr);
}
$callback($stdOut);
}
);
$process->start($loop);
$process->stdout->on(
'data',
function ($output) use (&$stdOut) {
$stdOut .= $output;
}
);
$process->stderr->on(
'data',
function ($output) use (&$stdErr) {
$stdErr .= $output;
}
);
}
function ssh($host, $cmd) {
return 'ssh -o "PasswordAuthentication no" -A ' . escapeshellarg($host) . ' ' . escapeshellarg($cmd);
}
// list of 17 hosts to SSH into
$hosts = [...];
$attempts = 0;
while (true) {
$attempts++;
echo '.';
if ($attempts % 60 === 0) {
echo "\n";
}
// StreamSelectLoop
$loop = Factory::create();
foreach ($hosts as $host) {
$loop->addTimer(
0.001,
function () use ($host, $loop, $attempts) {
run($loop, ssh($host, 'echo foo'), function ($value) use ($attempts) {
if ($value !== "foo\n") {
throw new \Exception('Found bad value ' . json_encode($value) . ' after ' . $attempts . ' attempts');
}
});
}
);
}
$loop->run();
}
The bad value returned here is an empty string.
I have been able to reproduce the problem in some cases without the timer and without the ssh command. But this is the most reliable reproduce case I have been able to come up with. On my local machine (running OSX) the problem is reproduced in 1-20 attempts. When running in a server environment it generally takes 20-200 attempts.
I managed to figure out that the process stdin's Stream::handleClose() is being called without the PHP stream's buffer being consumed.
A good way of seeing that is by replacing Stream::handleClose() with:
public function handleClose()
{
if (is_resource($this->stream)) {
$rest = stream_get_contents($this->stream);
if ($rest !== '') {
throw new \Exception($rest);
}
fclose($this->stream);
}
}
You will see that the string "foo\n" was still in the buffer. And also get a backtrace to where the close() call came from.
Stream already waits for buffered writes to go out before closing. But it does not process buffered reads. I believe that is the source of the bug.
Enjoy!