From b57024e68c136e343e3532c1f54194bf7e585036 Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Tue, 22 Apr 2014 23:51:46 +0200 Subject: [PATCH] [Process] Add support for streams as input --- .../Component/Process/Pipes/AbstractPipes.php | 74 ++++ .../Process/Pipes/PipesInterface.php | 60 +++ .../Component/Process/Pipes/UnixPipes.php | 214 ++++++++++ .../Component/Process/Pipes/WindowsPipes.php | 254 ++++++++++++ src/Symfony/Component/Process/Process.php | 27 +- .../Component/Process/ProcessPipes.php | 380 ------------------ .../Component/Process/ProcessUtils.php | 5 +- .../Process/Tests/AbstractProcessTest.php | 31 +- .../Process/Tests/ProcessBuilderTest.php | 2 +- 9 files changed, 646 insertions(+), 401 deletions(-) create mode 100644 src/Symfony/Component/Process/Pipes/AbstractPipes.php create mode 100644 src/Symfony/Component/Process/Pipes/PipesInterface.php create mode 100644 src/Symfony/Component/Process/Pipes/UnixPipes.php create mode 100644 src/Symfony/Component/Process/Pipes/WindowsPipes.php delete mode 100644 src/Symfony/Component/Process/ProcessPipes.php diff --git a/src/Symfony/Component/Process/Pipes/AbstractPipes.php b/src/Symfony/Component/Process/Pipes/AbstractPipes.php new file mode 100644 index 0000000000..d8b57d07a7 --- /dev/null +++ b/src/Symfony/Component/Process/Pipes/AbstractPipes.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +/** + * @author Romain Neutron + * + * @internal + */ +abstract class AbstractPipes implements PipesInterface +{ + /** @var array */ + public $pipes = array(); + + /** @var string */ + protected $inputBuffer = ''; + /** @var resource|null */ + protected $input; + + /** @var bool */ + private $blocked = true; + + /** + * {@inheritdoc} + */ + public function close() + { + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + $this->pipes = array(); + } + + /** + * Returns true if a system call has been interrupted. + * + * @return bool + */ + protected function hasSystemCallBeenInterrupted() + { + $lastError = error_get_last(); + + // stream_select returns false when the `select` system call is interrupted by an incoming signal + return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); + } + + /** + * Unblocks streams + */ + protected function unblock() + { + if (!$this->blocked) { + return; + } + + foreach ($this->pipes as $pipe) { + stream_set_blocking($pipe, 0); + } + if (null !== $this->input) { + stream_set_blocking($this->input, 0); + } + + $this->blocked = false; + } +} diff --git a/src/Symfony/Component/Process/Pipes/PipesInterface.php b/src/Symfony/Component/Process/Pipes/PipesInterface.php new file mode 100644 index 0000000000..09d3f61d6e --- /dev/null +++ b/src/Symfony/Component/Process/Pipes/PipesInterface.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +/** + * PipesInterface manages descriptors and pipes for the use of proc_open. + * + * @author Romain Neutron + * + * @internal + */ +interface PipesInterface +{ + const CHUNK_SIZE = 16384; + + /** + * Returns an array of descriptors for the use of proc_open. + * + * @return array + */ + public function getDescriptors(); + + /** + * Returns an array of filenames indexed by their related stream in case these pipes use temporary files. + * + * @return string[] + */ + public function getFiles(); + + /** + * Reads data in file handles and pipes. + * + * @param bool $blocking Whether to use blocking calls or not. + * @param bool $close Whether to close pipes if they've reached EOF. + * + * @return string[] An array of read data indexed by their fd. + */ + public function readAndWrite($blocking, $close = false); + + /** + * Returns if the current state has open file handles or pipes. + * + * @return bool + */ + public function areOpen(); + + /** + * Closes file handles and pipes. + */ + public function close(); +} diff --git a/src/Symfony/Component/Process/Pipes/UnixPipes.php b/src/Symfony/Component/Process/Pipes/UnixPipes.php new file mode 100644 index 0000000000..f6666fc9df --- /dev/null +++ b/src/Symfony/Component/Process/Pipes/UnixPipes.php @@ -0,0 +1,214 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Process; + +/** + * UnixPipes implementation uses unix pipes as handles. + * + * @author Romain Neutron + * + * @internal + */ +class UnixPipes extends AbstractPipes +{ + /** @var bool */ + private $ttyMode; + /** @var bool */ + private $ptyMode; + /** @var bool */ + private $disableOutput; + + public function __construct($ttyMode, $ptyMode, $input, $disableOutput) + { + $this->ttyMode = (bool) $ttyMode; + $this->ptyMode = (bool) $ptyMode; + $this->disableOutput = (bool) $disableOutput; + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = (string) $input; + } + } + + public function __destruct() + { + $this->close(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('/dev/null', 'c'); + + return array( + array('pipe', 'r'), + $nullstream, + $nullstream, + ); + } + + if ($this->ttyMode) { + return array( + array('file', '/dev/tty', 'r'), + array('file', '/dev/tty', 'w'), + array('file', '/dev/tty', 'w'), + ); + } + + if ($this->ptyMode && Process::isPtySupported()) { + return array( + array('pty'), + array('pty'), + array('pty'), + ); + } + + return array( + array('pipe', 'r'), + array('pipe', 'w'), // stdout + array('pipe', 'w'), // stderr + ); + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return array(); + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + // only stdin is left open, job has been done ! + // we can now close it + if (1 === count($this->pipes) && array(0) === array_keys($this->pipes)) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + if (empty($this->pipes)) { + return array(); + } + + $this->unblock(); + + $read = array(); + + if (null !== $this->input) { + // if input is a resource, let's add it to stream_select argument to + // fill a buffer + $r = array_merge($this->pipes, array('input' => $this->input)); + } else { + $r = $this->pipes; + } + // discard read on stdin + unset ($r[0]); + + $w = isset($this->pipes[0]) ? array($this->pipes[0]) : null; + $e = null; + + // let's have a look if something changed in streams + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + // if a system call has been interrupted, forget about it, let's try again + // otherwise, an error occurred, let's reset pipes + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = array(); + } + + return $read; + } + + // nothing has changed + if (0 === $n) { + return $read; + } + + foreach ($r as $pipe) { + // prior PHP 5.4 the array passed to stream_select is modified and + // lose key association, we have to find back the key + $type = (false !== $found = array_search($pipe, $this->pipes)) ? $found : 'input'; + $data = ''; + while ($dataread = fread($pipe, self::CHUNK_SIZE)) { + $data .= $dataread; + } + + if ($data) { + if ($type === 'input') { + $this->inputBuffer .= $data; + } else { + $read[$type] = $data; + } + } + + if (false === $data || (true === $close && feof($pipe) && '' === $data)) { + if ($type === 'input') { + // no more data to read on input resource + // use an empty buffer in the next reads + $this->input = null; + } else { + fclose($this->pipes[$type]); + unset($this->pipes[$type]); + } + } + } + + if (null !== $w && 0 < count($w)) { + while ($len = strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2<<18); // write 512k + if ($written > 0) { + $this->inputBuffer = (string) substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + // no input to read on resource, buffer is empty and stdin still open + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool) $this->pipes; + } + + /** + * Creates a new UnixPipes instance + * + * @param Process $process + * @param string|resource $input + * + * @return UnixPipes + */ + public static function create(Process $process, $input) + { + return new static($process->isTty(), $process->isPty(), $input, $process->isOutputDisabled()); + } +} diff --git a/src/Symfony/Component/Process/Pipes/WindowsPipes.php b/src/Symfony/Component/Process/Pipes/WindowsPipes.php new file mode 100644 index 0000000000..e90073c43b --- /dev/null +++ b/src/Symfony/Component/Process/Pipes/WindowsPipes.php @@ -0,0 +1,254 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Process; +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * WindowsPipes implementation uses temporary files as handles. + * + * @see https://bugs.php.net/bug.php?id=51800 + * @see https://bugs.php.net/bug.php?id=65650 + * + * @author Romain Neutron + * + * @internal + */ +class WindowsPipes extends AbstractPipes +{ + /** @var array */ + private $files = array(); + /** @var array */ + private $fileHandles = array(); + /** @var array */ + private $readBytes = array( + Process::STDOUT => 0, + Process::STDERR => 0, + ); + /** @var bool */ + private $disableOutput; + + public function __construct($disableOutput, $input) + { + $this->$disableOutput = (bool) $disableOutput; + + if (!$this->disableOutput) { + // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. + // Workaround for this problem is to use temporary files instead of pipes on Windows platform. + // + // @see https://bugs.php.net/bug.php?id=51800 + $this->files = array( + Process::STDOUT => tempnam(sys_get_temp_dir(), 'sf_proc_stdout'), + Process::STDERR => tempnam(sys_get_temp_dir(), 'sf_proc_stderr'), + ); + foreach ($this->files as $offset => $file) { + $this->fileHandles[$offset] = fopen($this->files[$offset], 'rb'); + if (false === $this->fileHandles[$offset]) { + throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); + } + } + } + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = $input; + } + } + + public function __destruct() + { + $this->close(); + $this->removeFiles(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('NUL', 'c'); + + return array( + array('pipe', 'r'), + $nullstream, + $nullstream, + ); + } + + // We're not using pipe on Windows platform as it hangs (https://bugs.php.net/bug.php?id=51800) + // We're not using file handles as it can produce corrupted output https://bugs.php.net/bug.php?id=65650 + // So we redirect output within the commandline and pass the nul device to the process + return array( + array('pipe', 'r'), + array('file', 'NUL', 'w'), + array('file', 'NUL', 'w'), + ); + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return $this->files; + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + $this->write($blocking, $close); + + $read = array(); + $fh = $this->fileHandles; + foreach ($fh as $type => $fileHandle) { + if (0 !== fseek($fileHandle, $this->readBytes[$type])) { + continue; + } + $data = ''; + $dataread = null; + while (!feof($fileHandle)) { + if (false !== $dataread = fread($fileHandle, self::CHUNK_SIZE)) { + $data .= $dataread; + } + } + if (0 < $length = strlen($data)) { + $this->readBytes[$type] += $length; + $read[$type] = $data; + } + + if (false === $dataread || (true === $close && feof($fileHandle) && '' === $data)) { + fclose($this->fileHandles[$type]); + unset($this->fileHandles[$type]); + } + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool) $this->pipes && (bool) $this->fileHandles; + } + + /** + * {@inheritdoc} + */ + public function close() + { + parent::close(); + foreach ($this->fileHandles as $handle) { + fclose($handle); + } + $this->fileHandles = array(); + } + + /** + * Creates a new WindowsPipes instance. + * + * @param Process $process The process + * @param $input + * + * @return WindowsPipes + */ + public static function create(Process $process, $input) + { + return new static($process->isOutputDisabled(), $input); + } + + /** + * Removes temporary files + */ + private function removeFiles() + { + foreach ($this->files as $filename) { + if (file_exists($filename)) { + @unlink($filename); + } + } + $this->files = array(); + } + + /** + * Writes input to stdin + * + * @param bool $blocking + * @param bool $close + */ + private function write($blocking, $close) + { + if (empty($this->pipes)) { + return; + } + + $this->unblock(); + + $r = null !== $this->input ? array('input' => $this->input) : null; + $w = isset($this->pipes[0]) ? array($this->pipes[0]) : null; + $e = null; + + // let's have a look if something changed in streams + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + // if a system call has been interrupted, forget about it, let's try again + // otherwise, an error occurred, let's reset pipes + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = array(); + } + + return; + } + + // nothing has changed + if (0 === $n) { + return; + } + + if (null !== $w && 0 < count($r)) { + $data = ''; + while ($dataread = fread($r['input'], self::CHUNK_SIZE)) { + $data .= $dataread; + } + + $this->inputBuffer .= $data; + + if (false === $data || (true === $close && feof($r['input']) && '' === $data)) { + // no more data to read on input resource + // use an empty buffer in the next reads + unset($this->input); + } + } + + if (null !== $w && 0 < count($w)) { + while ($len = strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2<<18); + if ($written > 0) { + $this->inputBuffer = (string) substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + // no input to read on resource, buffer is empty and stdin still open + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + } +} diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index c0d8863ca1..75b2bdd584 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -16,12 +16,16 @@ use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\Pipes\PipesInterface; +use Symfony\Component\Process\Pipes\UnixPipes; +use Symfony\Component\Process\Pipes\WindowsPipes; /** * Process is a thin wrapper around proc_* functions to easily * start independent PHP processes. * * @author Fabien Potencier + * @author Romain Neutron * * @api */ @@ -67,7 +71,7 @@ class Process private $pty; private $useFileHandles = false; - /** @var ProcessPipes */ + /** @var PipesInterface */ private $processPipes; private static $sigchild; @@ -282,13 +286,10 @@ class Process } $this->status = self::STATUS_STARTED; - $this->processPipes->unblock(); - if ($this->tty) { return; } - $this->processPipes->write(false, $this->input); $this->updateStatus(false); $this->checkTimeout(); } @@ -346,7 +347,7 @@ class Process do { $this->checkTimeout(); - $running = defined('PHP_WINDOWS_VERSION_BUILD') ? $this->isRunning() : $this->processPipes->hasOpenHandles(); + $running = defined('PHP_WINDOWS_VERSION_BUILD') ? $this->isRunning() : $this->processPipes->areOpen(); $close = !defined('PHP_WINDOWS_VERSION_BUILD') || !$running;; $this->readPipes(true, $close); } while ($running); @@ -1060,8 +1061,6 @@ class Process /** * Sets the contents of STDIN. * - * Deprecation: As of Symfony 2.5, this method only accepts scalar values. - * * @param string|null $stdin The new contents * * @return self The current Process instance @@ -1238,8 +1237,12 @@ class Process */ private function getDescriptors() { - $this->processPipes = new ProcessPipes($this->useFileHandles, $this->tty, $this->pty, $this->outputDisabled); - $descriptors = $this->processPipes->getDescriptors(); + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $this->processPipes = WindowsPipes::create($this, $this->input); + } else { + $this->processPipes = UnixPipes::create($this, $this->input); + } + $descriptors = $this->processPipes->getDescriptors($this->outputDisabled); if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { // last exit code is output on the fourth pipe and caught to work around --enable-sigchild @@ -1351,11 +1354,7 @@ class Process */ private function readPipes($blocking, $close) { - if ($close) { - $result = $this->processPipes->readAndCloseHandles($blocking); - } else { - $result = $this->processPipes->read($blocking); - } + $result = $this->processPipes->readAndWrite($blocking, $close); foreach ($result as $type => $data) { if (3 == $type) { diff --git a/src/Symfony/Component/Process/ProcessPipes.php b/src/Symfony/Component/Process/ProcessPipes.php deleted file mode 100644 index 3cc6115580..0000000000 --- a/src/Symfony/Component/Process/ProcessPipes.php +++ /dev/null @@ -1,380 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Process; - -use Symfony\Component\Process\Exception\RuntimeException; - -/** - * ProcessPipes manages descriptors and pipes for the use of proc_open. - */ -class ProcessPipes -{ - /** @var array */ - public $pipes = array(); - /** @var array */ - private $files = array(); - /** @var array */ - private $fileHandles = array(); - /** @var array */ - private $readBytes = array(); - /** @var bool */ - private $useFiles; - /** @var bool */ - private $ttyMode; - /** @var bool */ - private $ptyMode; - /** @var bool */ - private $disableOutput; - - const CHUNK_SIZE = 16384; - - public function __construct($useFiles, $ttyMode, $ptyMode = false, $disableOutput = false) - { - $this->useFiles = (bool) $useFiles; - $this->ttyMode = (bool) $ttyMode; - $this->ptyMode = (bool) $ptyMode; - $this->disableOutput = (bool) $disableOutput; - - // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. - // Workaround for this problem is to use temporary files instead of pipes on Windows platform. - // - // @see https://bugs.php.net/bug.php?id=51800 - if ($this->useFiles && !$this->disableOutput) { - $this->files = array( - Process::STDOUT => tempnam(sys_get_temp_dir(), 'sf_proc_stdout'), - Process::STDERR => tempnam(sys_get_temp_dir(), 'sf_proc_stderr'), - ); - foreach ($this->files as $offset => $file) { - $this->fileHandles[$offset] = fopen($this->files[$offset], 'rb'); - if (false === $this->fileHandles[$offset]) { - throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); - } - } - $this->readBytes = array( - Process::STDOUT => 0, - Process::STDERR => 0, - ); - } - } - - public function __destruct() - { - $this->close(); - $this->removeFiles(); - } - - /** - * Sets non-blocking mode on pipes. - */ - public function unblock() - { - foreach ($this->pipes as $pipe) { - stream_set_blocking($pipe, 0); - } - } - - /** - * Closes file handles and pipes. - */ - public function close() - { - $this->closeUnixPipes(); - foreach ($this->fileHandles as $handle) { - fclose($handle); - } - $this->fileHandles = array(); - } - - /** - * Closes Unix pipes. - * - * Nothing happens in case file handles are used. - */ - public function closeUnixPipes() - { - foreach ($this->pipes as $pipe) { - fclose($pipe); - } - $this->pipes = array(); - } - - /** - * Returns an array of descriptors for the use of proc_open. - * - * @return array - */ - public function getDescriptors() - { - if ($this->disableOutput) { - $nullstream = fopen(defined('PHP_WINDOWS_VERSION_BUILD') ? 'NUL' : '/dev/null', 'c'); - - return array( - array('pipe', 'r'), - $nullstream, - $nullstream, - ); - } - - if ($this->useFiles) { - // We're not using pipe on Windows platform as it hangs (https://bugs.php.net/bug.php?id=51800) - // We're not using file handles as it can produce corrupted output https://bugs.php.net/bug.php?id=65650 - // So we redirect output within the commandline and pass the nul device to the process - return array( - array('pipe', 'r'), - array('file', 'NUL', 'w'), - array('file', 'NUL', 'w'), - ); - } - - if ($this->ttyMode) { - return array( - array('file', '/dev/tty', 'r'), - array('file', '/dev/tty', 'w'), - array('file', '/dev/tty', 'w'), - ); - } elseif ($this->ptyMode && Process::isPtySupported()) { - return array( - array('pty'), - array('pty'), - array('pty'), - ); - } - - return array( - array('pipe', 'r'), // stdin - array('pipe', 'w'), // stdout - array('pipe', 'w'), // stderr - ); - } - - /** - * Returns an array of filenames indexed by their related stream in case these pipes use temporary files. - * - * @return array - */ - public function getFiles() - { - if ($this->useFiles) { - return $this->files; - } - - return array(); - } - - /** - * Reads data in file handles and pipes. - * - * @param bool $blocking Whether to use blocking calls or not. - * - * @return array An array of read data indexed by their fd. - */ - public function read($blocking) - { - return array_replace($this->readStreams($blocking), $this->readFileHandles()); - } - - /** - * Reads data in file handles and pipes, closes them if EOF is reached. - * - * @param bool $blocking Whether to use blocking calls or not. - * - * @return array An array of read data indexed by their fd. - */ - public function readAndCloseHandles($blocking) - { - return array_replace($this->readStreams($blocking, true), $this->readFileHandles(true)); - } - - /** - * Returns if the current state has open file handles or pipes. - * - * @return bool - */ - public function hasOpenHandles() - { - if (!$this->useFiles) { - return (bool) $this->pipes; - } - - return (bool) $this->pipes && (bool) $this->fileHandles; - } - - /** - * Writes stdin data. - * - * @param bool $blocking Whether to use blocking calls or not. - * @param string|null $stdin The data to write. - */ - public function write($blocking, $stdin) - { - if (null === $stdin) { - fclose($this->pipes[0]); - unset($this->pipes[0]); - - return; - } - - $writePipes = array($this->pipes[0]); - unset($this->pipes[0]); - $stdinLen = strlen($stdin); - $stdinOffset = 0; - - while ($writePipes) { - $r = null; - $w = $writePipes; - $e = null; - - if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? ceil(Process::TIMEOUT_PRECISION * 1E6) : 0)) { - // if a system call has been interrupted, forget about it, let's try again - if ($this->hasSystemCallBeenInterrupted()) { - continue; - } - break; - } - - // nothing has changed, let's wait until the process is ready - if (0 === $n) { - continue; - } - - if ($w) { - $written = fwrite($writePipes[0], (binary) substr($stdin, $stdinOffset), 8192); - if (false !== $written) { - $stdinOffset += $written; - } - if ($stdinOffset >= $stdinLen) { - fclose($writePipes[0]); - $writePipes = null; - } - } - } - } - - /** - * Reads data in file handles. - * - * @param bool $close Whether to close file handles or not. - * - * @return array An array of read data indexed by their fd. - */ - private function readFileHandles($close = false) - { - $read = array(); - $fh = $this->fileHandles; - foreach ($fh as $type => $fileHandle) { - if (0 !== fseek($fileHandle, $this->readBytes[$type])) { - continue; - } - $data = ''; - $dataread = null; - while (!feof($fileHandle)) { - if (false !== $dataread = fread($fileHandle, self::CHUNK_SIZE)) { - $data .= $dataread; - } - } - if (0 < $length = strlen($data)) { - $this->readBytes[$type] += $length; - $read[$type] = $data; - } - - if (false === $dataread || (true === $close && feof($fileHandle) && '' === $data)) { - fclose($this->fileHandles[$type]); - unset($this->fileHandles[$type]); - } - } - - return $read; - } - - /** - * Reads data in file pipes streams. - * - * @param bool $blocking Whether to use blocking calls or not. - * @param bool $close Whether to close file handles or not. - * - * @return array An array of read data indexed by their fd. - */ - private function readStreams($blocking, $close = false) - { - if (empty($this->pipes)) { - return array(); - } - - $read = array(); - - $r = $this->pipes; - $w = null; - $e = null; - - // let's have a look if something changed in streams - if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? ceil(Process::TIMEOUT_PRECISION * 1E6) : 0)) { - // if a system call has been interrupted, forget about it, let's try again - // otherwise, an error occurred, let's reset pipes - if (!$this->hasSystemCallBeenInterrupted()) { - $this->pipes = array(); - } - - return $read; - } - - // nothing has changed - if (0 === $n) { - return $read; - } - - foreach ($r as $pipe) { - $type = array_search($pipe, $this->pipes); - - $data = ''; - while ($dataread = fread($pipe, self::CHUNK_SIZE)) { - $data .= $dataread; - } - - if ($data) { - $read[$type] = $data; - } - - if (false === $data || (true === $close && feof($pipe) && '' === $data)) { - fclose($this->pipes[$type]); - unset($this->pipes[$type]); - } - } - - return $read; - } - - /** - * Returns true if a system call has been interrupted. - * - * @return bool - */ - private function hasSystemCallBeenInterrupted() - { - $lastError = error_get_last(); - - // stream_select returns false when the `select` system call is interrupted by an incoming signal - return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); - } - - /** - * Removes temporary files - */ - private function removeFiles() - { - foreach ($this->files as $filename) { - if (file_exists($filename)) { - @unlink($filename); - } - } - $this->files = array(); - } -} diff --git a/src/Symfony/Component/Process/ProcessUtils.php b/src/Symfony/Component/Process/ProcessUtils.php index 35ae17c508..2ec321cc12 100644 --- a/src/Symfony/Component/Process/ProcessUtils.php +++ b/src/Symfony/Component/Process/ProcessUtils.php @@ -87,6 +87,9 @@ class ProcessUtils public static function validateInput($caller, $input) { if (null !== $input) { + if (is_resource($input)) { + return $input; + } if (is_scalar($input)) { return (string) $input; } @@ -95,7 +98,7 @@ class ProcessUtils return (string) $input; } - throw new InvalidArgumentException(sprintf('%s only accepts strings.', $caller)); + throw new InvalidArgumentException(sprintf('%s only accepts strings or stream resources.', $caller)); } return $input; diff --git a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php index 4e0f4b5bb1..9872fee567 100644 --- a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php +++ b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php @@ -13,9 +13,9 @@ namespace Symfony\Component\Process\Tests; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Pipes\PipesInterface; use Symfony\Component\Process\Process; use Symfony\Component\Process\Exception\RuntimeException; -use Symfony\Component\Process\ProcessPipes; /** * @author Robert Schönthal @@ -90,9 +90,9 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase // has terminated so the internal pipes array is already empty. normally // the call to start() will not read any data as the process will not have // generated output, but this is non-deterministic so we must count it as - // a possibility. therefore we need 2 * ProcessPipes::CHUNK_SIZE plus + // a possibility. therefore we need 2 * PipesInterface::CHUNK_SIZE plus // another byte which will never be read. - $expectedOutputSize = ProcessPipes::CHUNK_SIZE * 2 + 2; + $expectedOutputSize = PipesInterface::CHUNK_SIZE * 2 + 2; $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize); $p = $this->getProcess(sprintf('php -r %s', escapeshellarg($code))); @@ -158,6 +158,28 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedLength, strlen($p->getErrorOutput())); } + /** + * @dataProvider pipesCodeProvider + */ + public function testSetStreamAsInput($code, $size) + { + $expected = str_repeat(str_repeat('*', 1024), $size) . '!'; + $expectedLength = (1024 * $size) + 1; + + $stream = fopen('php://temporary', 'w+'); + fwrite($stream, $expected); + rewind($stream); + + $p = $this->getProcess(sprintf('php -r %s', escapeshellarg($code))); + $p->setInput($stream); + $p->run(); + + fclose($stream); + + $this->assertEquals($expectedLength, strlen($p->getOutput())); + $this->assertEquals($expectedLength, strlen($p->getErrorOutput())); + } + public function testSetInputWhileRunningThrowsAnException() { $process = $this->getProcess('php -r "usleep(500000);"'); @@ -175,7 +197,7 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase /** * @dataProvider provideInvalidInputValues * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException - * @expectedExceptionMessage Symfony\Component\Process\Process::setInput only accepts strings. + * @expectedExceptionMessage Symfony\Component\Process\Process::setInput only accepts strings or stream resources. */ public function testInvalidInput($value) { @@ -188,7 +210,6 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase return array( array(array()), array(new NonStringifiable()), - array(fopen('php://temporary', 'w')), ); } diff --git a/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php b/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php index 56e4052075..e864f66234 100644 --- a/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php @@ -215,7 +215,7 @@ class ProcessBuilderTest extends \PHPUnit_Framework_TestCase /** * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException - * @expectedExceptionMessage Symfony\Component\Process\ProcessBuilder::setInput only accepts strings. + * @expectedExceptionMessage Symfony\Component\Process\ProcessBuilder::setInput only accepts strings or stream resources. */ public function testInvalidInput() {