diff --git a/.gitattributes b/.gitattributes
index 84c7add0..14c3c359 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,4 +1,3 @@
/Tests export-ignore
/phpunit.xml.dist export-ignore
-/.gitattributes export-ignore
-/.gitignore export-ignore
+/.git* export-ignore
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..4689c4da
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,8 @@
+Please do not submit any Pull Requests here. They will be closed.
+---
+
+Please submit your PR here instead:
+https://github.com/symfony/symfony
+
+This repository is what we call a "subtree split": a read-only subset of that main repository.
+We're looking forward to your PR there!
diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml
new file mode 100644
index 00000000..e55b4781
--- /dev/null
+++ b/.github/workflows/close-pull-request.yml
@@ -0,0 +1,20 @@
+name: Close Pull Request
+
+on:
+ pull_request_target:
+ types: [opened]
+
+jobs:
+ run:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: superbrothers/close-pull-request@v3
+ with:
+ comment: |
+ Thanks for your Pull Request! We love contributions.
+
+ However, you should instead open your PR on the main repository:
+ https://github.com/symfony/symfony
+
+ This repository is what we call a "subtree split": a read-only subset of that main repository.
+ We're looking forward to your PR there!
diff --git a/ExecutableFinder.php b/ExecutableFinder.php
index feee4ad4..89edd22f 100644
--- a/ExecutableFinder.php
+++ b/ExecutableFinder.php
@@ -19,7 +19,15 @@
*/
class ExecutableFinder
{
- private $suffixes = ['.exe', '.bat', '.cmd', '.com'];
+ private const CMD_BUILTINS = [
+ 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date',
+ 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto',
+ 'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause',
+ 'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set',
+ 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol',
+ ];
+
+ private $suffixes = [];
/**
* Replaces default suffixes of executable.
@@ -44,43 +52,52 @@ public function addSuffix(string $suffix)
* @param string|null $default The default to return if no executable is found
* @param array $extraDirs Additional dirs to check into
*
- * @return string|null The executable path or default value
+ * @return string|null
*/
- public function find(string $name, string $default = null, array $extraDirs = [])
+ public function find(string $name, ?string $default = null, array $extraDirs = [])
{
- if (ini_get('open_basedir')) {
- $searchPath = array_merge(explode(\PATH_SEPARATOR, ini_get('open_basedir')), $extraDirs);
- $dirs = [];
- foreach ($searchPath as $path) {
- // Silencing against https://bugs.php.net/69240
- if (@is_dir($path)) {
- $dirs[] = $path;
- } else {
- if (basename($path) == $name && @is_executable($path)) {
- return $path;
- }
- }
- }
- } else {
- $dirs = array_merge(
- explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')),
- $extraDirs
- );
+ // windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes
+ if ('\\' === \DIRECTORY_SEPARATOR && \in_array(strtolower($name), self::CMD_BUILTINS, true)) {
+ return $name;
}
- $suffixes = [''];
+ $dirs = array_merge(
+ explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')),
+ $extraDirs
+ );
+
+ $suffixes = [];
if ('\\' === \DIRECTORY_SEPARATOR) {
$pathExt = getenv('PATHEXT');
- $suffixes = array_merge($pathExt ? explode(\PATH_SEPARATOR, $pathExt) : $this->suffixes, $suffixes);
+ $suffixes = $this->suffixes;
+ $suffixes = array_merge($suffixes, $pathExt ? explode(\PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com']);
}
+ $suffixes = '' !== pathinfo($name, PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']);
foreach ($suffixes as $suffix) {
foreach ($dirs as $dir) {
+ if ('' === $dir) {
+ $dir = '.';
+ }
if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) {
return $file;
}
+
+ if (!@is_dir($dir) && basename($dir) === $name.$suffix && @is_executable($dir)) {
+ return $dir;
+ }
}
}
+ if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) {
+ return $default;
+ }
+
+ $execResult = exec('command -v -- '.escapeshellarg($name));
+
+ if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) {
+ return $executablePath;
+ }
+
return $default;
}
}
diff --git a/InputStream.php b/InputStream.php
index 4f8f7133..0c45b524 100644
--- a/InputStream.php
+++ b/InputStream.php
@@ -17,6 +17,8 @@
* Provides a way to continuously write to the input of a Process until the InputStream is closed.
*
* @author Nicolas Grekas
+ *
+ * @implements \IteratorAggregate
*/
class InputStream implements \IteratorAggregate
{
@@ -28,7 +30,7 @@ class InputStream implements \IteratorAggregate
/**
* Sets a callback that is called when the write buffer becomes empty.
*/
- public function onEmpty(callable $onEmpty = null)
+ public function onEmpty(?callable $onEmpty = null)
{
$this->onEmpty = $onEmpty;
}
@@ -67,7 +69,7 @@ public function isClosed()
}
/**
- * @return \Traversable
+ * @return \Traversable
*/
#[\ReturnTypeWillChange]
public function getIterator()
diff --git a/LICENSE b/LICENSE
index 9ff2d0d6..0138f8f0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2021 Fabien Potencier
+Copyright (c) 2004-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php
index e4f03f76..c3a9680d 100644
--- a/PhpExecutableFinder.php
+++ b/PhpExecutableFinder.php
@@ -29,20 +29,17 @@ public function __construct()
/**
* Finds The PHP executable.
*
- * @return string|false The PHP executable path or false if it cannot be found
+ * @return string|false
*/
public function find(bool $includeArgs = true)
{
if ($php = getenv('PHP_BINARY')) {
- if (!is_executable($php)) {
- $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v';
- if ($php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) {
- if (!is_executable($php)) {
- return false;
- }
- } else {
- return false;
- }
+ if (!is_executable($php) && !$php = $this->executableFinder->find($php)) {
+ return false;
+ }
+
+ if (@is_dir($php)) {
+ return false;
}
return $php;
@@ -52,12 +49,12 @@ public function find(bool $includeArgs = true)
$args = $includeArgs && $args ? ' '.implode(' ', $args) : '';
// PHP_BINARY return the current sapi executable
- if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cgi-fcgi', 'cli', 'cli-server', 'phpdbg'], true)) {
+ if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cli', 'cli-server', 'phpdbg'], true)) {
return \PHP_BINARY.$args;
}
if ($php = getenv('PHP_PATH')) {
- if (!@is_executable($php)) {
+ if (!@is_executable($php) || @is_dir($php)) {
return false;
}
@@ -65,12 +62,12 @@ public function find(bool $includeArgs = true)
}
if ($php = getenv('PHP_PEAR_PHP_BIN')) {
- if (@is_executable($php)) {
+ if (@is_executable($php) && !@is_dir($php)) {
return $php;
}
}
- if (@is_executable($php = \PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php'))) {
+ if (@is_executable($php = \PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php')) && !@is_dir($php)) {
return $php;
}
@@ -85,7 +82,7 @@ public function find(bool $includeArgs = true)
/**
* Finds the PHP executable arguments.
*
- * @return array The PHP executable arguments
+ * @return array
*/
public function findArguments()
{
diff --git a/PhpProcess.php b/PhpProcess.php
index 2bc338e5..3a1d147c 100644
--- a/PhpProcess.php
+++ b/PhpProcess.php
@@ -32,7 +32,7 @@ class PhpProcess extends Process
* @param int $timeout The timeout in seconds
* @param array|null $php Path to the PHP binary to use with any additional arguments
*/
- public function __construct(string $script, string $cwd = null, array $env = null, int $timeout = 60, array $php = null)
+ public function __construct(string $script, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null)
{
if (null === $php) {
$executableFinder = new PhpExecutableFinder();
@@ -53,7 +53,7 @@ public function __construct(string $script, string $cwd = null, array $env = nul
/**
* {@inheritdoc}
*/
- public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60)
+ public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60)
{
throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class));
}
@@ -61,7 +61,7 @@ public static function fromShellCommandline(string $command, string $cwd = null,
/**
* {@inheritdoc}
*/
- public function start(callable $callback = null, array $env = [])
+ public function start(?callable $callback = null, array $env = [])
{
if (null === $this->getCommandLine()) {
throw new RuntimeException('Unable to find the PHP executable.');
diff --git a/Pipes/AbstractPipes.php b/Pipes/AbstractPipes.php
index 2d145872..656dc032 100644
--- a/Pipes/AbstractPipes.php
+++ b/Pipes/AbstractPipes.php
@@ -47,7 +47,9 @@ public function __construct($input)
public function close()
{
foreach ($this->pipes as $pipe) {
- fclose($pipe);
+ if (\is_resource($pipe)) {
+ fclose($pipe);
+ }
}
$this->pipes = [];
}
@@ -102,7 +104,7 @@ protected function write(): ?array
stream_set_blocking($input, 0);
} elseif (!isset($this->inputBuffer[0])) {
if (!\is_string($input)) {
- if (!is_scalar($input)) {
+ if (!\is_scalar($input)) {
throw new InvalidArgumentException(sprintf('"%s" yielded a value of type "%s", but only scalars and stream resources are supported.', get_debug_type($this->input), get_debug_type($input)));
}
$input = (string) $input;
@@ -133,7 +135,7 @@ protected function write(): ?array
}
if ($input) {
- for (;;) {
+ while (true) {
$data = fread($input, self::CHUNK_SIZE);
if (!isset($data[0])) {
break;
diff --git a/Pipes/UnixPipes.php b/Pipes/UnixPipes.php
index 58a8da07..5a0e9d47 100644
--- a/Pipes/UnixPipes.php
+++ b/Pipes/UnixPipes.php
@@ -35,10 +35,7 @@ public function __construct(?bool $ttyMode, bool $ptyMode, $input, bool $haveRea
parent::__construct($input);
}
- /**
- * @return array
- */
- public function __sleep()
+ public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
diff --git a/Pipes/WindowsPipes.php b/Pipes/WindowsPipes.php
index 69768f3d..968dd026 100644
--- a/Pipes/WindowsPipes.php
+++ b/Pipes/WindowsPipes.php
@@ -88,10 +88,7 @@ public function __construct($input, bool $haveReadSupport)
parent::__construct($input);
}
- /**
- * @return array
- */
- public function __sleep()
+ public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
@@ -152,7 +149,7 @@ public function readAndWrite(bool $blocking, bool $close = false): array
if ($w) {
@stream_select($r, $w, $e, 0, Process::TIMEOUT_PRECISION * 1E6);
} elseif ($this->fileHandles) {
- usleep(Process::TIMEOUT_PRECISION * 1E6);
+ usleep((int) (Process::TIMEOUT_PRECISION * 1E6));
}
}
foreach ($this->fileHandles as $type => $fileHandle) {
diff --git a/Process.php b/Process.php
index a541cd66..91f9e8fe 100644
--- a/Process.php
+++ b/Process.php
@@ -27,6 +27,8 @@
*
* @author Fabien Potencier
* @author Romain Neutron
+ *
+ * @implements \IteratorAggregate
*/
class Process implements \IteratorAggregate
{
@@ -53,7 +55,7 @@ class Process implements \IteratorAggregate
private $hasCallback = false;
private $commandline;
private $cwd;
- private $env;
+ private $env = [];
private $input;
private $starttime;
private $lastOutputTime;
@@ -78,6 +80,7 @@ class Process implements \IteratorAggregate
private $processPipes;
private $latestSignal;
+ private $cachedExitCode;
private static $sigchild;
@@ -138,7 +141,7 @@ class Process implements \IteratorAggregate
*
* @throws LogicException When proc_open is not installed
*/
- public function __construct(array $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60)
+ public function __construct(array $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60)
{
if (!\function_exists('proc_open')) {
throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.');
@@ -174,7 +177,7 @@ public function __construct(array $command, string $cwd = null, array $env = nul
* In order to inject dynamic values into command-lines, we strongly recommend using placeholders.
* This will save escaping values, which is not portable nor secure anyway:
*
- * $process = Process::fromShellCommandline('my_command "$MY_VAR"');
+ * $process = Process::fromShellCommandline('my_command "${:MY_VAR}"');
* $process->run(null, ['MY_VAR' => $theValue]);
*
* @param string $command The command line to pass to the shell of the OS
@@ -187,7 +190,7 @@ public function __construct(array $command, string $cwd = null, array $env = nul
*
* @throws LogicException When proc_open is not installed
*/
- public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60)
+ public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60)
{
$process = new static([], $cwd, $env, $input, $timeout);
$process->commandline = $command;
@@ -245,7 +248,7 @@ public function __clone()
*
* @final
*/
- public function run(callable $callback = null, array $env = []): int
+ public function run(?callable $callback = null, array $env = []): int
{
$this->start($callback, $env);
@@ -264,7 +267,7 @@ public function run(callable $callback = null, array $env = []): int
*
* @final
*/
- public function mustRun(callable $callback = null, array $env = []): self
+ public function mustRun(?callable $callback = null, array $env = []): self
{
if (0 !== $this->run($callback, $env)) {
throw new ProcessFailedException($this);
@@ -292,7 +295,7 @@ public function mustRun(callable $callback = null, array $env = []): self
* @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, array $env = [])
+ public function start(?callable $callback = null, array $env = [])
{
if ($this->isRunning()) {
throw new RuntimeException('Process is already running.');
@@ -305,10 +308,10 @@ public function start(callable $callback = null, array $env = [])
$descriptors = $this->getDescriptors();
if ($this->env) {
- $env += $this->env;
+ $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env;
}
- $env += $this->getDefaultEnv();
+ $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv();
if (\is_array($commandline = $this->commandline)) {
$commandline = implode(' ', array_map([$this, 'escapeArgument'], $commandline));
@@ -329,7 +332,7 @@ public function start(callable $callback = null, array $env = [])
// See https://unix.stackexchange.com/questions/71205/background-process-pipe-input
$commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;';
- $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code';
+ $commandline .= 'pid=$!; echo $pid >&3; wait $pid 2>/dev/null; code=$?; echo $code >&3; exit $code';
// Workaround for the bug, when PTS functionality is enabled.
// @see : https://bugs.php.net/69442
@@ -338,7 +341,7 @@ public function start(callable $callback = null, array $env = [])
$envPairs = [];
foreach ($env as $k => $v) {
- if (false !== $v) {
+ if (false !== $v && false === \in_array($k, ['argc', 'argv', 'ARGC', 'ARGV'], true)) {
$envPairs[] = $k.'='.$v;
}
}
@@ -349,7 +352,7 @@ public function start(callable $callback = null, array $env = [])
$this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options);
- if (!\is_resource($this->process)) {
+ if (!$this->process) {
throw new RuntimeException('Unable to launch a new process.');
}
$this->status = self::STATUS_STARTED;
@@ -383,7 +386,7 @@ public function start(callable $callback = null, array $env = [])
*
* @final
*/
- public function restart(callable $callback = null, array $env = []): self
+ public function restart(?callable $callback = null, array $env = []): self
{
if ($this->isRunning()) {
throw new RuntimeException('Process is already running.');
@@ -410,7 +413,7 @@ public function restart(callable $callback = null, array $env = []): self
* @throws ProcessSignaledException When process stopped after receiving signal
* @throws LogicException When process is not yet started
*/
- public function wait(callable $callback = null)
+ public function wait(?callable $callback = null)
{
$this->requireProcessIsStarted(__FUNCTION__);
@@ -426,7 +429,7 @@ public function wait(callable $callback = null)
do {
$this->checkTimeout();
- $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
+ $running = $this->isRunning() && ('\\' === \DIRECTORY_SEPARATOR || $this->processPipes->areOpen());
$this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
} while ($running);
@@ -530,7 +533,7 @@ public function disableOutput()
throw new RuntimeException('Disabling output while the process is running is not possible.');
}
if (null !== $this->idleTimeout) {
- throw new LogicException('Output can not be disabled while an idle timeout is set.');
+ throw new LogicException('Output cannot be disabled while an idle timeout is set.');
}
$this->outputDisabled = true;
@@ -569,7 +572,7 @@ public function isOutputDisabled()
/**
* Returns the current output of the process (STDOUT).
*
- * @return string The process output
+ * @return string
*
* @throws LogicException in case the output has been disabled
* @throws LogicException In case the process is not started
@@ -591,7 +594,7 @@ public function getOutput()
* In comparison with the getOutput method which always return the whole
* output, this one returns the new output since the last call.
*
- * @return string The process output since the last call
+ * @return string
*
* @throws LogicException in case the output has been disabled
* @throws LogicException In case the process is not started
@@ -615,10 +618,10 @@ public function getIncrementalOutput()
*
* @param int $flags A bit field of Process::ITER_* flags
*
+ * @return \Generator
+ *
* @throws LogicException in case the output has been disabled
* @throws LogicException In case the process is not started
- *
- * @return \Generator
*/
#[\ReturnTypeWillChange]
public function getIterator(int $flags = 0)
@@ -685,7 +688,7 @@ public function clearOutput()
/**
* Returns the current error output of the process (STDERR).
*
- * @return string The process error output
+ * @return string
*
* @throws LogicException in case the output has been disabled
* @throws LogicException In case the process is not started
@@ -708,7 +711,7 @@ public function getErrorOutput()
* whole error output, this one returns the new error output since the last
* call.
*
- * @return string The process error output since the last call
+ * @return string
*
* @throws LogicException in case the output has been disabled
* @throws LogicException In case the process is not started
@@ -776,7 +779,7 @@ public function getExitCodeText()
/**
* Checks if the process ended successfully.
*
- * @return bool true if the process ended successfully, false otherwise
+ * @return bool
*/
public function isSuccessful()
{
@@ -814,7 +817,7 @@ public function getTermSignal()
$this->requireProcessIsTerminated(__FUNCTION__);
if ($this->isSigchildEnabled() && -1 === $this->processInformation['termsig']) {
- throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
+ throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal cannot be retrieved.');
}
return $this->processInformation['termsig'];
@@ -855,7 +858,7 @@ public function getStopSignal()
/**
* Checks if the process is currently running.
*
- * @return bool true if the process is currently running, false otherwise
+ * @return bool
*/
public function isRunning()
{
@@ -871,7 +874,7 @@ public function isRunning()
/**
* Checks if the process has been started with no regard to the current state.
*
- * @return bool true if status is ready, false otherwise
+ * @return bool
*/
public function isStarted()
{
@@ -881,7 +884,7 @@ public function isStarted()
/**
* Checks if the process is terminated.
*
- * @return bool true if process is terminated, false otherwise
+ * @return bool
*/
public function isTerminated()
{
@@ -895,7 +898,7 @@ public function isTerminated()
*
* The status is one of: ready, started, terminated.
*
- * @return string The current process status
+ * @return string
*/
public function getStatus()
{
@@ -908,11 +911,11 @@ public function getStatus()
* Stops the process.
*
* @param int|float $timeout The timeout in seconds
- * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
+ * @param int|null $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
*
* @return int|null The exit-code of the process or null if it's not running
*/
- public function stop(float $timeout = 10, int $signal = null)
+ public function stop(float $timeout = 10, ?int $signal = null)
{
$timeoutMicro = microtime(true) + $timeout;
if ($this->isRunning()) {
@@ -971,8 +974,6 @@ public function addErrorOutput(string $line)
/**
* Gets the last output time in seconds.
- *
- * @return float|null The last output time in seconds or null if it isn't started
*/
public function getLastOutputTime(): ?float
{
@@ -982,7 +983,7 @@ public function getLastOutputTime(): ?float
/**
* Gets the command line to be executed.
*
- * @return string The command to execute
+ * @return string
*/
public function getCommandLine()
{
@@ -990,9 +991,9 @@ public function getCommandLine()
}
/**
- * Gets the process timeout (max. runtime).
+ * Gets the process timeout in seconds (max. runtime).
*
- * @return float|null The timeout in seconds or null if it's disabled
+ * @return float|null
*/
public function getTimeout()
{
@@ -1000,9 +1001,9 @@ public function getTimeout()
}
/**
- * Gets the process idle timeout (max. time since last output).
+ * Gets the process idle timeout in seconds (max. time since last output).
*
- * @return float|null The timeout in seconds or null if it's disabled
+ * @return float|null
*/
public function getIdleTimeout()
{
@@ -1038,7 +1039,7 @@ public function setTimeout(?float $timeout)
public function setIdleTimeout(?float $timeout)
{
if (null !== $timeout && $this->outputDisabled) {
- throw new LogicException('Idle timeout can not be set while the output is disabled.');
+ throw new LogicException('Idle timeout cannot be set while the output is disabled.');
}
$this->idleTimeout = $this->validateTimeout($timeout);
@@ -1071,7 +1072,7 @@ public function setTty(bool $tty)
/**
* Checks if the TTY mode is enabled.
*
- * @return bool true if the TTY mode is enabled, false otherwise
+ * @return bool
*/
public function isTty()
{
@@ -1103,7 +1104,7 @@ public function isPty()
/**
* Gets the working directory.
*
- * @return string|null The current working directory or null on failure
+ * @return string|null
*/
public function getWorkingDirectory()
{
@@ -1131,7 +1132,7 @@ public function setWorkingDirectory(string $cwd)
/**
* Gets the environment variables.
*
- * @return array The current environment variables
+ * @return array
*/
public function getEnv()
{
@@ -1141,25 +1142,12 @@ public function getEnv()
/**
* Sets the environment variables.
*
- * Each environment variable value should be a string.
- * If it is an array, the variable is ignored.
- * If it is false or null, it will be removed when
- * env vars are otherwise inherited.
- *
- * That happens in PHP when 'argv' is registered into
- * the $_ENV array for instance.
- *
- * @param array $env The new environment variables
+ * @param array $env The new environment variables
*
* @return $this
*/
public function setEnv(array $env)
{
- // Process can not handle env values that are arrays
- $env = array_filter($env, function ($value) {
- return !\is_array($value);
- });
-
$this->env = $env;
return $this;
@@ -1168,7 +1156,7 @@ public function setEnv(array $env)
/**
* Gets the Process input.
*
- * @return resource|string|\Iterator|null The Process input
+ * @return resource|string|\Iterator|null
*/
public function getInput()
{
@@ -1189,7 +1177,7 @@ public function getInput()
public function setInput($input)
{
if ($this->isRunning()) {
- throw new LogicException('Input can not be set while the process is running.');
+ throw new LogicException('Input cannot be set while the process is running.');
}
$this->input = ProcessUtils::validateInput(__METHOD__, $input);
@@ -1321,9 +1309,9 @@ private function getDescriptors(): array
*
* @param callable|null $callback The user defined PHP callback
*
- * @return \Closure A PHP closure
+ * @return \Closure
*/
- protected function buildCallback(callable $callback = null)
+ protected function buildCallback(?callable $callback = null)
{
if ($this->outputDisabled) {
return function ($type, $data) use ($callback): bool {
@@ -1358,6 +1346,19 @@ protected function updateStatus(bool $blocking)
$this->processInformation = proc_get_status($this->process);
$running = $this->processInformation['running'];
+ // In PHP < 8.3, "proc_get_status" only returns the correct exit status on the first call.
+ // Subsequent calls return -1 as the process is discarded. This workaround caches the first
+ // retrieved exit status for consistent results in later calls, mimicking PHP 8.3 behavior.
+ if (\PHP_VERSION_ID < 80300) {
+ if (!isset($this->cachedExitCode) && !$running && -1 !== $this->processInformation['exitcode']) {
+ $this->cachedExitCode = $this->processInformation['exitcode'];
+ }
+
+ if (isset($this->cachedExitCode) && !$running && -1 === $this->processInformation['exitcode']) {
+ $this->processInformation['exitcode'] = $this->cachedExitCode;
+ }
+ }
+
$this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running);
if ($this->fallbackStatus && $this->isSigchildEnabled()) {
@@ -1455,8 +1456,9 @@ private function readPipes(bool $blocking, bool $close)
private function close(): int
{
$this->processPipes->close();
- if (\is_resource($this->process)) {
+ if ($this->process) {
proc_close($this->process);
+ $this->process = null;
}
$this->exitcode = $this->processInformation['exitcode'];
$this->status = self::STATUS_TERMINATED;
@@ -1504,8 +1506,6 @@ private function resetProcessData()
* @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants)
* @param bool $throwException Whether to throw exception in case signal failed
*
- * @return bool True if the signal was sent successfully, false otherwise
- *
* @throws LogicException In case the process is not running
* @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
* @throws RuntimeException In case of failure
@@ -1514,7 +1514,7 @@ private function doSignal(int $signal, bool $throwException): bool
{
if (null === $pid = $this->getPid()) {
if ($throwException) {
- throw new LogicException('Can not send signal on a non running process.');
+ throw new LogicException('Cannot send signal on a non running process.');
}
return false;
@@ -1592,7 +1592,14 @@ function ($m) use (&$env, &$varCache, &$varCount, $uid) {
$cmd
);
- $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')';
+ static $comSpec;
+
+ if (!$comSpec && $comSpec = (new ExecutableFinder())->find('cmd.exe')) {
+ // Escape according to CommandLineToArgvW rules
+ $comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec) .'"';
+ }
+
+ $cmd = ($comSpec ?? 'cmd').' /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')';
foreach ($this->processPipes->getFiles() as $offset => $filename) {
$cmd .= ' '.$offset.'>"'.$filename.'"';
}
@@ -1638,7 +1645,7 @@ private function escapeArgument(?string $argument): string
if (str_contains($argument, "\0")) {
$argument = str_replace("\0", '?', $argument);
}
- if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) {
+ if (!preg_match('/[()%!^"<>&|\s]/', $argument)) {
return $argument;
}
$argument = preg_replace('/(\\\\+)$/', '$1$1', $argument);
@@ -1659,20 +1666,9 @@ private function replacePlaceholders(string $commandline, array $env)
private function getDefaultEnv(): array
{
- $env = [];
-
- foreach ($_SERVER as $k => $v) {
- if (\is_string($v) && false !== $v = getenv($k)) {
- $env[$k] = $v;
- }
- }
-
- foreach ($_ENV as $k => $v) {
- if (\is_string($v)) {
- $env[$k] = $v;
- }
- }
+ $env = getenv();
+ $env = ('\\' === \DIRECTORY_SEPARATOR ? array_intersect_ukey($env, $_SERVER, 'strcasecmp') : array_intersect_key($env, $_SERVER)) ?: $env;
- return $env;
+ return $_ENV + ('\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($env, $_ENV, 'strcasecmp') : $env);
}
}
diff --git a/ProcessUtils.php b/ProcessUtils.php
index 3be7e61a..2a7aff71 100644
--- a/ProcessUtils.php
+++ b/ProcessUtils.php
@@ -35,7 +35,7 @@ private function __construct()
* @param string $caller The name of method call that validates the input
* @param mixed $input The input to validate
*
- * @return mixed The validated input
+ * @return mixed
*
* @throws InvalidArgumentException In case the input is not valid
*/
@@ -48,7 +48,7 @@ public static function validateInput(string $caller, $input)
if (\is_string($input)) {
return $input;
}
- if (is_scalar($input)) {
+ if (\is_scalar($input)) {
return (string) $input;
}
if ($input instanceof Process) {
diff --git a/README.md b/README.md
index afce5e45..8777de4a 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,17 @@ Process Component
The Process component executes commands in sub-processes.
+Sponsor
+-------
+
+The Process component for Symfony 5.4/6.0 is [backed][1] by [SensioLabs][2].
+
+As the creator of Symfony, SensioLabs supports companies using Symfony, with an
+offering encompassing consultancy, expertise, services, training, and technical
+assistance to ensure the success of web application development projects.
+
+Help Symfony by [sponsoring][3] its development!
+
Resources
---------
@@ -11,3 +22,7 @@ Resources
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
+
+[1]: https://symfony.com/backers
+[2]: https://sensiolabs.com
+[3]: https://symfony.com/sponsor
diff --git a/Tests/ErrorProcessInitiator.php b/Tests/ErrorProcessInitiator.php
old mode 100755
new mode 100644
index 4c8556ac..0b75add6
--- a/Tests/ErrorProcessInitiator.php
+++ b/Tests/ErrorProcessInitiator.php
@@ -14,18 +14,18 @@
use Symfony\Component\Process\Exception\ProcessTimedOutException;
use Symfony\Component\Process\Process;
-require \dirname(__DIR__).'/vendor/autoload.php';
+require is_file(\dirname(__DIR__).'/vendor/autoload.php') ? \dirname(__DIR__).'/vendor/autoload.php' : \dirname(__DIR__, 5).'/vendor/autoload.php';
['e' => $php] = getopt('e:') + ['e' => 'php'];
try {
- $process = new Process("exec $php -r \"echo 'ready'; trigger_error('error', E_USER_ERROR);\"");
+ $process = new Process([$php, '-r', "echo 'ready'; trigger_error('error', E_USER_ERROR);"]);
$process->start();
$process->setTimeout(0.5);
while (!str_contains($process->getOutput(), 'ready')) {
usleep(1000);
}
- $process->signal(\SIGSTOP);
+ $process->isRunning() && $process->signal(\SIGSTOP);
$process->wait();
return $process->getExitCode();
diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php
index 83f263ff..84e5b3c3 100644
--- a/Tests/ExecutableFinderTest.php
+++ b/Tests/ExecutableFinderTest.php
@@ -19,29 +19,18 @@
*/
class ExecutableFinderTest extends TestCase
{
- private $path;
-
protected function tearDown(): void
{
- if ($this->path) {
- // Restore path if it was changed.
- putenv('PATH='.$this->path);
- }
- }
-
- private function setPath($path)
- {
- $this->path = getenv('PATH');
- putenv('PATH='.$path);
+ putenv('PATH='.($_SERVER['PATH'] ?? $_SERVER['Path']));
}
public function testFind()
{
- if (ini_get('open_basedir')) {
+ if (\ini_get('open_basedir')) {
$this->markTestSkipped('Cannot test when open_basedir is set');
}
- $this->setPath(\dirname(\PHP_BINARY));
+ putenv('PATH='.\dirname(\PHP_BINARY));
$finder = new ExecutableFinder();
$result = $finder->find($this->getPhpBinaryName());
@@ -51,13 +40,13 @@ public function testFind()
public function testFindWithDefault()
{
- if (ini_get('open_basedir')) {
+ if (\ini_get('open_basedir')) {
$this->markTestSkipped('Cannot test when open_basedir is set');
}
$expected = 'defaultValue';
- $this->setPath('');
+ putenv('PATH=');
$finder = new ExecutableFinder();
$result = $finder->find('foo', $expected);
@@ -67,11 +56,11 @@ public function testFindWithDefault()
public function testFindWithNullAsDefault()
{
- if (ini_get('open_basedir')) {
+ if (\ini_get('open_basedir')) {
$this->markTestSkipped('Cannot test when open_basedir is set');
}
- $this->setPath('');
+ putenv('PATH=');
$finder = new ExecutableFinder();
@@ -82,11 +71,11 @@ public function testFindWithNullAsDefault()
public function testFindWithExtraDirs()
{
- if (ini_get('open_basedir')) {
+ if (\ini_get('open_basedir')) {
$this->markTestSkipped('Cannot test when open_basedir is set');
}
- $this->setPath('');
+ putenv('PATH=');
$extraDirs = [\dirname(\PHP_BINARY)];
@@ -96,67 +85,94 @@ public function testFindWithExtraDirs()
$this->assertSamePath(\PHP_BINARY, $result);
}
+ /**
+ * @runInSeparateProcess
+ */
public function testFindWithOpenBaseDir()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Cannot run test on windows');
}
- if (ini_get('open_basedir')) {
+ if (\ini_get('open_basedir')) {
$this->markTestSkipped('Cannot test when open_basedir is set');
}
- $this->iniSet('open_basedir', \dirname(\PHP_BINARY).\PATH_SEPARATOR.'/');
+ putenv('PATH='.\dirname(\PHP_BINARY));
+ $initialOpenBaseDir = ini_set('open_basedir', \dirname(\PHP_BINARY).\PATH_SEPARATOR.'/');
- $finder = new ExecutableFinder();
- $result = $finder->find($this->getPhpBinaryName());
+ try {
+ $finder = new ExecutableFinder();
+ $result = $finder->find($this->getPhpBinaryName());
- $this->assertSamePath(\PHP_BINARY, $result);
+ $this->assertSamePath(\PHP_BINARY, $result);
+ } finally {
+ ini_set('open_basedir', $initialOpenBaseDir);
+ }
}
- public function testFindProcessInOpenBasedir()
+ /**
+ * @runInSeparateProcess
+ */
+ public function testFindBatchExecutableOnWindows()
{
- if (ini_get('open_basedir')) {
+ if (\ini_get('open_basedir')) {
$this->markTestSkipped('Cannot test when open_basedir is set');
}
- if ('\\' === \DIRECTORY_SEPARATOR) {
- $this->markTestSkipped('Cannot run test on windows');
+ if ('\\' !== \DIRECTORY_SEPARATOR) {
+ $this->markTestSkipped('Can be only tested on windows');
}
- $this->setPath('');
- $this->iniSet('open_basedir', \PHP_BINARY.\PATH_SEPARATOR.'/');
+ $target = str_replace('.tmp', '_tmp', tempnam(sys_get_temp_dir(), 'example-windows-executable'));
- $finder = new ExecutableFinder();
- $result = $finder->find($this->getPhpBinaryName(), false);
+ try {
+ touch($target);
+ touch($target.'.BAT');
- $this->assertSamePath(\PHP_BINARY, $result);
- }
+ $this->assertFalse(is_executable($target));
- public function testFindBatchExecutableOnWindows()
- {
- if (ini_get('open_basedir')) {
- $this->markTestSkipped('Cannot test when open_basedir is set');
- }
- if ('\\' !== \DIRECTORY_SEPARATOR) {
- $this->markTestSkipped('Can be only tested on windows');
+ putenv('PATH='.sys_get_temp_dir());
+
+ $finder = new ExecutableFinder();
+ $result = $finder->find(basename($target), false);
+ } finally {
+ unlink($target);
+ unlink($target.'.BAT');
}
- $target = tempnam(sys_get_temp_dir(), 'example-windows-executable');
+ $this->assertSamePath($target.'.BAT', $result);
+ }
- touch($target);
- touch($target.'.BAT');
+ /**
+ * @runInSeparateProcess
+ */
+ public function testEmptyDirInPath()
+ {
+ putenv(sprintf('PATH=%s%s', \dirname(\PHP_BINARY), \PATH_SEPARATOR));
- $this->assertFalse(is_executable($target));
+ try {
+ touch('executable');
+ chmod('executable', 0700);
- $this->setPath(sys_get_temp_dir());
+ $finder = new ExecutableFinder();
+ $result = $finder->find('executable');
- $finder = new ExecutableFinder();
- $result = $finder->find(basename($target), false);
+ $this->assertSame(sprintf('.%sexecutable', \DIRECTORY_SEPARATOR), $result);
+ } finally {
+ unlink('executable');
+ }
+ }
- unlink($target);
- unlink($target.'.BAT');
+ public function testFindBuiltInCommandOnWindows()
+ {
+ if ('\\' !== \DIRECTORY_SEPARATOR) {
+ $this->markTestSkipped('Can be only tested on windows');
+ }
- $this->assertSamePath($target.'.BAT', $result);
+ $finder = new ExecutableFinder();
+ $this->assertSame('rmdir', strtolower($finder->find('RMDIR')));
+ $this->assertSame('cd', strtolower($finder->find('cd')));
+ $this->assertSame('move', strtolower($finder->find('MoVe')));
}
private function assertSamePath($expected, $tested)
diff --git a/Tests/PhpExecutableFinderTest.php b/Tests/PhpExecutableFinderTest.php
index cf3ffb55..23de6d42 100644
--- a/Tests/PhpExecutableFinderTest.php
+++ b/Tests/PhpExecutableFinderTest.php
@@ -50,12 +50,36 @@ public function testFindArguments()
public function testNotExitsBinaryFile()
{
$f = new PhpExecutableFinder();
- $phpBinaryEnv = \PHP_BINARY;
- putenv('PHP_BINARY=/usr/local/php/bin/php-invalid');
- $this->assertFalse($f->find(), '::find() returns false because of not exist file');
- $this->assertFalse($f->find(false), '::find(false) returns false because of not exist file');
+ $originalPhpBinary = getenv('PHP_BINARY');
- putenv('PHP_BINARY='.$phpBinaryEnv);
+ try {
+ putenv('PHP_BINARY=/usr/local/php/bin/php-invalid');
+
+ $this->assertFalse($f->find(), '::find() returns false because of not exist file');
+ $this->assertFalse($f->find(false), '::find(false) returns false because of not exist file');
+ } finally {
+ putenv('PHP_BINARY='.$originalPhpBinary);
+ }
+ }
+
+ public function testFindWithExecutableDirectory()
+ {
+ if ('\\' === \DIRECTORY_SEPARATOR) {
+ $this->markTestSkipped('Directories are not executable on Windows');
+ }
+
+ $originalPhpBinary = getenv('PHP_BINARY');
+
+ try {
+ $executableDirectoryPath = sys_get_temp_dir().'/PhpExecutableFinderTest_testFindWithExecutableDirectory';
+ @mkdir($executableDirectoryPath);
+ $this->assertTrue(is_executable($executableDirectoryPath));
+ putenv('PHP_BINARY='.$executableDirectoryPath);
+
+ $this->assertFalse((new PhpExecutableFinder())->find());
+ } finally {
+ putenv('PHP_BINARY='.$originalPhpBinary);
+ }
}
}
diff --git a/Tests/PipeStdinInStdoutStdErrStreamSelect.php b/Tests/PipeStdinInStdoutStdErrStreamSelect.php
index a206d2b8..09124a4b 100644
--- a/Tests/PipeStdinInStdoutStdErrStreamSelect.php
+++ b/Tests/PipeStdinInStdoutStdErrStreamSelect.php
@@ -34,7 +34,7 @@
exit(ERR_TIMEOUT);
}
- if (in_array(\STDOUT, $w) && strlen($out) > 0) {
+ if (in_array(\STDOUT, $w) && '' !== $out) {
$written = fwrite(\STDOUT, (string) $out, 32768);
if (false === $written) {
exit(ERR_WRITE_FAILED);
@@ -45,7 +45,7 @@
$write = array_diff($write, [\STDOUT]);
}
- if (in_array(\STDERR, $w) && strlen($err) > 0) {
+ if (in_array(\STDERR, $w) && '' !== $err) {
$written = fwrite(\STDERR, (string) $err, 32768);
if (false === $written) {
exit(ERR_WRITE_FAILED);
diff --git a/Tests/ProcessFailedExceptionTest.php b/Tests/ProcessFailedExceptionTest.php
index d6d7bfb0..259ffd63 100644
--- a/Tests/ProcessFailedExceptionTest.php
+++ b/Tests/ProcessFailedExceptionTest.php
@@ -25,7 +25,7 @@ class ProcessFailedExceptionTest extends TestCase
*/
public function testProcessFailedExceptionThrowsException()
{
- $process = $this->getMockBuilder(Process::class)->setMethods(['isSuccessful'])->setConstructorArgs([['php']])->getMock();
+ $process = $this->getMockBuilder(Process::class)->onlyMethods(['isSuccessful'])->setConstructorArgs([['php']])->getMock();
$process->expects($this->once())
->method('isSuccessful')
->willReturn(true);
@@ -49,7 +49,7 @@ public function testProcessFailedExceptionPopulatesInformationFromProcessOutput(
$errorOutput = 'FATAL: Unexpected error';
$workingDirectory = getcwd();
- $process = $this->getMockBuilder(Process::class)->setMethods(['isSuccessful', 'getOutput', 'getErrorOutput', 'getExitCode', 'getExitCodeText', 'isOutputDisabled', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock();
+ $process = $this->getMockBuilder(Process::class)->onlyMethods(['isSuccessful', 'getOutput', 'getErrorOutput', 'getExitCode', 'getExitCodeText', 'isOutputDisabled', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock();
$process->expects($this->once())
->method('isSuccessful')
->willReturn(false);
@@ -97,7 +97,7 @@ public function testDisabledOutputInFailedExceptionDoesNotPopulateOutput()
$exitText = 'General error';
$workingDirectory = getcwd();
- $process = $this->getMockBuilder(Process::class)->setMethods(['isSuccessful', 'isOutputDisabled', 'getExitCode', 'getExitCodeText', 'getOutput', 'getErrorOutput', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock();
+ $process = $this->getMockBuilder(Process::class)->onlyMethods(['isSuccessful', 'isOutputDisabled', 'getExitCode', 'getExitCodeText', 'getOutput', 'getErrorOutput', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock();
$process->expects($this->once())
->method('isSuccessful')
->willReturn(false);
diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php
index 70b8e051..e4d92874 100644
--- a/Tests/ProcessTest.php
+++ b/Tests/ProcessTest.php
@@ -66,11 +66,11 @@ public function testInvalidCwd()
$cmd->run();
}
+ /**
+ * @group transient-on-windows
+ */
public function testThatProcessDoesNotThrowWarningDuringRun()
{
- if ('\\' === \DIRECTORY_SEPARATOR) {
- $this->markTestSkipped('This test is transient on Windows');
- }
@trigger_error('Test Error', \E_USER_NOTICE);
$process = $this->getProcessForCode('sleep(3)');
$process->run();
@@ -130,12 +130,11 @@ public function testStopWithTimeoutIsActuallyWorking()
$this->assertLessThan(15, microtime(true) - $start);
}
+ /**
+ * @group transient-on-windows
+ */
public function testWaitUntilSpecificOutput()
{
- if ('\\' === \DIRECTORY_SEPARATOR) {
- $this->markTestIncomplete('This test is too transient on Windows, help wanted to improve it');
- }
-
$p = $this->getProcess([self::$phpBin, __DIR__.'/KillableProcessWithOutput.php']);
$p->start();
@@ -276,7 +275,7 @@ public function testLiveStreamAsInput()
public function testSetInputWhileRunningThrowsAnException()
{
$this->expectException(LogicException::class);
- $this->expectExceptionMessage('Input can not be set while the process is running.');
+ $this->expectExceptionMessage('Input cannot be set while the process is running.');
$process = $this->getProcessForCode('sleep(30);');
$process->start();
try {
@@ -301,7 +300,7 @@ public function testInvalidInput($value)
$process->setInput($value);
}
- public function provideInvalidInputValues()
+ public static function provideInvalidInputValues()
{
return [
[[]],
@@ -319,7 +318,7 @@ public function testValidInput($expected, $value)
$this->assertSame($expected, $process->getInput());
}
- public function provideInputValues()
+ public static function provideInputValues()
{
return [
[null, null],
@@ -328,7 +327,7 @@ public function provideInputValues()
];
}
- public function chainedCommandsOutputProvider()
+ public static function chainedCommandsOutputProvider()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
return [
@@ -422,7 +421,7 @@ public function testIncrementalOutput($getOutput, $getIncrementalOutput, $uri)
fclose($h);
}
- public function provideIncrementalOutput()
+ public static function provideIncrementalOutput()
{
return [
['getOutput', 'getIncrementalOutput', 'php://stdout'],
@@ -772,7 +771,8 @@ public function testIterateOverProcessWithTimeout()
$start = microtime(true);
try {
$process->start();
- foreach ($process as $buffer);
+ foreach ($process as $buffer) {
+ }
$this->fail('A RuntimeException should have been raised');
} catch (RuntimeException $e) {
}
@@ -938,7 +938,7 @@ public function testExitCodeIsAvailableAfterSignal()
public function testSignalProcessNotRunning()
{
$this->expectException(LogicException::class);
- $this->expectExceptionMessage('Can not send signal on a non running process.');
+ $this->expectExceptionMessage('Cannot send signal on a non running process.');
$process = $this->getProcess('foo');
$process->signal(1); // SIGHUP
}
@@ -956,7 +956,7 @@ public function testMethodsThatNeedARunningProcess($method)
$process->{$method}();
}
- public function provideMethodsThatNeedARunningProcess()
+ public static function provideMethodsThatNeedARunningProcess()
{
return [
['getOutput'],
@@ -987,7 +987,7 @@ public function testMethodsThatNeedATerminatedProcess($method)
throw $e;
}
- public function provideMethodsThatNeedATerminatedProcess()
+ public static function provideMethodsThatNeedATerminatedProcess()
{
return [
['hasBeenSignaled'],
@@ -1057,7 +1057,7 @@ public function testEnableOrDisableOutputAfterRunDoesNotThrowException()
public function testDisableOutputWhileIdleTimeoutIsSet()
{
$this->expectException(LogicException::class);
- $this->expectExceptionMessage('Output can not be disabled while an idle timeout is set.');
+ $this->expectExceptionMessage('Output cannot be disabled while an idle timeout is set.');
$process = $this->getProcess('foo');
$process->setIdleTimeout(1);
$process->disableOutput();
@@ -1066,7 +1066,7 @@ public function testDisableOutputWhileIdleTimeoutIsSet()
public function testSetIdleTimeoutWhileOutputIsDisabled()
{
$this->expectException(LogicException::class);
- $this->expectExceptionMessage('timeout can not be set while the output is disabled.');
+ $this->expectExceptionMessage('timeout cannot be set while the output is disabled.');
$process = $this->getProcess('foo');
$process->disableOutput();
$process->setIdleTimeout(1);
@@ -1092,7 +1092,7 @@ public function testGetOutputWhileDisabled($fetchMethod)
$p->{$fetchMethod}();
}
- public function provideOutputFetchingMethods()
+ public static function provideOutputFetchingMethods()
{
return [
['getOutput'],
@@ -1129,17 +1129,17 @@ public function testTermSignalTerminatesProcessCleanly()
$this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
}
- public function responsesCodeProvider()
+ public static function responsesCodeProvider()
{
return [
- //expected output / getter / code to execute
+ // expected output / getter / code to execute
// [1,'getExitCode','exit(1);'],
// [true,'isSuccessful','exit();'],
['output', 'getOutput', 'echo \'output\';'],
];
}
- public function pipesCodeProvider()
+ public static function pipesCodeProvider()
{
$variations = [
'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);',
@@ -1182,7 +1182,7 @@ public function testIncrementalOutputDoesNotRequireAnotherCall($stream, $method)
$process->stop();
}
- public function provideVariousIncrementals()
+ public static function provideVariousIncrementals()
{
return [
['php://stdout', 'getIncrementalOutput'],
@@ -1424,7 +1424,12 @@ public function testGetCommandLine()
{
$p = new Process(['/usr/bin/php']);
- $expected = '\\' === \DIRECTORY_SEPARATOR ? '"/usr/bin/php"' : "'/usr/bin/php'";
+ $expected = '\\' === \DIRECTORY_SEPARATOR ? '/usr/bin/php' : "'/usr/bin/php'";
+ $this->assertSame($expected, $p->getCommandLine());
+
+ $p = new Process(['cd', '/d']);
+
+ $expected = '\\' === \DIRECTORY_SEPARATOR ? 'cd /d' : "'cd' '/d'";
$this->assertSame($expected, $p->getCommandLine());
}
@@ -1448,7 +1453,7 @@ public function testRawCommandLine()
$this->assertSame($expected, str_replace('Standard input code', '-', $p->getOutput()));
}
- public function provideEscapeArgument()
+ public static function provideEscapeArgument()
{
yield ['a"b%c%'];
yield ['a"b^c^'];
@@ -1504,8 +1509,11 @@ public function testPreparedCommandWithNoValues()
public function testEnvArgument()
{
- $env = ['FOO' => 'Foo', 'BAR' => 'Bar'];
$cmd = '\\' === \DIRECTORY_SEPARATOR ? 'echo !FOO! !BAR! !BAZ!' : 'echo $FOO $BAR $BAZ';
+ $p = Process::fromShellCommandline($cmd);
+ $this->assertSame([], $p->getEnv());
+
+ $env = ['FOO' => 'Foo', 'BAR' => 'Bar'];
$p = Process::fromShellCommandline($cmd, null, $env);
$p->run(null, ['BAR' => 'baR', 'BAZ' => 'baZ']);
@@ -1520,13 +1528,95 @@ public function testWaitStoppedDeadProcess()
$process->setTimeout(2);
$process->wait();
$this->assertFalse($process->isRunning());
+
+ if ('\\' !== \DIRECTORY_SEPARATOR && !\Closure::bind(function () { return $this->isSigchildEnabled(); }, $process, $process)()) {
+ $this->assertSame(0, $process->getExitCode());
+ }
+ }
+
+ public function testEnvCaseInsensitiveOnWindows()
+ {
+ $p = $this->getProcessForCode('print_r([$_SERVER[\'PATH\'] ?? 1, $_SERVER[\'Path\'] ?? 2]);', null, ['PATH' => 'bar/baz']);
+ $p->run(null, ['Path' => 'foo/bar']);
+
+ if ('\\' === \DIRECTORY_SEPARATOR) {
+ $this->assertSame('Array ( [0] => 1 [1] => foo/bar )', preg_replace('/\s++/', ' ', trim($p->getOutput())));
+ } else {
+ $this->assertSame('Array ( [0] => bar/baz [1] => foo/bar )', preg_replace('/\s++/', ' ', trim($p->getOutput())));
+ }
+ }
+
+ public function testMultipleCallsToProcGetStatus()
+ {
+ $process = $this->getProcess('echo foo');
+ $process->start(static function () use ($process) {
+ return $process->isRunning();
+ });
+ while ($process->isRunning()) {
+ usleep(1000);
+ }
+ $this->assertSame(0, $process->getExitCode());
+ }
+
+ public function testFailingProcessWithMultipleCallsToProcGetStatus()
+ {
+ $process = $this->getProcess('exit 123');
+ $process->start(static function () use ($process) {
+ return $process->isRunning();
+ });
+ while ($process->isRunning()) {
+ usleep(1000);
+ }
+ $this->assertSame(123, $process->getExitCode());
+ }
+
+ /**
+ * @group slow
+ */
+ public function testLongRunningProcessWithMultipleCallsToProcGetStatus()
+ {
+ $process = $this->getProcess('sleep 1 && echo "done" && php -r "exit(0);"');
+ $process->start(static function () use ($process) {
+ return $process->isRunning();
+ });
+ while ($process->isRunning()) {
+ usleep(1000);
+ }
+ $this->assertSame(0, $process->getExitCode());
+ }
+
+ /**
+ * @group slow
+ */
+ public function testLongRunningProcessWithMultipleCallsToProcGetStatusError()
+ {
+ $process = $this->getProcess('sleep 1 && echo "failure" && php -r "exit(123);"');
+ $process->start(static function () use ($process) {
+ return $process->isRunning();
+ });
+ while ($process->isRunning()) {
+ usleep(1000);
+ }
+ $this->assertSame(123, $process->getExitCode());
+ }
+
+ /**
+ * @group transient-on-windows
+ */
+ public function testNotTerminableInputPipe()
+ {
+ $process = $this->getProcess('echo foo');
+ $process->setInput(\STDIN);
+ $process->start();
+ $process->setTimeout(2);
+ $process->wait();
+ $this->assertFalse($process->isRunning());
}
/**
* @param string|array $commandline
- * @param mixed $input
*/
- private function getProcess($commandline, string $cwd = null, array $env = null, $input = null, ?int $timeout = 60): Process
+ private function getProcess($commandline, ?string $cwd = null, ?array $env = null, $input = null, ?int $timeout = 60): Process
{
if (\is_string($commandline)) {
$process = Process::fromShellCommandline($commandline, $cwd, $env, $input, $timeout);
@@ -1541,7 +1631,7 @@ private function getProcess($commandline, string $cwd = null, array $env = null,
return self::$process = $process;
}
- private function getProcessForCode(string $code, string $cwd = null, array $env = null, $input = null, ?int $timeout = 60): Process
+ private function getProcessForCode(string $code, ?string $cwd = null, ?array $env = null, $input = null, ?int $timeout = 60): Process
{
return $this->getProcess([self::$phpBin, '-r', $code], $cwd, $env, $input, $timeout);
}