From 5be4e4ab0ec353b60a7492b2c389f656e2bd0bd5 Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Mon, 9 Dec 2013 18:58:46 +1100 Subject: [PATCH 1/2] [Process] Redirect output without a shell Currently processes that output large amounts of data will block. If the output is not important then this becomes an issue. It can be worked around by redirecting the output to > /dev/null but this requires an instance of /bin/sh to do the work. This patch adds the ability to set the processPipes, and a new processPipe that redirects to /dev/null. | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #9007 | License | MIT | Doc PR | https://github.com/symfony/symfony-docs/pull/3303 --- .../Component/Process/NullProcessPipes.php | 38 +++++++++++++++++++ src/Symfony/Component/Process/Process.php | 13 ++++++- .../Process/Tests/HeavyOutputtingProcess.php | 9 +++++ .../Process/Tests/NullProcessPipesTest.php | 26 +++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Process/NullProcessPipes.php create mode 100644 src/Symfony/Component/Process/Tests/HeavyOutputtingProcess.php create mode 100644 src/Symfony/Component/Process/Tests/NullProcessPipesTest.php diff --git a/src/Symfony/Component/Process/NullProcessPipes.php b/src/Symfony/Component/Process/NullProcessPipes.php new file mode 100644 index 0000000000000..438c1c0581474 --- /dev/null +++ b/src/Symfony/Component/Process/NullProcessPipes.php @@ -0,0 +1,38 @@ + + * + * 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; + +/** + * NullProcessPipes allow redirecting output to dev/null without a subshell. Useful for processes that communicate + * over other means. + */ +class NullProcessPipes extends ProcessPipes +{ + /** + * Returns an array of descriptors for the use of proc_open. + * + * @return array + */ + public function getDescriptors() + { + $nullfile = defined('PHP_WINDOWS_VERSION_BUILD') ? 'NUL' : '/dev/null'; + return array( + + array('pipe', 'r'), // stdin + array('file', $nullfile, 'a+'), // stdout + array('file', $nullfile, 'a+'), //stderr + ); + } +} + diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 39caa3308f7e3..0ec4981e8a9f8 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -991,6 +991,15 @@ public function checkTimeout() } } + /** + * Sets the process pipes to use. + * + * @param ProcessPipes $pipes + */ + public function setProcessPipes(ProcessPipes $pipes) { + $this->processPipes = $pipes; + } + /** * Creates the descriptors needed by the proc_open. * @@ -998,7 +1007,9 @@ public function checkTimeout() */ private function getDescriptors() { - $this->processPipes = new ProcessPipes($this->useFileHandles); + if (!$this->processPipes) { + $this->processPipes = new ProcessPipes($this->useFileHandles); + } $descriptors = $this->processPipes->getDescriptors(); if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { diff --git a/src/Symfony/Component/Process/Tests/HeavyOutputtingProcess.php b/src/Symfony/Component/Process/Tests/HeavyOutputtingProcess.php new file mode 100644 index 0000000000000..c350fbd995a5b --- /dev/null +++ b/src/Symfony/Component/Process/Tests/HeavyOutputtingProcess.php @@ -0,0 +1,9 @@ +markTestSkipped('Windows does not support NullProcessPipes'); + } + $process->setProcessPipes(new NullProcessPipes()); + $process->start(); + + while($process->isRunning()) { + usleep(100e3); + } + + // No output! + $this->assertEquals('', $process->getOutput()); + } +} From f7a9f2c0576a031dbbb56c1cc6cc42a354eaed3e Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Tue, 10 Dec 2013 10:42:17 +1100 Subject: [PATCH 2/2] Review feedback for setProcessPipes --- src/Symfony/Component/Process/Process.php | 7 +++- .../Component/Process/ProcessBuilder.php | 22 +++++++++++- .../Process/Tests/NullProcessPipesTest.php | 35 +++++++++++++++---- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 0ec4981e8a9f8..0eb26476a12c9 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -995,8 +995,13 @@ public function checkTimeout() * Sets the process pipes to use. * * @param ProcessPipes $pipes + * @throws LogicException If called after start() */ - public function setProcessPipes(ProcessPipes $pipes) { + public function setProcessPipes(ProcessPipes $pipes) + { + if ($this->status !== self::STATUS_READY) { + throw new LogicException('ProcessPipes cannot be changed after the process has started'); + } $this->processPipes = $pipes; } diff --git a/src/Symfony/Component/Process/ProcessBuilder.php b/src/Symfony/Component/Process/ProcessBuilder.php index b6168feb62d0e..dcb1b18c445c8 100644 --- a/src/Symfony/Component/Process/ProcessBuilder.php +++ b/src/Symfony/Component/Process/ProcessBuilder.php @@ -29,6 +29,7 @@ class ProcessBuilder private $options = array(); private $inheritEnv = true; private $prefix = array(); + private $processPipes; public function __construct(array $arguments = array()) { @@ -117,6 +118,20 @@ public function setInput($stdin) return $this; } + /** + * Sets the process pipes to use for the new process + * + * @param ProcessPipes $pipes + * + * @return ProcessBuilder + */ + public function setProcessPipes(ProcessPipes $pipes) + { + $this->processPipes = $pipes; + + return $this; + } + /** * Sets the process timeout. * @@ -172,6 +187,11 @@ public function getProcess() $env = $this->env; } - return new Process($script, $this->cwd, $env, $this->stdin, $this->timeout, $options); + $process = new Process($script, $this->cwd, $env, $this->stdin, $this->timeout, $options); + if ($this->processPipes) { + $process->setProcessPipes($this->processPipes); + } + + return $process; } } diff --git a/src/Symfony/Component/Process/Tests/NullProcessPipesTest.php b/src/Symfony/Component/Process/Tests/NullProcessPipesTest.php index 876d80dfd0835..8e3c91e50f9fe 100644 --- a/src/Symfony/Component/Process/Tests/NullProcessPipesTest.php +++ b/src/Symfony/Component/Process/Tests/NullProcessPipesTest.php @@ -1,26 +1,47 @@ markTestSkipped('Windows does not support NullProcessPipes'); - } + $process->setProcessPipes(new NullProcessPipes()); $process->start(); - while($process->isRunning()) { + while ($process->isRunning()) { usleep(100e3); } // No output! $this->assertEquals('', $process->getOutput()); } + + public function testReassigningPipesAfterStartIsNotAllowed() + { + $process = new Process('php -r "sleep(1);'); + + $process->setProcessPipes(new NullProcessPipes()); + $process->start(); + + $this->setExpectedException('Symfony\Component\Process\Exception\LogicException'); + $process->setProcessPipes(new ProcessPipes()); + } + + public function testRestartedProcesses() { + $process = new Process('echo asdf'); + + $process->run(); + $this->assertEquals("asdf\n", $process->getOutput()); + + $process->run(); + $this->assertEquals("asdf\n", $process->getOutput()); + } }