diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index c65793bf0eaea..9e1be7ef0e721 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -27,7 +27,7 @@ * @author Fabien Potencier * @author Romain Neutron */ -class Process +class Process implements \IteratorAggregate { const ERR = 'err'; const OUT = 'out'; @@ -498,6 +498,54 @@ public function getIncrementalOutput() return $latest; } + /** + * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR). + * + * @param bool $blocking Whether to use a blocking read call. + * @param bool $clearOutput Whether to clear or keep output in memory. + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + * + * @return \Generator + */ + public function getIterator($blocking = true, $clearOutput = true) + { + $this->readPipesForOutput(__FUNCTION__, false); + + while (null !== $this->callback) { + $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); + + if (isset($out[0])) { + if ($clearOutput) { + $this->clearOutput(); + } else { + $this->incrementalOutputOffset = ftell($this->stdout); + } + + yield self::OUT => $out; + } + + $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); + + if (isset($err[0])) { + if ($clearOutput) { + $this->clearErrorOutput(); + } else { + $this->incrementalErrorOutputOffset = ftell($this->stderr); + } + + yield self::ERR => $err; + } + + if (!$blocking && !isset($out[0]) && !isset($err[0])) { + yield self::OUT => ''; + } + + $this->readPipesForOutput(__FUNCTION__, $blocking); + } + } + /** * Clears the process output. * @@ -1296,11 +1344,12 @@ protected function isSigchildEnabled() /** * Reads pipes for the freshest output. * - * @param $caller The name of the method that needs fresh outputs + * @param string $caller The name of the method that needs fresh outputs + * @param bool $blocking Whether to use blocking calls or not. * * @throws LogicException in case output has been disabled or process is not started */ - private function readPipesForOutput($caller) + private function readPipesForOutput($caller, $blocking = false) { if ($this->outputDisabled) { throw new LogicException('Output has been disabled.'); @@ -1308,7 +1357,7 @@ private function readPipesForOutput($caller) $this->requireProcessIsStarted($caller); - $this->updateStatus(false); + $this->updateStatus($blocking); } /** diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index a488e3d110f67..7ccd0415dc2b6 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -1270,6 +1270,42 @@ public function testInputStreamOnEmpty() $this->assertSame('123456', $process->getOutput()); } + public function testIteratorOutput() + { + $input = new InputStream(); + + $process = new Process(self::$phpBin.' -r '.escapeshellarg('fwrite(STDOUT, 123); fwrite(STDERR, 234); fwrite(STDOUT, fread(STDIN, 3)); fwrite(STDERR, 456);')); + $process->setInput($input); + $process->start(); + $output = array(); + + foreach ($process as $type => $data) { + $output[] = array($type, $data); + break; + } + $expectedOutput = array( + array($process::OUT, '123'), + ); + $this->assertSame($expectedOutput, $output); + + $input->write(345); + + foreach ($process as $type => $data) { + $output[] = array($type, $data); + } + + $this->assertSame('', $process->getOutput()); + $this->assertFalse($process->isRunning()); + + $expectedOutput = array( + array($process::OUT, '123'), + array($process::ERR, '234'), + array($process::OUT, '345'), + array($process::ERR, '456'), + ); + $this->assertSame($expectedOutput, $output); + } + /** * @param string $commandline * @param null|string $cwd