From 330b61fecbef76a2e0c275cbfaee1dbeab176468 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 12 Jan 2017 18:50:31 +0100 Subject: [PATCH] [Process] Accept command line arrays and per-run env vars, fixing signaling and escaping --- UPGRADE-3.3.md | 2 + UPGRADE-4.0.md | 2 + src/Symfony/Component/Process/CHANGELOG.md | 3 + src/Symfony/Component/Process/PhpProcess.php | 15 +- src/Symfony/Component/Process/Process.php | 146 ++++++++++++++--- .../Component/Process/ProcessBuilder.php | 4 +- .../Component/Process/ProcessUtils.php | 4 + .../Process/Tests/PhpProcessTest.php | 8 +- .../Process/Tests/ProcessBuilderTest.php | 32 ++-- .../Component/Process/Tests/ProcessTest.php | 155 +++++++++++------- .../Process/Tests/ProcessUtilsTest.php | 3 + 11 files changed, 260 insertions(+), 114 deletions(-) diff --git a/UPGRADE-3.3.md b/UPGRADE-3.3.md index 3e645b1f9a..7f6316ccbc 100644 --- a/UPGRADE-3.3.md +++ b/UPGRADE-3.3.md @@ -66,6 +66,8 @@ HttpKernel Process ------- + * The `ProcessUtils::escapeArgument()` method has been deprecated, use a command line array or give env vars to the `Process::start/run()` method instead. + * Not inheriting environment variables is deprecated. * Configuring `proc_open()` options is deprecated. diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index 164e27a96f..25017e71b2 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -228,6 +228,8 @@ HttpKernel Process ------- + * The `ProcessUtils::escapeArgument()` method has been removed, use a command line array or give env vars to the `Process::start/run()` method instead. + * Environment variables are always inherited in sub-processes. * Configuring `proc_open()` options has been removed. diff --git a/src/Symfony/Component/Process/CHANGELOG.md b/src/Symfony/Component/Process/CHANGELOG.md index 76503ce730..bb719be711 100644 --- a/src/Symfony/Component/Process/CHANGELOG.md +++ b/src/Symfony/Component/Process/CHANGELOG.md @@ -4,6 +4,9 @@ CHANGELOG 3.3.0 ----- + * added command line arrays in the `Process` class + * added `$env` argument to `Process::start()`, `run()`, `mustRun()` and `restart()` methods + * deprecated the `ProcessUtils::escapeArgument()` method * deprecated not inheriting environment variables * deprecated configuring `proc_open()` options * deprecated configuring enhanced Windows compatibility diff --git a/src/Symfony/Component/Process/PhpProcess.php b/src/Symfony/Component/Process/PhpProcess.php index a7f6d941ea..ecb132e1f7 100644 --- a/src/Symfony/Component/Process/PhpProcess.php +++ b/src/Symfony/Component/Process/PhpProcess.php @@ -38,20 +38,16 @@ class PhpProcess extends Process $executableFinder = new PhpExecutableFinder(); if (false === $php = $executableFinder->find()) { $php = null; + } else { + $php = explode(' ', $php); } if ('phpdbg' === PHP_SAPI) { $file = tempnam(sys_get_temp_dir(), 'dbg'); file_put_contents($file, $script); register_shutdown_function('unlink', $file); - $php .= ' '.ProcessUtils::escapeArgument($file); + $php[] = $file; $script = null; } - if ('\\' !== DIRECTORY_SEPARATOR && null !== $php) { - // exec is mandatory to deal with sending a signal to the process - // see https://github.com/symfony/symfony/issues/5030 about prepending - // command with exec - $php = 'exec '.$php; - } if (null !== $options) { @trigger_error(sprintf('The $options parameter of the %s constructor is deprecated since version 3.3 and will be removed in 4.0.', __CLASS__), E_USER_DEPRECATED); } @@ -70,12 +66,13 @@ class PhpProcess extends Process /** * {@inheritdoc} */ - public function start(callable $callback = null) + public function start(callable $callback = null/*, array $env = array()*/) { if (null === $this->getCommandLine()) { throw new RuntimeException('Unable to find the PHP executable.'); } + $env = 1 < func_num_args() ? func_get_arg(1) : null; - parent::start($callback); + parent::start($callback, $env); } } diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 77ffb0742a..edf4712f36 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -136,7 +136,7 @@ class Process implements \IteratorAggregate /** * Constructor. * - * @param string $commandline The command line to run + * @param string|array $commandline The command line to run * @param string|null $cwd The working directory or null to use the working dir of the current PHP process * @param array|null $env The environment variables or null to use the same environment as the current PHP process * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input @@ -151,7 +151,7 @@ class Process implements \IteratorAggregate throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); } - $this->commandline = $commandline; + $this->setCommandline($commandline); $this->cwd = $cwd; // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started @@ -199,16 +199,20 @@ class Process implements \IteratorAggregate * * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR + * @param array $env An array of additional env vars to set when running the process * * @return int The exit status code * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process stopped after receiving signal * @throws LogicException In case a callback is provided and output has been disabled + * + * @final since version 3.3 */ - public function run($callback = null) + public function run($callback = null/*, array $env = array()*/) { - $this->start($callback); + $env = 1 < func_num_args() ? func_get_arg(1) : null; + $this->start($callback, $env); return $this->wait(); } @@ -220,19 +224,23 @@ class Process implements \IteratorAggregate * exits with a non-zero exit code. * * @param callable|null $callback + * @param array $env An array of additional env vars to set when running the process * * @return self * * @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled * @throws ProcessFailedException if the process didn't terminate successfully + * + * @final since version 3.3 */ - public function mustRun(callable $callback = null) + public function mustRun(callable $callback = null/*, array $env = array()*/) { if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); } + $env = 1 < func_num_args() ? func_get_arg(1) : null; - if (0 !== $this->run($callback)) { + if (0 !== $this->run($callback, $env)) { throw new ProcessFailedException($this); } @@ -253,28 +261,48 @@ class Process implements \IteratorAggregate * * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR + * @param array $env An array of additional env vars to set when running the process * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process is already running * @throws LogicException In case a callback is provided and output has been disabled */ - public function start(callable $callback = null) + public function start(callable $callback = null/*, array $env = array()*/) { if ($this->isRunning()) { throw new RuntimeException('Process is already running'); } + if (2 <= func_num_args()) { + $env = func_get_arg(1); + } else { + if (__CLASS__ !== static::class) { + $r = new \ReflectionMethod($this, __FUNCTION__); + if (__CLASS__ !== $r->getDeclaringClass()->getName() && (2 > $r->getNumberOfParameters() || 'env' !== $r->getParameters()[0]->name)) { + @trigger_error(sprintf('The %s::start() method expects a second "$env" argument since version 3.3. It will be made mandatory in 4.0.', static::class), E_USER_DEPRECATED); + } + } + $env = null; + } $this->resetProcessData(); $this->starttime = $this->lastOutputTime = microtime(true); $this->callback = $this->buildCallback($callback); $this->hasCallback = null !== $callback; $descriptors = $this->getDescriptors(); - + $inheritEnv = $this->inheritEnv; $commandline = $this->commandline; - $env = $this->env; + if (null === $env) { + $env = $this->env; + } else { + if ($this->env) { + $env += $this->env; + } + $inheritEnv = true; + } + $envBackup = array(); - if (null !== $env && $this->inheritEnv) { + if (null !== $env && $inheritEnv) { foreach ($env as $k => $v) { $envBackup[$k] = getenv($v); putenv(false === $v || null === $v ? $k : "$k=$v"); @@ -284,14 +312,8 @@ class Process implements \IteratorAggregate @trigger_error(sprintf('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', __METHOD__), E_USER_DEPRECATED); } if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) { - $commandline = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $commandline).')'; - foreach ($this->processPipes->getFiles() as $offset => $filename) { - $commandline .= ' '.$offset.'>"'.$filename.'"'; - } - - if (!isset($this->options['bypass_shell'])) { - $this->options['bypass_shell'] = true; - } + $this->options['bypass_shell'] = true; + $commandline = $this->prepareWindowsCommandLine($commandline, $envBackup); } elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { // last exit code is output on the fourth pipe and caught to work around --enable-sigchild $descriptors[3] = array('pipe', 'w'); @@ -335,6 +357,7 @@ class Process implements \IteratorAggregate * * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR + * @param array $env An array of additional env vars to set when running the process * * @return $this * @@ -342,15 +365,18 @@ class Process implements \IteratorAggregate * @throws RuntimeException When process is already running * * @see start() + * + * @final since version 3.3 */ - public function restart(callable $callback = null) + public function restart(callable $callback = null/*, array $env = array()*/) { if ($this->isRunning()) { throw new RuntimeException('Process is already running'); } + $env = 1 < func_num_args() ? func_get_arg(1) : null; $process = clone $this; - $process->start($callback); + $process->start($callback, $env); return $process; } @@ -909,12 +935,20 @@ class Process implements \IteratorAggregate /** * Sets the command line to be executed. * - * @param string $commandline The command to execute + * @param string|array $commandline The command to execute * * @return self The current Process instance */ public function setCommandLine($commandline) { + if (is_array($commandline)) { + $commandline = implode(' ', array_map(array($this, 'escapeArgument'), $commandline)); + + if ('\\' !== DIRECTORY_SEPARATOR) { + // exec is mandatory to deal with sending a signal to the process + $commandline = 'exec '.$commandline; + } + } $this->commandline = $commandline; return $this; @@ -1589,6 +1623,50 @@ class Process implements \IteratorAggregate return true; } + private function prepareWindowsCommandLine($cmd, array &$envBackup) + { + $uid = uniqid('', true); + $varCount = 0; + $varCache = array(); + $cmd = preg_replace_callback( + '/"( + [^"%!^]*+ + (?: + (?: !LF! | "(?:\^[%!^])?+" ) + [^"%!^]*+ + )++ + )"/x', + function ($m) use (&$envBackup, &$varCache, &$varCount, $uid) { + if (isset($varCache[$m[0]])) { + return $varCache[$m[0]]; + } + if (false !== strpos($value = $m[1], "\0")) { + $value = str_replace("\0", '?', $value); + } + if (false === strpbrk($value, "\"%!\n")) { + return '"'.$value.'"'; + } + + $value = str_replace(array('!LF!', '"^!"', '"^%"', '"^^"', '""'), array("\n", '!', '%', '^', '"'), $value); + $value = preg_replace('/(\\\\*)"/', '$1$1\\"', $value); + + $var = $uid.++$varCount; + putenv("$var=\"$value\""); + $envBackup[$var] = false; + + return $varCache[$m[0]] = '!'.$var.'!'; + }, + $cmd + ); + + $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; + foreach ($this->processPipes->getFiles() as $offset => $filename) { + $cmd .= ' '.$offset.'>"'.$filename.'"'; + } + + return $cmd; + } + /** * Ensures the process is running or terminated, throws a LogicException if the process has a not started. * @@ -1616,4 +1694,30 @@ class Process implements \IteratorAggregate throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); } } + + /** + * Escapes a string to be used as a shell argument. + * + * @param string $argument The argument that will be escaped + * + * @return string The escaped argument + */ + private function escapeArgument($argument) + { + if ('\\' !== DIRECTORY_SEPARATOR) { + return "'".str_replace("'", "'\\''", $argument)."'"; + } + if ('' === $argument = (string) $argument) { + return '""'; + } + if (false !== strpos($argument, "\0")) { + $argument = str_replace("\0", '?', $argument); + } + if (!preg_match('/[()%!^"<>&|\s]/', $argument)) { + return $argument; + } + $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); + + return '"'.str_replace(array('"', '^', '%', '!', "\n"), array('""', '"^^"', '"^%"', '"^!"', '!LF!'), $argument).'"'; + } } diff --git a/src/Symfony/Component/Process/ProcessBuilder.php b/src/Symfony/Component/Process/ProcessBuilder.php index 02069136c6..2a5bb2bc3f 100644 --- a/src/Symfony/Component/Process/ProcessBuilder.php +++ b/src/Symfony/Component/Process/ProcessBuilder.php @@ -271,9 +271,7 @@ class ProcessBuilder } $arguments = array_merge($this->prefix, $this->arguments); - $script = implode(' ', array_map(array(__NAMESPACE__.'\\ProcessUtils', 'escapeArgument'), $arguments)); - - $process = new Process($script, $this->cwd, $this->env, $this->input, $this->timeout, $this->options); + $process = new Process($arguments, $this->cwd, $this->env, $this->input, $this->timeout, $this->options); if ($this->inheritEnv) { $process->inheritEnvironmentVariables(); diff --git a/src/Symfony/Component/Process/ProcessUtils.php b/src/Symfony/Component/Process/ProcessUtils.php index 500202e584..382c2be7a6 100644 --- a/src/Symfony/Component/Process/ProcessUtils.php +++ b/src/Symfony/Component/Process/ProcessUtils.php @@ -35,9 +35,13 @@ class ProcessUtils * @param string $argument The argument that will be escaped * * @return string The escaped argument + * + * @deprecated since version 3.3, to be removed in 4.0. Use a command line array or give env vars to the `Process::start/run()` method instead. */ public static function escapeArgument($argument) { + @trigger_error('The '.__METHOD__.'() method is deprecated since version 3.3 and will be removed in 4.0. Use a command line array or give env vars to the Process::start/run() method instead.', E_USER_DEPRECATED); + //Fix for PHP bug #43784 escapeshellarg removes % from given string //Fix for PHP bug #49446 escapeshellarg doesn't work on Windows //@see https://bugs.php.net/bug.php?id=43784 diff --git a/src/Symfony/Component/Process/Tests/PhpProcessTest.php b/src/Symfony/Component/Process/Tests/PhpProcessTest.php index f54623a5ec..aa90d6ae27 100644 --- a/src/Symfony/Component/Process/Tests/PhpProcessTest.php +++ b/src/Symfony/Component/Process/Tests/PhpProcessTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Process\Tests; -use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\PhpProcess; class PhpProcessTest extends \PHPUnit_Framework_TestCase @@ -31,19 +30,18 @@ PHP public function testCommandLine() { $process = new PhpProcess(<<<'PHP' -getCommandLine(); - $f = new PhpExecutableFinder(); - $this->assertContains($f->find(), $commandLine, '::getCommandLine() returns the command line of PHP before start'); - $process->start(); $this->assertContains($commandLine, $process->getCommandLine(), '::getCommandLine() returns the command line of PHP after start'); $process->wait(); $this->assertContains($commandLine, $process->getCommandLine(), '::getCommandLine() returns the command line of PHP after wait'); + + $this->assertSame(phpversion().PHP_SAPI, $process->getOutput()); } } diff --git a/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php b/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php index 0767506f61..bb42217304 100644 --- a/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php @@ -90,16 +90,16 @@ class ProcessBuilderTest extends \PHPUnit_Framework_TestCase $proc = $pb->setArguments(array('-v'))->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertEquals('"/usr/bin/php" "-v"', $proc->getCommandLine()); + $this->assertEquals('/usr/bin/php -v', $proc->getCommandLine()); } else { - $this->assertEquals("'/usr/bin/php' '-v'", $proc->getCommandLine()); + $this->assertEquals("exec '/usr/bin/php' '-v'", $proc->getCommandLine()); } $proc = $pb->setArguments(array('-i'))->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertEquals('"/usr/bin/php" "-i"', $proc->getCommandLine()); + $this->assertEquals('/usr/bin/php -i', $proc->getCommandLine()); } else { - $this->assertEquals("'/usr/bin/php' '-i'", $proc->getCommandLine()); + $this->assertEquals("exec '/usr/bin/php' '-i'", $proc->getCommandLine()); } } @@ -110,16 +110,16 @@ class ProcessBuilderTest extends \PHPUnit_Framework_TestCase $proc = $pb->setArguments(array('-v'))->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertEquals('"/usr/bin/php" "composer.phar" "-v"', $proc->getCommandLine()); + $this->assertEquals('/usr/bin/php composer.phar -v', $proc->getCommandLine()); } else { - $this->assertEquals("'/usr/bin/php' 'composer.phar' '-v'", $proc->getCommandLine()); + $this->assertEquals("exec '/usr/bin/php' 'composer.phar' '-v'", $proc->getCommandLine()); } $proc = $pb->setArguments(array('-i'))->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertEquals('"/usr/bin/php" "composer.phar" "-i"', $proc->getCommandLine()); + $this->assertEquals('/usr/bin/php composer.phar -i', $proc->getCommandLine()); } else { - $this->assertEquals("'/usr/bin/php' 'composer.phar' '-i'", $proc->getCommandLine()); + $this->assertEquals("exec '/usr/bin/php' 'composer.phar' '-i'", $proc->getCommandLine()); } } @@ -129,9 +129,9 @@ class ProcessBuilderTest extends \PHPUnit_Framework_TestCase $proc = $pb->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertSame('^%"path"^% "foo \\" bar" "%baz%baz"', $proc->getCommandLine()); + $this->assertSame('""^%"path"^%"" "foo "" bar" ""^%"baz"^%"baz"', $proc->getCommandLine()); } else { - $this->assertSame("'%path%' 'foo \" bar' '%baz%baz'", $proc->getCommandLine()); + $this->assertSame("exec '%path%' 'foo \" bar' '%baz%baz'", $proc->getCommandLine()); } } @@ -142,9 +142,9 @@ class ProcessBuilderTest extends \PHPUnit_Framework_TestCase $proc = $pb->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertSame('^%"prefix"^% "arg"', $proc->getCommandLine()); + $this->assertSame('""^%"prefix"^%"" arg', $proc->getCommandLine()); } else { - $this->assertSame("'%prefix%' 'arg'", $proc->getCommandLine()); + $this->assertSame("exec '%prefix%' 'arg'", $proc->getCommandLine()); } } @@ -163,9 +163,9 @@ class ProcessBuilderTest extends \PHPUnit_Framework_TestCase ->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertEquals('"/usr/bin/php"', $process->getCommandLine()); + $this->assertEquals('/usr/bin/php', $process->getCommandLine()); } else { - $this->assertEquals("'/usr/bin/php'", $process->getCommandLine()); + $this->assertEquals("exec '/usr/bin/php'", $process->getCommandLine()); } } @@ -175,9 +175,9 @@ class ProcessBuilderTest extends \PHPUnit_Framework_TestCase ->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertEquals('"/usr/bin/php"', $process->getCommandLine()); + $this->assertEquals('/usr/bin/php', $process->getCommandLine()); } else { - $this->assertEquals("'/usr/bin/php'", $process->getCommandLine()); + $this->assertEquals("exec '/usr/bin/php'", $process->getCommandLine()); } } diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index 9227a69b64..7873f287fe 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -33,12 +33,6 @@ class ProcessTest extends \PHPUnit_Framework_TestCase { $phpBin = new PhpExecutableFinder(); self::$phpBin = getenv('SYMFONY_PROCESS_PHP_TEST_BINARY') ?: ('phpdbg' === PHP_SAPI ? 'php' : $phpBin->find()); - if ('\\' !== DIRECTORY_SEPARATOR) { - // exec is mandatory to deal with sending a signal to the process - // see https://github.com/symfony/symfony/issues/5030 about prepending - // command with exec - self::$phpBin = 'exec '.self::$phpBin; - } ob_start(); phpinfo(INFO_GENERAL); @@ -59,7 +53,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase $this->markTestSkipped('This test is transient on Windows'); } @trigger_error('Test Error', E_USER_NOTICE); - $process = $this->getProcess(self::$phpBin." -r 'sleep(3)'"); + $process = $this->getProcessForCode('sleep(3)'); $process->run(); $actualError = error_get_last(); $this->assertEquals('Test Error', $actualError['message']); @@ -102,7 +96,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testStopWithTimeoutIsActuallyWorking() { - $p = $this->getProcess(self::$phpBin.' '.__DIR__.'/NonStopableProcess.php 30'); + $p = $this->getProcess(array(self::$phpBin, __DIR__.'/NonStopableProcess.php', 30)); $p->start(); while (false === strpos($p->getOutput(), 'received')) { @@ -128,7 +122,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase $expectedOutputSize = PipesInterface::CHUNK_SIZE * 2 + 2; $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize); - $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code))); + $p = $this->getProcessForCode($code); $p->start(); @@ -167,7 +161,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testProcessResponses($expected, $getter, $code) { - $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code))); + $p = $this->getProcessForCode($code); $p->run(); $this->assertSame($expected, $p->$getter()); @@ -183,7 +177,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase $expected = str_repeat(str_repeat('*', 1024), $size).'!'; $expectedLength = (1024 * $size) + 1; - $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code))); + $p = $this->getProcessForCode($code); $p->setInput($expected); $p->run(); @@ -203,7 +197,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase fwrite($stream, $expected); rewind($stream); - $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code))); + $p = $this->getProcessForCode($code); $p->setInput($stream); $p->run(); @@ -219,7 +213,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase fwrite($stream, 'hello'); rewind($stream); - $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('stream_copy_to_stream(STDIN, STDOUT);'))); + $p = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);'); $p->setInput($stream); $p->start(function ($type, $data) use ($stream) { if ('hello' === $data) { @@ -237,7 +231,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testSetInputWhileRunningThrowsAnException() { - $process = $this->getProcess(self::$phpBin.' -r "sleep(30);"'); + $process = $this->getProcessForCode('sleep(30);'); $process->start(); try { $process->setInput('foobar'); @@ -314,7 +308,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testCallbackIsExecutedForOutput() { - $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('echo \'foo\';'))); + $p = $this->getProcessForCode('echo \'foo\';'); $called = false; $p->run(function ($type, $buffer) use (&$called) { @@ -326,7 +320,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testCallbackIsExecutedForOutputWheneverOutputIsDisabled() { - $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('echo \'foo\';'))); + $p = $this->getProcessForCode('echo \'foo\';'); $p->disableOutput(); $called = false; @@ -339,7 +333,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testGetErrorOutput() { - $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'))); + $p = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'); $p->run(); $this->assertEquals(3, preg_match_all('/ERROR/', $p->getErrorOutput(), $matches)); @@ -347,7 +341,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testFlushErrorOutput() { - $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'))); + $p = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'); $p->run(); $p->clearErrorOutput(); @@ -361,7 +355,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase { $lock = tempnam(sys_get_temp_dir(), __FUNCTION__); - $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('file_put_contents($s = \''.$uri.'\', \'foo\'); flock(fopen('.var_export($lock, true).', \'r\'), LOCK_EX); file_put_contents($s, \'bar\');'))); + $p = $this->getProcessForCode('file_put_contents($s = \''.$uri.'\', \'foo\'); flock(fopen('.var_export($lock, true).', \'r\'), LOCK_EX); file_put_contents($s, \'bar\');'); $h = fopen($lock, 'w'); flock($h, LOCK_EX); @@ -392,7 +386,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testGetOutput() { - $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { echo \' foo \'; $n++; }'))); + $p = $this->getProcessForCode('$n = 0; while ($n < 3) { echo \' foo \'; $n++; }'); $p->run(); $this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches)); @@ -400,7 +394,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testFlushOutput() { - $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n=0;while ($n<3) {echo \' foo \';$n++;}'))); + $p = $this->getProcessForCode('$n=0;while ($n<3) {echo \' foo \';$n++;}'); $p->run(); $p->clearOutput(); @@ -440,7 +434,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase $this->markTestSkipped('Windows does not have /dev/tty support'); } - $process = $this->getProcess('echo "foo" >> /dev/null && '.self::$phpBin.' -r "usleep(100000);"'); + $process = $this->getProcess('echo "foo" >> /dev/null && '.$this->getProcessForCode('usleep(100000);')->getCommandLine()); $process->setTty(true); $process->start(); $this->assertTrue($process->isRunning()); @@ -544,7 +538,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testStartIsNonBlocking() { - $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $process = $this->getProcessForCode('usleep(500000);'); $start = microtime(true); $process->start(); $end = microtime(true); @@ -563,7 +557,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase { $this->skipIfNotEnhancedSigchild(); - $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"'); + $process = $this->getProcessForCode('usleep(100000);'); $this->assertNull($process->getExitCode()); $process->start(); $this->assertNull($process->getExitCode()); @@ -575,7 +569,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase { $this->skipIfNotEnhancedSigchild(); - $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"'); + $process = $this->getProcessForCode('usleep(100000);'); $process->run(); $this->assertEquals(0, $process->getExitCode()); $process->start(); @@ -595,7 +589,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testStatus() { - $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"'); + $process = $this->getProcessForCode('usleep(100000);'); $this->assertFalse($process->isRunning()); $this->assertFalse($process->isStarted()); $this->assertFalse($process->isTerminated()); @@ -614,7 +608,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testStop() { - $process = $this->getProcess(self::$phpBin.' -r "sleep(31);"'); + $process = $this->getProcessForCode('sleep(31);'); $process->start(); $this->assertTrue($process->isRunning()); $process->stop(); @@ -634,7 +628,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase { $this->skipIfNotEnhancedSigchild(); - $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"'); + $process = $this->getProcessForCode('usleep(100000);'); $process->start(); $this->assertFalse($process->isSuccessful()); @@ -648,7 +642,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase { $this->skipIfNotEnhancedSigchild(); - $process = $this->getProcess(self::$phpBin.' -r "throw new \Exception(\'BOUM\');"'); + $process = $this->getProcessForCode('throw new \Exception(\'BOUM\');'); $process->run(); $this->assertFalse($process->isSuccessful()); } @@ -684,7 +678,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase } $this->skipIfNotEnhancedSigchild(); - $process = $this->getProcess(self::$phpBin.' -r "sleep(32);"'); + $process = $this->getProcessForCode('sleep(32);'); $process->start(); $process->stop(); $this->assertTrue($process->hasBeenSignaled()); @@ -702,7 +696,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase } $this->skipIfNotEnhancedSigchild(false); - $process = $this->getProcess(self::$phpBin.' -r "sleep(32.1)"'); + $process = $this->getProcessForCode('sleep(32.1);'); $process->start(); posix_kill($process->getPid(), 9); // SIGKILL @@ -711,7 +705,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testRestart() { - $process1 = $this->getProcess(self::$phpBin.' -r "echo getmypid();"'); + $process1 = $this->getProcessForCode('echo getmypid();'); $process1->run(); $process2 = $process1->restart(); @@ -733,7 +727,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testRunProcessWithTimeout() { - $process = $this->getProcess(self::$phpBin.' -r "sleep(30);"'); + $process = $this->getProcessForCode('sleep(30);'); $process->setTimeout(0.1); $start = microtime(true); try { @@ -753,7 +747,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testIterateOverProcessWithTimeout() { - $process = $this->getProcess(self::$phpBin.' -r "sleep(30);"'); + $process = $this->getProcessForCode('sleep(30);'); $process->setTimeout(0.1); $start = microtime(true); try { @@ -787,7 +781,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testCheckTimeoutOnStartedProcess() { - $process = $this->getProcess(self::$phpBin.' -r "sleep(33);"'); + $process = $this->getProcessForCode('sleep(33);'); $process->setTimeout(0.1); $process->start(); @@ -809,7 +803,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testIdleTimeout() { - $process = $this->getProcess(self::$phpBin.' -r "sleep(34);"'); + $process = $this->getProcessForCode('sleep(34);'); $process->setTimeout(60); $process->setIdleTimeout(0.1); @@ -826,7 +820,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testIdleTimeoutNotExceededWhenOutputIsSent() { - $process = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('while (true) {echo \'foo \'; usleep(1000);}'))); + $process = $this->getProcessForCode('while (true) {echo \'foo \'; usleep(1000);}'); $process->setTimeout(1); $process->start(); @@ -852,7 +846,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testStartAfterATimeout() { - $process = $this->getProcess(self::$phpBin.' -r "sleep(35);"'); + $process = $this->getProcessForCode('sleep(35);'); $process->setTimeout(0.1); try { @@ -870,7 +864,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testGetPid() { - $process = $this->getProcess(self::$phpBin.' -r "sleep(36);"'); + $process = $this->getProcessForCode('sleep(36);'); $process->start(); $this->assertGreaterThan(0, $process->getPid()); $process->stop(0); @@ -894,7 +888,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testSignal() { - $process = $this->getProcess(self::$phpBin.' '.__DIR__.'/SignalListener.php'); + $process = $this->getProcess(array(self::$phpBin, __DIR__.'/SignalListener.php')); $process->start(); while (false === strpos($process->getOutput(), 'Caught')) { @@ -965,7 +959,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testMethodsThatNeedATerminatedProcess($method) { - $process = $this->getProcess(self::$phpBin.' -r "sleep(37);"'); + $process = $this->getProcessForCode('sleep(37);'); $process->start(); try { $process->{$method}(); @@ -998,7 +992,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase $this->markTestSkipped('POSIX signals do not work on Windows'); } - $process = $this->getProcess(self::$phpBin.' -r "sleep(38);"'); + $process = $this->getProcessForCode('sleep(38);'); $process->start(); try { $process->signal($signal); @@ -1034,7 +1028,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testDisableOutputWhileRunningThrowsException() { - $p = $this->getProcess(self::$phpBin.' -r "sleep(39);"'); + $p = $this->getProcessForCode('sleep(39);'); $p->start(); $p->disableOutput(); } @@ -1045,7 +1039,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testEnableOutputWhileRunningThrowsException() { - $p = $this->getProcess(self::$phpBin.' -r "sleep(40);"'); + $p = $this->getProcessForCode('sleep(40);'); $p->disableOutput(); $p->start(); $p->enableOutput(); @@ -1097,7 +1091,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testGetOutputWhileDisabled($fetchMethod) { - $p = $this->getProcess(self::$phpBin.' -r "sleep(41);"'); + $p = $this->getProcessForCode('sleep(41);'); $p->disableOutput(); $p->start(); $p->{$fetchMethod}(); @@ -1115,7 +1109,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testStopTerminatesProcessCleanly() { - $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(42);"'); + $process = $this->getProcessForCode('echo 123; sleep(42);'); $process->run(function () use ($process) { $process->stop(); }); @@ -1124,7 +1118,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testKillSignalTerminatesProcessCleanly() { - $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(43);"'); + $process = $this->getProcessForCode('echo 123; sleep(43);'); $process->run(function () use ($process) { $process->signal(9); // SIGKILL }); @@ -1133,7 +1127,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testTermSignalTerminatesProcessCleanly() { - $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(44);"'); + $process = $this->getProcessForCode('echo 123; sleep(44);'); $process->run(function () use ($process) { $process->signal(15); // SIGTERM }); @@ -1179,7 +1173,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testIncrementalOutputDoesNotRequireAnotherCall($stream, $method) { - $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\''.$stream.'\', $n, 1); $n++; usleep(1000); }'), null, null, null, null); + $process = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\''.$stream.'\', $n, 1); $n++; usleep(1000); }', null, null, null, null); $process->start(); $result = ''; $limit = microtime(true) + 3; @@ -1208,7 +1202,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase yield 'pong'; }; - $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('stream_copy_to_stream(STDIN, STDOUT);'), null, null, $input()); + $process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);', null, null, $input()); $process->run(); $this->assertSame('pingpong', $process->getOutput()); } @@ -1217,7 +1211,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase { $input = new InputStream(); - $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('echo \'ping\'; stream_copy_to_stream(STDIN, STDOUT);')); + $process = $this->getProcessForCode('echo \'ping\'; stream_copy_to_stream(STDIN, STDOUT);'); $process->setInput($input); $process->start(function ($type, $data) use ($input) { @@ -1251,7 +1245,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase $input->onEmpty($stream); $input->write($stream()); - $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('echo fread(STDIN, 3);')); + $process = $this->getProcessForCode('echo fread(STDIN, 3);'); $process->setInput($input); $process->start(function ($type, $data) use ($input) { $input->close(); @@ -1269,7 +1263,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase $input->close(); }); - $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('stream_copy_to_stream(STDIN, STDOUT);')); + $process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);'); $process->setInput($input); $process->start(); $input->write('ping'); @@ -1283,7 +1277,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase $input = new InputStream(); $input->onEmpty(function () use (&$i) { ++$i; }); - $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('echo 123; echo fread(STDIN, 1); echo 456;')); + $process = $this->getProcessForCode('echo 123; echo fread(STDIN, 1); echo 456;'); $process->setInput($input); $process->start(function ($type, $data) use ($input) { if ('123' === $data) { @@ -1300,7 +1294,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase { $input = new InputStream(); - $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('fwrite(STDOUT, 123); fwrite(STDERR, 234); flush(); usleep(10000); fwrite(STDOUT, fread(STDIN, 3)); fwrite(STDERR, 456);')); + $process = $this->getProcessForCode('fwrite(STDOUT, 123); fwrite(STDERR, 234); flush(); usleep(10000); fwrite(STDOUT, fread(STDIN, 3)); fwrite(STDERR, 456);'); $process->setInput($input); $process->start(); $output = array(); @@ -1336,7 +1330,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase { $input = new InputStream(); - $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('fwrite(STDOUT, fread(STDIN, 3));')); + $process = $this->getProcessForCode('fwrite(STDOUT, fread(STDIN, 3));'); $process->setInput($input); $process->start(); $output = array(); @@ -1370,8 +1364,8 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testChainedProcesses() { - $p1 = new Process(self::$phpBin.' -r '.escapeshellarg('fwrite(STDERR, 123); fwrite(STDOUT, 456);')); - $p2 = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('stream_copy_to_stream(STDIN, STDOUT);'))); + $p1 = $this->getProcessForCode('fwrite(STDERR, 123); fwrite(STDOUT, 456);'); + $p2 = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);'); $p2->setInput($p1); $p1->start(); @@ -1385,7 +1379,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase public function testEnvIsInherited() { - $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('echo serialize($_SERVER);'), null, array('BAR' => 'BAZ')); + $process = $this->getProcessForCode('echo serialize($_SERVER);', null, array('BAR' => 'BAZ')); putenv('FOO=BAR'); @@ -1402,7 +1396,7 @@ class ProcessTest extends \PHPUnit_Framework_TestCase */ public function testInheritEnvDisabled() { - $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('echo serialize($_SERVER);'), null, array('BAR' => 'BAZ')); + $process = $this->getProcessForCode('echo serialize($_SERVER);', null, array('BAR' => 'BAZ')); putenv('FOO=BAR'); @@ -1418,6 +1412,39 @@ class ProcessTest extends \PHPUnit_Framework_TestCase $this->assertSame($expected, $env); } + /** + * @dataProvider provideEscapeArgument + */ + public function testEscapeArgument($arg) + { + $p = new Process(array(self::$phpBin, '-r', 'echo $argv[1];', $arg)); + $p->run(); + + $this->assertSame($arg, $p->getOutput()); + } + + public function provideEscapeArgument() + { + yield array('a"b%c%'); + yield array('a"b^c^'); + yield array("a\nb'c"); + yield array('a^b c!'); + yield array("a!b\tc"); + yield array('a\\\\"\\"'); + yield array('éÉèÈàÀöä'); + } + + public function testEnvArgument() + { + $env = array('FOO' => 'Foo', 'BAR' => 'Bar'); + $cmd = '\\' === DIRECTORY_SEPARATOR ? 'echo !FOO! !BAR! !BAZ!' : 'echo $FOO $BAR $BAZ'; + $p = new Process($cmd, null, $env); + $p->run(null, array('BAR' => 'baR', 'BAZ' => 'baZ')); + + $this->assertSame('Foo baR baZ', rtrim($p->getOutput())); + $this->assertSame($env, $p->getEnv()); + } + /** * @param string $commandline * @param null|string $cwd @@ -1455,6 +1482,14 @@ class ProcessTest extends \PHPUnit_Framework_TestCase return self::$process = $process; } + /** + * @return Process + */ + private function getProcessForCode($code, $cwd = null, array $env = null, $input = null, $timeout = 60) + { + return $this->getProcess(array(self::$phpBin, '-r', $code), $cwd, $env, $input, $timeout); + } + private function skipIfNotEnhancedSigchild($expectException = true) { if (self::$sigchild) { diff --git a/src/Symfony/Component/Process/Tests/ProcessUtilsTest.php b/src/Symfony/Component/Process/Tests/ProcessUtilsTest.php index e6564cde5b..d03f8dddba 100644 --- a/src/Symfony/Component/Process/Tests/ProcessUtilsTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessUtilsTest.php @@ -13,6 +13,9 @@ namespace Symfony\Component\Process\Tests; use Symfony\Component\Process\ProcessUtils; +/** + * @group legacy + */ class ProcessUtilsTest extends \PHPUnit_Framework_TestCase { /**