feature #21474 [Process] Accept command line arrays and per-run env vars, fixing signaling and escaping (nicolas-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[Process] Accept command line arrays and per-run env vars, fixing signaling and escaping

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #12488, #11972, #10025, #11335, #5759, #5030, #19993, #10486
| License       | MIT
| Doc PR        | -

I think I found a way to fix this network of issues once for all.
Of all the linked ones, only the last two are still open: the remaining were closed in dead ends.

Instead of trying to make `ProcessUtil::escapeArgument` work correctly on Windows - which is impossible as discussed in #21347 - this PR deprecates it in favor of a more powerful approach.

Depending on the use case:

- when a simple command should be run, `Process` now accepts an array of arguments (the "binary" being the first arg). Making this the responsibility of `Process` (instead of `ProcessBuilder`) gives two benefits:
  - escape becomes an internal detail that doesn't leak - thus can't be misused ([see here](https://github.com/symfony/symfony/pull/21347#issuecomment-274051370))
  - since we know we're running a single command, we can prefix it automatically by "exec" - thus fixing a long standing issue with signaling

```php
        $p = new Process(array('php', '-r', 'echo 123;'));
        echo $p->getCommandLine();
        // displays on Linux:
        // exec 'php' '-r' 'echo 123;'
```

- when a shell expression is required, passing a string is still allowed. To make it easy and look-like sql prepared statements, env vars can be used when running the command. Since the shell is OS-specific (think Windows vs Linux) - this PR assumes no portability, so one should just use each shell's specific syntax.

From the fixtures:
```php
        $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());
```

Commits
-------

330b61fecb [Process] Accept command line arrays and per-run env vars, fixing signaling and escaping
This commit is contained in:
Fabien Potencier 2017-02-08 16:45:40 +01:00
commit 3193331855
11 changed files with 260 additions and 114 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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);
}
}

View File

@ -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).'"';
}
}

View File

@ -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();

View File

@ -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

View File

@ -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'
<?php echo 'foobar';
<?php echo phpversion().PHP_SAPI;
PHP
);
$commandLine = $process->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());
}
}

View File

@ -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());
}
}

View File

@ -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) {

View File

@ -13,6 +13,9 @@ namespace Symfony\Component\Process\Tests;
use Symfony\Component\Process\ProcessUtils;
/**
* @group legacy
*/
class ProcessUtilsTest extends \PHPUnit_Framework_TestCase
{
/**