Skip to content

Process close does not wait for stdout/stderr to drain #18

@igorwwwwwwwwwwwwwwwwwwww

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      pFad - Phonifier reborn

      Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy