From faffe7e3a8a832580aa8090ba73b7c77f680fb54 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 1 Apr 2014 18:31:55 +0200 Subject: [PATCH 1/2] [Console] added a Process helper --- src/Symfony/Component/Console/CHANGELOG.md | 2 + .../Console/Helper/DebugFormatterHelper.php | 82 ++++++++++++++++ .../Console/Helper/ProcessHelper.php | 93 +++++++++++++++++++ src/Symfony/Component/Console/composer.json | 1 + 4 files changed, 178 insertions(+) create mode 100644 src/Symfony/Component/Console/Helper/DebugFormatterHelper.php create mode 100644 src/Symfony/Component/Console/Helper/ProcessHelper.php diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 9c5741b5e7..c741f156f7 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -5,11 +5,13 @@ CHANGELOG ----- * deprecated the dialog helper (use the question helper instead) + * added a Process helper * deprecated TableHelper in favor of Table * deprecated ProgressHelper in favor of ProgressBar * added a question helper * added a way to set the process name of a command * added a way to set a default command instead of `ListCommand` + * added a way to set the process title of a command 2.4.0 ----- diff --git a/src/Symfony/Component/Console/Helper/DebugFormatterHelper.php b/src/Symfony/Component/Console/Helper/DebugFormatterHelper.php new file mode 100644 index 0000000000..c7616cf898 --- /dev/null +++ b/src/Symfony/Component/Console/Helper/DebugFormatterHelper.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Helper\Helper; + +/** + * Helps outputting debug information when running an external program from a command. + * + * An external program can be a Process, an HTTP request, or anything else. + * + * @author Fabien Potencier + */ +class DebugFormatterHelper extends Helper +{ + private $colors = array('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'); + private $started = array(); + private $count = -1; + + public function start($id, $message, $prefix = 'RUN') + { + $this->started[$id] = array('border' => ++$this->count % count($this->colors)); + + return sprintf("%s %s %s\n", $this->getBorder($id), $prefix, $message); + } + + public function progress($id, $buffer, $error = false, $prefix = 'OUT', $errorPrefix = 'ERR') + { + $message = ''; + + if ($error) { + if (!isset($this->started[$id]['err'])) { + $message = sprintf("%s %s ", $this->getBorder($id), $errorPrefix); + $this->started[$id]['err'] = true; + } + + $message .= str_replace("\n", sprintf("\n%s %s ", $this->getBorder($id), $errorPrefix), $buffer); + } else { + if (!isset($this->started[$id]['out'])) { + $message = sprintf("%s %s ", $this->getBorder($id), $prefix); + $this->started[$id]['out'] = true; + } + + $message .= str_replace("\n", sprintf("\n%s %s ", $this->getBorder($id), $prefix), $buffer); + } + + return $message; + } + + public function stop($id, $message, $successful, $prefix = 'RES') + { + $trailingEOL = isset($this->started[$id]['out']) || isset($this->started[$id]['err']) ? "\n" : ''; + + if ($successful) { + return sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); + } + + return sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); + } + + private function getBorder($id) + { + return sprintf(' ', $this->colors[$this->started[$id]['border']]); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'debug_formatter'; + } +} diff --git a/src/Symfony/Component/Console/Helper/ProcessHelper.php b/src/Symfony/Component/Console/Helper/ProcessHelper.php new file mode 100644 index 0000000000..30c38c1ece --- /dev/null +++ b/src/Symfony/Component/Console/Helper/ProcessHelper.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Process; + +/** + * The Process class provides helpers to run external processes. + * + * @author Fabien Potencier + */ +class ProcessHelper extends Helper +{ + /** + * Runs an external process. + * + * @param OutputInterface $output An OutputInterface instance + * @param string|Process $cmd An instance of Process or a command to run + * @param string|null $error An error message that must be displayed if something went wrong + * @param callback|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return Process The process that ran + */ + public function run(OutputInterface $output, $cmd, $error = null, $callback = null) + { + $verbose = $output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE; + $debug = $output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG; + + $formatter = $this->getHelperSet()->get('debug_formatter'); + + $process = $cmd instanceof Process ? $cmd : new Process($cmd); + + if ($verbose) { + $output->write($formatter->start(spl_object_hash($process), $process->getCommandLine())); + } + + if ($debug) { + $callback = $this->wrapCallback($output, $process, $callback); + } + + $process->run($callback); + + if ($verbose) { + $message = $process->isSuccessful() ? 'Command ran successfully' : sprintf('%s Command did not run sucessfully', $process->getExitCode()); + $output->write($formatter->stop(spl_object_hash($process), $message, $process->isSuccessful())); + } + + if (!$process->isSuccessful() && null !== $error) { + $output->writeln(sprintf('%s'), $error); + } + + return $process; + } + + /** + * Wraps a Process callback to add debugging output. + * + * @param OutputInterface $output An OutputInterface interface + * @param callable|null $callback A PHP callable + */ + public function wrapCallback(OutputInterface $output, Process $process, $callback = null) + { + $formatter = $this->getHelperSet()->get('debug_formatter'); + + return function ($type, $buffer) use ($output, $process, $callback, $formatter) { + $output->write($formatter->progress(spl_object_hash($process), $buffer, 'err' === $type)); + + if (null !== $callback) { + $callback($type, $buffer); + } + }; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'process'; + } +} diff --git a/src/Symfony/Component/Console/composer.json b/src/Symfony/Component/Console/composer.json index 40131af463..ec597c558c 100644 --- a/src/Symfony/Component/Console/composer.json +++ b/src/Symfony/Component/Console/composer.json @@ -24,6 +24,7 @@ }, "suggest": { "symfony/event-dispatcher": "", + "symfony/process": "", "psr/log": "For using the console logger" }, "autoload": { From edc1bfeb2b1a5d2b4cab20e3728b6bc3919e689c Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Thu, 3 Apr 2014 10:19:54 +0200 Subject: [PATCH 2/2] [Console] Add process helper tests --- src/Symfony/Component/Console/Application.php | 4 + src/Symfony/Component/Console/CHANGELOG.md | 8 +- .../Console/Helper/DebugFormatterHelper.php | 57 ++++++++- .../Console/Helper/ProcessHelper.php | 74 +++++++++--- .../Tests/Helper/ProcessHelperTest.php | 108 ++++++++++++++++++ src/Symfony/Component/Console/composer.json | 1 + 6 files changed, 225 insertions(+), 27 deletions(-) create mode 100644 src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 41d8f93bbb..5b3a0dc286 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -13,6 +13,8 @@ namespace Symfony\Component\Console; use Symfony\Component\Console\Descriptor\TextDescriptor; use Symfony\Component\Console\Descriptor\XmlDescriptor; +use Symfony\Component\Console\Helper\DebugFormatterHelper; +use Symfony\Component\Console\Helper\ProcessHelper; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\ArgvInput; @@ -962,6 +964,8 @@ class Application new DialogHelper(), new ProgressHelper(), new TableHelper(), + new DebugFormatterHelper(), + new ProcessHelper(), new QuestionHelper(), )); } diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index c741f156f7..55ded1f280 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -1,17 +1,21 @@ CHANGELOG ========= +2.6.0 +----- + + * added a Process helper + * added a DebugFormatter helper + 2.5.0 ----- * deprecated the dialog helper (use the question helper instead) - * added a Process helper * deprecated TableHelper in favor of Table * deprecated ProgressHelper in favor of ProgressBar * added a question helper * added a way to set the process name of a command * added a way to set a default command instead of `ListCommand` - * added a way to set the process title of a command 2.4.0 ----- diff --git a/src/Symfony/Component/Console/Helper/DebugFormatterHelper.php b/src/Symfony/Component/Console/Helper/DebugFormatterHelper.php index c7616cf898..cdb620d168 100644 --- a/src/Symfony/Component/Console/Helper/DebugFormatterHelper.php +++ b/src/Symfony/Component/Console/Helper/DebugFormatterHelper.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Console\Helper; -use Symfony\Component\Console\Helper\Helper; - /** * Helps outputting debug information when running an external program from a command. * @@ -26,6 +24,15 @@ class DebugFormatterHelper extends Helper private $started = array(); private $count = -1; + /** + * Starts a debug formatting session + * + * @param string $id The id of the formatting session + * @param string $message The message to display + * @param string $prefix The prefix to use + * + * @return string + */ public function start($id, $message, $prefix = 'RUN') { $this->started[$id] = array('border' => ++$this->count % count($this->colors)); @@ -33,20 +40,39 @@ class DebugFormatterHelper extends Helper return sprintf("%s %s %s\n", $this->getBorder($id), $prefix, $message); } + /** + * Adds progress to a formatting session + * + * @param string $id The id of the formatting session + * @param string $buffer The message to display + * @param bool $error Whether to consider the buffer as error + * @param string $prefix The prefix for output + * @param string $errorPrefix The prefix for error output + * + * @return string + */ public function progress($id, $buffer, $error = false, $prefix = 'OUT', $errorPrefix = 'ERR') { $message = ''; if ($error) { + if (isset($this->started[$id]['out'])) { + $message .= "\n"; + unset($this->started[$id]['out']); + } if (!isset($this->started[$id]['err'])) { - $message = sprintf("%s %s ", $this->getBorder($id), $errorPrefix); + $message .= sprintf("%s %s ", $this->getBorder($id), $errorPrefix); $this->started[$id]['err'] = true; } $message .= str_replace("\n", sprintf("\n%s %s ", $this->getBorder($id), $errorPrefix), $buffer); } else { + if (isset($this->started[$id]['err'])) { + $message .= "\n"; + unset($this->started[$id]['err']); + } if (!isset($this->started[$id]['out'])) { - $message = sprintf("%s %s ", $this->getBorder($id), $prefix); + $message .= sprintf("%s %s ", $this->getBorder($id), $prefix); $this->started[$id]['out'] = true; } @@ -56,6 +82,16 @@ class DebugFormatterHelper extends Helper return $message; } + /** + * Stops a formatting session + * + * @param string $id The id of the formatting session + * @param string $message The message to display + * @param bool $successful Whether to consider the result as success + * @param string $prefix The prefix for the end output + * + * @return string + */ public function stop($id, $message, $successful, $prefix = 'RES') { $trailingEOL = isset($this->started[$id]['out']) || isset($this->started[$id]['err']) ? "\n" : ''; @@ -64,16 +100,25 @@ class DebugFormatterHelper extends Helper return sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); } - return sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); + $message = sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); + + unset($this->started[$id]['out'], $this->started[$id]['err']); + + return $message; } + /** + * @param string $id The id of the formatting session + * + * @return string + */ private function getBorder($id) { return sprintf(' ', $this->colors[$this->started[$id]['border']]); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Component/Console/Helper/ProcessHelper.php b/src/Symfony/Component/Console/Helper/ProcessHelper.php index 30c38c1ece..2ae780b801 100644 --- a/src/Symfony/Component/Console/Helper/ProcessHelper.php +++ b/src/Symfony/Component/Console/Helper/ProcessHelper.php @@ -11,12 +11,13 @@ namespace Symfony\Component\Console\Helper; -use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; +use Symfony\Component\Process\ProcessBuilder; /** - * The Process class provides helpers to run external processes. + * The ProcessHelper class provides helpers to run external processes. * * @author Fabien Potencier */ @@ -25,40 +26,72 @@ class ProcessHelper extends Helper /** * Runs an external process. * - * @param OutputInterface $output An OutputInterface instance - * @param string|Process $cmd An instance of Process or a command to run - * @param string|null $error An error message that must be displayed if something went wrong - * @param callback|null $callback A PHP callback to run whenever there is some - * output available on STDOUT or STDERR + * @param OutputInterface $output An OutputInterface instance + * @param string|array|Process $cmd An instance of Process or an array of arguments to escape and run or a command to run + * @param string|null $error An error message that must be displayed if something went wrong + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR * * @return Process The process that ran */ public function run(OutputInterface $output, $cmd, $error = null, $callback = null) { - $verbose = $output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE; - $debug = $output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG; - $formatter = $this->getHelperSet()->get('debug_formatter'); - $process = $cmd instanceof Process ? $cmd : new Process($cmd); + if (is_array($cmd)) { + $process = ProcessBuilder::create($cmd)->getProcess(); + } elseif ($cmd instanceof Process) { + $process = $cmd; + } else { + $process = new Process($cmd); + } - if ($verbose) { + if ($output->isVeryVerbose()) { $output->write($formatter->start(spl_object_hash($process), $process->getCommandLine())); } - if ($debug) { + if ($output->isDebug()) { $callback = $this->wrapCallback($output, $process, $callback); } $process->run($callback); - if ($verbose) { - $message = $process->isSuccessful() ? 'Command ran successfully' : sprintf('%s Command did not run sucessfully', $process->getExitCode()); + if ($output->isVeryVerbose()) { + $message = $process->isSuccessful() ? 'Command ran successfully' : sprintf('%s Command did not run successfully', $process->getExitCode()); $output->write($formatter->stop(spl_object_hash($process), $message, $process->isSuccessful())); } if (!$process->isSuccessful() && null !== $error) { - $output->writeln(sprintf('%s'), $error); + $output->writeln(sprintf('%s', $error)); + } + + return $process; + } + + /** + * Runs the process. + * + * This is identical to run() except that an exception is thrown if the process + * exits with a non-zero exit code. + * + * @param OutputInterface $output An OutputInterface instance + * @param string|Process $cmd An instance of Process or a command to run + * @param string|null $error An error message that must be displayed if something went wrong + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return Process The process that ran + * + * @throws ProcessFailedException + * + * @see run() + */ + public function mustRun(OutputInterface $output, $cmd, $error = null, $callback = null) + { + $process = $this->run($output, $cmd, $error, $callback); + + if (!$process->isSuccessful()) { + throw new ProcessFailedException($process); } return $process; @@ -68,23 +101,26 @@ class ProcessHelper extends Helper * Wraps a Process callback to add debugging output. * * @param OutputInterface $output An OutputInterface interface + * @param Process $process The Process * @param callable|null $callback A PHP callable + * + * @return callable */ public function wrapCallback(OutputInterface $output, Process $process, $callback = null) { $formatter = $this->getHelperSet()->get('debug_formatter'); return function ($type, $buffer) use ($output, $process, $callback, $formatter) { - $output->write($formatter->progress(spl_object_hash($process), $buffer, 'err' === $type)); + $output->write($formatter->progress(spl_object_hash($process), $buffer, Process::ERR === $type)); if (null !== $callback) { - $callback($type, $buffer); + call_user_func($callback, $type, $buffer); } }; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php new file mode 100644 index 0000000000..0ca247f1eb --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Helper; + +use Symfony\Component\Console\Helper\DebugFormatterHelper; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Output\StreamOutput; +use Symfony\Component\Console\Helper\ProcessHelper; +use Symfony\Component\Process\Process; + +class ProcessHelperTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider provideCommandsAndOutput + */ + public function testVariousProcessRuns($expected, $cmd, $verbosity, $error) + { + $helper = new ProcessHelper(); + $helper->setHelperSet(new HelperSet(array(new DebugFormatterHelper()))); + $output = $this->getOutputStream($verbosity); + $helper->run($output, $cmd, $error); + $this->assertEquals($expected, $this->getOutput($output)); + } + + public function testPassedCallbackIsExecuted() + { + $helper = new ProcessHelper(); + $helper->setHelperSet(new HelperSet(array(new DebugFormatterHelper()))); + $output = $this->getOutputStream(StreamOutput::VERBOSITY_NORMAL); + + $executed = false; + $callback = function () use (&$executed) { $executed = true; }; + + $helper->run($output, 'php -r "echo 42;"', null, $callback); + $this->assertTrue($executed); + } + + public function provideCommandsAndOutput() + { + $successOutputVerbose = <<getStream()); + + return stream_get_contents($output->getStream()); + } +} diff --git a/src/Symfony/Component/Console/composer.json b/src/Symfony/Component/Console/composer.json index ec597c558c..4a106d7db6 100644 --- a/src/Symfony/Component/Console/composer.json +++ b/src/Symfony/Component/Console/composer.json @@ -20,6 +20,7 @@ }, "require-dev": { "symfony/event-dispatcher": "~2.1", + "symfony/process": "~2.1", "psr/log": "~1.0" }, "suggest": {