Read standard output and standard error of child process using stream_select.
If a process filled up the buffer of the standard error pipe before closing the standard output pipe, it would hang. As a result, the call to stream_get_contents for the standard output pipe would not return, as the standard error pipe needs to be read from in order for the process to continue. This change uses stream_select to read from both pipes whenever data becomes available.
This commit is contained in:
parent
64b0f60368
commit
07c9264884
|
@ -92,14 +92,14 @@ abstract class BaseCommandExecutor implements CommandExecutor
|
||||||
$pipes = array();
|
$pipes = array();
|
||||||
$process = proc_open($command, $descriptorSpec, $pipes, $this->buildPath, null);
|
$process = proc_open($command, $descriptorSpec, $pipes, $this->buildPath, null);
|
||||||
|
|
||||||
|
$this->lastOutput = '';
|
||||||
|
$this->lastError = '';
|
||||||
|
|
||||||
if (is_resource($process)) {
|
if (is_resource($process)) {
|
||||||
fclose($pipes[0]);
|
fclose($pipes[0]);
|
||||||
|
|
||||||
$this->lastOutput = stream_get_contents($pipes[1]);
|
list($this->lastOutput, $this->lastError) =
|
||||||
$this->lastError = stream_get_contents($pipes[2]);
|
$this->readAlternating([$pipes[1], $pipes[2]]);
|
||||||
|
|
||||||
fclose($pipes[1]);
|
|
||||||
fclose($pipes[2]);
|
|
||||||
|
|
||||||
$status = proc_close($process);
|
$status = proc_close($process);
|
||||||
}
|
}
|
||||||
|
@ -125,6 +125,41 @@ abstract class BaseCommandExecutor implements CommandExecutor
|
||||||
return $rtn;
|
return $rtn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads from array of streams as data becomes available.
|
||||||
|
* @param array $descriptors
|
||||||
|
* @return string[] data read from each descriptor
|
||||||
|
*/
|
||||||
|
private function readAlternating(array $descriptors)
|
||||||
|
{
|
||||||
|
$outputs = [];
|
||||||
|
foreach ($descriptors as $key => $descriptor) {
|
||||||
|
stream_set_blocking($descriptor, false);
|
||||||
|
$outputs[$key] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
$read = $descriptors;
|
||||||
|
$write = null;
|
||||||
|
$except = null;
|
||||||
|
|
||||||
|
stream_select($read, $write, $except, null);
|
||||||
|
|
||||||
|
foreach ($read as $descriptor) {
|
||||||
|
$key = array_search($descriptor, $descriptors);
|
||||||
|
|
||||||
|
if (feof($descriptor)) {
|
||||||
|
fclose($descriptor);
|
||||||
|
unset($descriptors[$key]);
|
||||||
|
} else {
|
||||||
|
$outputs[$key] .= fgets($descriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (count($descriptors) > 0);
|
||||||
|
|
||||||
|
return $outputs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the output from the last command run.
|
* Returns the output from the last command run.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -58,6 +58,27 @@ class CommandExecutorTest extends \PHPUnit_Framework_TestCase
|
||||||
$this->assertFalse($returnValue);
|
$this->assertFalse($returnValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a script that generates an output that fills the standard error
|
||||||
|
* buffer first, followed by the standard output buffer. The function
|
||||||
|
* should be able to read from both streams, thereby preventing the child
|
||||||
|
* process from blocking because one of its buffers is full.
|
||||||
|
*/
|
||||||
|
public function testExecuteCommand_AlternatesBothBuffers()
|
||||||
|
{
|
||||||
|
$length = 80000;
|
||||||
|
$script = <<<EOD
|
||||||
|
/bin/sh -c 'data="$(printf %%${length}s | tr " " "-")"; >&2 echo "\$data"; >&1 echo "\$data"'
|
||||||
|
EOD;
|
||||||
|
$data = str_repeat("-", $length);
|
||||||
|
|
||||||
|
$returnValue = $this->testedExecutor->executeCommand(array($script));
|
||||||
|
$this->assertTrue($returnValue);
|
||||||
|
|
||||||
|
$this->assertEquals($data, trim($this->testedExecutor->getLastOutput()));
|
||||||
|
$this->assertEquals($data, trim($this->testedExecutor->getLastError()));
|
||||||
|
}
|
||||||
|
|
||||||
public function testFindBinary_ReturnsPathInSpecifiedRoot()
|
public function testFindBinary_ReturnsPathInSpecifiedRoot()
|
||||||
{
|
{
|
||||||
$thisFileName = "CommandExecutorTest.php";
|
$thisFileName = "CommandExecutorTest.php";
|
||||||
|
|
Loading…
Reference in a new issue