From 2ad752b7ac0e488beaa4555df0320f60c93571d8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 19 May 2021 15:18:37 +0200 Subject: [PATCH 01/73] Bump Symfony 6 to PHP 8 --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index d90078c3..9f1aa8cf 100644 --- a/composer.json +++ b/composer.json @@ -16,8 +16,7 @@ } ], "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.15" + "php": ">=8.0.2" }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" }, From 4a56cc900fabeaa3bf7f4295a36d21cfd307abd9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 2 Jun 2021 18:09:43 +0200 Subject: [PATCH 02/73] Update phpunit.xml.dist files for phpunit >= 9.3 --- phpunit.xml.dist | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c32f2510..13bd3f83 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ - - + + ./ - - ./Tests - ./vendor - - - + + + ./Tests + ./vendor + + From 25fc22d2608595d8f90fab042b354a36cf55fffd Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 27 May 2021 19:18:44 +0200 Subject: [PATCH 03/73] Add union types --- InputStream.php | 2 +- PhpProcess.php | 2 +- Pipes/AbstractPipes.php | 4 ++-- Pipes/UnixPipes.php | 2 +- Pipes/WindowsPipes.php | 2 +- Process.php | 6 +++--- ProcessUtils.php | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/InputStream.php b/InputStream.php index c86fca86..4a6811aa 100644 --- a/InputStream.php +++ b/InputStream.php @@ -39,7 +39,7 @@ public function onEmpty(callable $onEmpty = null) * @param resource|string|int|float|bool|\Traversable|null $input The input to append as scalar, * stream resource or \Traversable */ - public function write($input) + public function write(mixed $input) { if (null === $input) { return; diff --git a/PhpProcess.php b/PhpProcess.php index 2bc338e5..d5d08845 100644 --- a/PhpProcess.php +++ b/PhpProcess.php @@ -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, mixed $input = null, ?float $timeout = 60) { throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } diff --git a/Pipes/AbstractPipes.php b/Pipes/AbstractPipes.php index 77636c2a..1d37eb76 100644 --- a/Pipes/AbstractPipes.php +++ b/Pipes/AbstractPipes.php @@ -30,7 +30,7 @@ abstract class AbstractPipes implements PipesInterface /** * @param resource|string|int|float|bool|\Iterator|null $input */ - public function __construct($input) + public function __construct(mixed $input) { if (\is_resource($input) || $input instanceof \Iterator) { $this->input = $input; @@ -171,7 +171,7 @@ protected function write(): ?array /** * @internal */ - public function handleError($type, $msg) + public function handleError(int $type, string $msg) { $this->lastError = $msg; } diff --git a/Pipes/UnixPipes.php b/Pipes/UnixPipes.php index 7cb5bab7..0081f932 100644 --- a/Pipes/UnixPipes.php +++ b/Pipes/UnixPipes.php @@ -26,7 +26,7 @@ class UnixPipes extends AbstractPipes private $ptyMode; private $haveReadSupport; - public function __construct(?bool $ttyMode, bool $ptyMode, $input, bool $haveReadSupport) + public function __construct(?bool $ttyMode, bool $ptyMode, mixed $input, bool $haveReadSupport) { $this->ttyMode = $ttyMode; $this->ptyMode = $ptyMode; diff --git a/Pipes/WindowsPipes.php b/Pipes/WindowsPipes.php index 3a1ef405..611f7ae5 100644 --- a/Pipes/WindowsPipes.php +++ b/Pipes/WindowsPipes.php @@ -35,7 +35,7 @@ class WindowsPipes extends AbstractPipes ]; private $haveReadSupport; - public function __construct($input, bool $haveReadSupport) + public function __construct(mixed $input, bool $haveReadSupport) { $this->haveReadSupport = $haveReadSupport; diff --git a/Process.php b/Process.php index 877f16cd..da1514c0 100644 --- a/Process.php +++ b/Process.php @@ -138,7 +138,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, mixed $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.'); @@ -187,7 +187,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, mixed $input = null, ?float $timeout = 60) { $process = new static([], $cwd, $env, $input, $timeout); $process->commandline = $command; @@ -1182,7 +1182,7 @@ public function getInput() * * @throws LogicException In case the process is running */ - public function setInput($input) + public function setInput(mixed $input) { if ($this->isRunning()) { throw new LogicException('Input can not be set while the process is running.'); diff --git a/ProcessUtils.php b/ProcessUtils.php index 3be7e61a..d5bc4478 100644 --- a/ProcessUtils.php +++ b/ProcessUtils.php @@ -39,7 +39,7 @@ private function __construct() * * @throws InvalidArgumentException In case the input is not valid */ - public static function validateInput(string $caller, $input) + public static function validateInput(string $caller, mixed $input) { if (null !== $input) { if (\is_resource($input)) { From 545332f29b514c66bb6b92ebb4bad007cdfd5d81 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 12 Jul 2021 11:26:55 +0200 Subject: [PATCH 04/73] Add return types, round 1 --- Pipes/UnixPipes.php | 2 +- Pipes/WindowsPipes.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Pipes/UnixPipes.php b/Pipes/UnixPipes.php index 0081f932..063aa6ad 100644 --- a/Pipes/UnixPipes.php +++ b/Pipes/UnixPipes.php @@ -35,7 +35,7 @@ public function __construct(?bool $ttyMode, bool $ptyMode, mixed $input, bool $h parent::__construct($input); } - public function __sleep() + public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } diff --git a/Pipes/WindowsPipes.php b/Pipes/WindowsPipes.php index 611f7ae5..e68ed951 100644 --- a/Pipes/WindowsPipes.php +++ b/Pipes/WindowsPipes.php @@ -88,7 +88,7 @@ public function __construct(mixed $input, bool $haveReadSupport) parent::__construct($input); } - public function __sleep() + public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } From e96c4f6dee4e49fb849debe833aba0a8b68d5e69 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 21 Jul 2021 12:04:28 +0200 Subject: [PATCH 05/73] Narrow existing return types on private/internal/final/test methods --- Process.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Process.php b/Process.php index a2fdb71d..e7ed10ed 100644 --- a/Process.php +++ b/Process.php @@ -258,13 +258,11 @@ public function run(callable $callback = null, array $env = []): int * This is identical to run() except that an exception is thrown if the process * exits with a non-zero exit code. * - * @return $this - * * @throws ProcessFailedException if the process didn't terminate successfully * * @final */ - public function mustRun(callable $callback = null, array $env = []): self + public function mustRun(callable $callback = null, array $env = []): static { if (0 !== $this->run($callback, $env)) { throw new ProcessFailedException($this); @@ -374,8 +372,6 @@ public function start(callable $callback = null, array $env = []) * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * - * @return static - * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process is already running * @@ -383,7 +379,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 = []): static { if ($this->isRunning()) { throw new RuntimeException('Process is already running.'); From d5052fc2a90891954a5d469fb3a9b9905b2fd6ed Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 5 Aug 2021 00:33:39 +0200 Subject: [PATCH 06/73] Remove ReturnTypeWillChange Signed-off-by: Alexander M. Turek --- InputStream.php | 6 +----- Process.php | 5 +---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/InputStream.php b/InputStream.php index d4730d63..d970ccd1 100644 --- a/InputStream.php +++ b/InputStream.php @@ -66,11 +66,7 @@ public function isClosed() return !$this->open; } - /** - * @return \Traversable - */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \Traversable { $this->open = true; diff --git a/Process.php b/Process.php index e7ed10ed..aa64e050 100644 --- a/Process.php +++ b/Process.php @@ -613,11 +613,8 @@ public function getIncrementalOutput() * * @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) + public function getIterator(int $flags = 0): \Generator { $this->readPipesForOutput(__FUNCTION__, false); From 03ee97adf3a53fec824499b3731f7db82ca5d610 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 12 Aug 2021 17:01:43 +0200 Subject: [PATCH 07/73] Add return types - batch 2/n --- ExecutableFinder.php | 2 +- PhpExecutableFinder.php | 4 +- PhpProcess.php | 2 +- Process.php | 86 ++++++++++++++++++++--------------------- ProcessUtils.php | 2 +- 5 files changed, 48 insertions(+), 48 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index feee4ad4..849a7241 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -46,7 +46,7 @@ public function addSuffix(string $suffix) * * @return string|null The executable path or default value */ - public function find(string $name, string $default = null, array $extraDirs = []) + public function find(string $name, string $default = null, array $extraDirs = []): ?string { if (ini_get('open_basedir')) { $searchPath = array_merge(explode(\PATH_SEPARATOR, ini_get('open_basedir')), $extraDirs); diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index e4f03f76..a32f948b 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -31,7 +31,7 @@ public function __construct() * * @return string|false The PHP executable path or false if it cannot be found */ - public function find(bool $includeArgs = true) + public function find(bool $includeArgs = true): string|false { if ($php = getenv('PHP_BINARY')) { if (!is_executable($php)) { @@ -87,7 +87,7 @@ public function find(bool $includeArgs = true) * * @return array The PHP executable arguments */ - public function findArguments() + public function findArguments(): array { $arguments = []; if ('phpdbg' === \PHP_SAPI) { diff --git a/PhpProcess.php b/PhpProcess.php index d5d08845..cb749f9b 100644 --- a/PhpProcess.php +++ b/PhpProcess.php @@ -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, mixed $input = null, ?float $timeout = 60) + public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, mixed $input = null, ?float $timeout = 60): static { throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } diff --git a/Process.php b/Process.php index aa64e050..cf40fc89 100644 --- a/Process.php +++ b/Process.php @@ -187,7 +187,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, mixed $input = null, ?float $timeout = 60) + public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, mixed $input = null, ?float $timeout = 60): static { $process = new static([], $cwd, $env, $input, $timeout); $process->commandline = $command; @@ -198,7 +198,7 @@ public static function fromShellCommandline(string $command, string $cwd = null, /** * @return array */ - public function __sleep() + public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } @@ -406,7 +406,7 @@ public function restart(callable $callback = null, array $env = []): static * @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): int { $this->requireProcessIsStarted(__FUNCTION__); @@ -489,7 +489,7 @@ public function waitUntil(callable $callback): bool * * @return int|null The process id if running, null otherwise */ - public function getPid() + public function getPid(): ?int { return $this->isRunning() ? $this->processInformation['pid'] : null; } @@ -505,7 +505,7 @@ public function getPid() * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed * @throws RuntimeException In case of failure */ - public function signal(int $signal) + public function signal(int $signal): static { $this->doSignal($signal, true); @@ -520,7 +520,7 @@ public function signal(int $signal) * @throws RuntimeException In case the process is already running * @throws LogicException if an idle timeout is set */ - public function disableOutput() + public function disableOutput(): static { if ($this->isRunning()) { throw new RuntimeException('Disabling output while the process is running is not possible.'); @@ -541,7 +541,7 @@ public function disableOutput() * * @throws RuntimeException In case the process is already running */ - public function enableOutput() + public function enableOutput(): static { if ($this->isRunning()) { throw new RuntimeException('Enabling output while the process is running is not possible.'); @@ -557,7 +557,7 @@ public function enableOutput() * * @return bool */ - public function isOutputDisabled() + public function isOutputDisabled(): bool { return $this->outputDisabled; } @@ -570,7 +570,7 @@ public function isOutputDisabled() * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ - public function getOutput() + public function getOutput(): string { $this->readPipesForOutput(__FUNCTION__); @@ -592,7 +592,7 @@ public function getOutput() * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ - public function getIncrementalOutput() + public function getIncrementalOutput(): string { $this->readPipesForOutput(__FUNCTION__); @@ -666,7 +666,7 @@ public function getIterator(int $flags = 0): \Generator * * @return $this */ - public function clearOutput() + public function clearOutput(): static { ftruncate($this->stdout, 0); fseek($this->stdout, 0); @@ -683,7 +683,7 @@ public function clearOutput() * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ - public function getErrorOutput() + public function getErrorOutput(): string { $this->readPipesForOutput(__FUNCTION__); @@ -706,7 +706,7 @@ public function getErrorOutput() * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ - public function getIncrementalErrorOutput() + public function getIncrementalErrorOutput(): string { $this->readPipesForOutput(__FUNCTION__); @@ -725,7 +725,7 @@ public function getIncrementalErrorOutput() * * @return $this */ - public function clearErrorOutput() + public function clearErrorOutput(): static { ftruncate($this->stderr, 0); fseek($this->stderr, 0); @@ -739,7 +739,7 @@ public function clearErrorOutput() * * @return int|null The exit status code, null if the Process is not terminated */ - public function getExitCode() + public function getExitCode(): ?int { $this->updateStatus(false); @@ -757,7 +757,7 @@ public function getExitCode() * @see http://tldp.org/LDP/abs/html/exitcodes.html * @see http://en.wikipedia.org/wiki/Unix_signal */ - public function getExitCodeText() + public function getExitCodeText(): ?string { if (null === $exitcode = $this->getExitCode()) { return null; @@ -771,7 +771,7 @@ public function getExitCodeText() * * @return bool true if the process ended successfully, false otherwise */ - public function isSuccessful() + public function isSuccessful(): bool { return 0 === $this->getExitCode(); } @@ -785,7 +785,7 @@ public function isSuccessful() * * @throws LogicException In case the process is not terminated */ - public function hasBeenSignaled() + public function hasBeenSignaled(): bool { $this->requireProcessIsTerminated(__FUNCTION__); @@ -802,7 +802,7 @@ public function hasBeenSignaled() * @throws RuntimeException In case --enable-sigchild is activated * @throws LogicException In case the process is not terminated */ - public function getTermSignal() + public function getTermSignal(): int { $this->requireProcessIsTerminated(__FUNCTION__); @@ -822,7 +822,7 @@ public function getTermSignal() * * @throws LogicException In case the process is not terminated */ - public function hasBeenStopped() + public function hasBeenStopped(): bool { $this->requireProcessIsTerminated(__FUNCTION__); @@ -838,7 +838,7 @@ public function hasBeenStopped() * * @throws LogicException In case the process is not terminated */ - public function getStopSignal() + public function getStopSignal(): int { $this->requireProcessIsTerminated(__FUNCTION__); @@ -850,7 +850,7 @@ public function getStopSignal() * * @return bool true if the process is currently running, false otherwise */ - public function isRunning() + public function isRunning(): bool { if (self::STATUS_STARTED !== $this->status) { return false; @@ -866,7 +866,7 @@ public function isRunning() * * @return bool true if status is ready, false otherwise */ - public function isStarted() + public function isStarted(): bool { return self::STATUS_READY != $this->status; } @@ -876,7 +876,7 @@ public function isStarted() * * @return bool true if process is terminated, false otherwise */ - public function isTerminated() + public function isTerminated(): bool { $this->updateStatus(false); @@ -890,7 +890,7 @@ public function isTerminated() * * @return string The current process status */ - public function getStatus() + public function getStatus(): string { $this->updateStatus(false); @@ -905,7 +905,7 @@ public function getStatus() * * @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): ?int { $timeoutMicro = microtime(true) + $timeout; if ($this->isRunning()) { @@ -977,7 +977,7 @@ public function getLastOutputTime(): ?float * * @return string The command to execute */ - public function getCommandLine() + public function getCommandLine(): string { return \is_array($this->commandline) ? implode(' ', array_map([$this, 'escapeArgument'], $this->commandline)) : $this->commandline; } @@ -987,7 +987,7 @@ public function getCommandLine() * * @return float|null The timeout in seconds or null if it's disabled */ - public function getTimeout() + public function getTimeout(): ?float { return $this->timeout; } @@ -997,7 +997,7 @@ public function getTimeout() * * @return float|null The timeout in seconds or null if it's disabled */ - public function getIdleTimeout() + public function getIdleTimeout(): ?float { return $this->idleTimeout; } @@ -1011,7 +1011,7 @@ public function getIdleTimeout() * * @throws InvalidArgumentException if the timeout is negative */ - public function setTimeout(?float $timeout) + public function setTimeout(?float $timeout): static { $this->timeout = $this->validateTimeout($timeout); @@ -1028,7 +1028,7 @@ public function setTimeout(?float $timeout) * @throws LogicException if the output is disabled * @throws InvalidArgumentException if the timeout is negative */ - public function setIdleTimeout(?float $timeout) + public function setIdleTimeout(?float $timeout): static { if (null !== $timeout && $this->outputDisabled) { throw new LogicException('Idle timeout can not be set while the output is disabled.'); @@ -1046,7 +1046,7 @@ public function setIdleTimeout(?float $timeout) * * @throws RuntimeException In case the TTY mode is not supported */ - public function setTty(bool $tty) + public function setTty(bool $tty): static { if ('\\' === \DIRECTORY_SEPARATOR && $tty) { throw new RuntimeException('TTY mode is not supported on Windows platform.'); @@ -1066,7 +1066,7 @@ public function setTty(bool $tty) * * @return bool true if the TTY mode is enabled, false otherwise */ - public function isTty() + public function isTty(): bool { return $this->tty; } @@ -1076,7 +1076,7 @@ public function isTty() * * @return $this */ - public function setPty(bool $bool) + public function setPty(bool $bool): static { $this->pty = $bool; @@ -1088,7 +1088,7 @@ public function setPty(bool $bool) * * @return bool */ - public function isPty() + public function isPty(): bool { return $this->pty; } @@ -1098,7 +1098,7 @@ public function isPty() * * @return string|null The current working directory or null on failure */ - public function getWorkingDirectory() + public function getWorkingDirectory(): ?string { if (null === $this->cwd) { // getcwd() will return false if any one of the parent directories does not have @@ -1114,7 +1114,7 @@ public function getWorkingDirectory() * * @return $this */ - public function setWorkingDirectory(string $cwd) + public function setWorkingDirectory(string $cwd): static { $this->cwd = $cwd; @@ -1126,7 +1126,7 @@ public function setWorkingDirectory(string $cwd) * * @return array The current environment variables */ - public function getEnv() + public function getEnv(): array { return $this->env; } @@ -1146,7 +1146,7 @@ public function getEnv() * * @return $this */ - public function setEnv(array $env) + public function setEnv(array $env): static { // Process can not handle env values that are arrays $env = array_filter($env, function ($value) { @@ -1179,7 +1179,7 @@ public function getInput() * * @throws LogicException In case the process is running */ - public function setInput(mixed $input) + public function setInput(mixed $input): static { if ($this->isRunning()) { throw new LogicException('Input can not be set while the process is running.'); @@ -1274,7 +1274,7 @@ public static function isTtySupported(): bool * * @return bool */ - public static function isPtySupported() + public static function isPtySupported(): bool { static $result; @@ -1316,7 +1316,7 @@ private function getDescriptors(): array * * @return \Closure A PHP closure */ - protected function buildCallback(callable $callback = null) + protected function buildCallback(callable $callback = null): \Closure { if ($this->outputDisabled) { return function ($type, $data) use ($callback): bool { @@ -1367,7 +1367,7 @@ protected function updateStatus(bool $blocking) * * @return bool */ - protected function isSigchildEnabled() + protected function isSigchildEnabled(): bool { if (null !== self::$sigchild) { return self::$sigchild; diff --git a/ProcessUtils.php b/ProcessUtils.php index d5bc4478..1d86ad00 100644 --- a/ProcessUtils.php +++ b/ProcessUtils.php @@ -39,7 +39,7 @@ private function __construct() * * @throws InvalidArgumentException In case the input is not valid */ - public static function validateInput(string $caller, mixed $input) + public static function validateInput(string $caller, mixed $input): mixed { if (null !== $input) { if (\is_resource($input)) { From ff98682d5355a5c8fb8a3d293689e7a8c1056aaf Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 16 Aug 2021 18:31:32 +0200 Subject: [PATCH 08/73] Run php-cs-fixer --- Process.php | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/Process.php b/Process.php index cf40fc89..d067284f 100644 --- a/Process.php +++ b/Process.php @@ -183,8 +183,6 @@ public function __construct(array $command, string $cwd = null, array $env = nul * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input * @param int|float|null $timeout The timeout in seconds or null to disable * - * @return static - * * @throws LogicException When proc_open is not installed */ public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, mixed $input = null, ?float $timeout = 60): static @@ -195,9 +193,6 @@ public static function fromShellCommandline(string $command, string $cwd = null, return $process; } - /** - * @return array - */ public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); @@ -554,8 +549,6 @@ public function enableOutput(): static /** * Returns true in case the output is disabled, false otherwise. - * - * @return bool */ public function isOutputDisabled(): bool { @@ -781,8 +774,6 @@ public function isSuccessful(): bool * * It always returns false on Windows. * - * @return bool - * * @throws LogicException In case the process is not terminated */ public function hasBeenSignaled(): bool @@ -797,8 +788,6 @@ public function hasBeenSignaled(): bool * * It is only meaningful if hasBeenSignaled() returns true. * - * @return int - * * @throws RuntimeException In case --enable-sigchild is activated * @throws LogicException In case the process is not terminated */ @@ -818,8 +807,6 @@ public function getTermSignal(): int * * It always returns false on Windows. * - * @return bool - * * @throws LogicException In case the process is not terminated */ public function hasBeenStopped(): bool @@ -834,8 +821,6 @@ public function hasBeenStopped(): bool * * It is only meaningful if hasBeenStopped() returns true. * - * @return int - * * @throws LogicException In case the process is not terminated */ public function getStopSignal(): int @@ -1085,8 +1070,6 @@ public function setPty(bool $bool): static /** * Returns PTY state. - * - * @return bool */ public function isPty(): bool { @@ -1271,8 +1254,6 @@ public static function isTtySupported(): bool /** * Returns whether PTY is supported on the current operating system. - * - * @return bool */ public static function isPtySupported(): bool { @@ -1364,8 +1345,6 @@ protected function updateStatus(bool $blocking) /** * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. - * - * @return bool */ protected function isSigchildEnabled(): bool { From 3ecb9b7dcc7b0095bc687c5ce3d35f02ab7f7ca5 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 24 Aug 2021 22:21:00 +0200 Subject: [PATCH 09/73] Add back `@return $this` annotations --- Process.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Process.php b/Process.php index 7ef708fa..cd1e920c 100644 --- a/Process.php +++ b/Process.php @@ -253,6 +253,8 @@ public function run(callable $callback = null, array $env = []): int * This is identical to run() except that an exception is thrown if the process * exits with a non-zero exit code. * + * @return $this + * * @throws ProcessFailedException if the process didn't terminate successfully * * @final From 6d4b8b30f502cf098fdeaa2a160d545a2c2d4621 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 5 Oct 2021 17:32:15 +0200 Subject: [PATCH 10/73] Add type to final/internal public/protected properties --- Pipes/AbstractPipes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipes/AbstractPipes.php b/Pipes/AbstractPipes.php index 1d37eb76..dca7ae7f 100644 --- a/Pipes/AbstractPipes.php +++ b/Pipes/AbstractPipes.php @@ -20,7 +20,7 @@ */ abstract class AbstractPipes implements PipesInterface { - public $pipes = []; + public array $pipes = []; private $inputBuffer = ''; private $input; From 31d7ad3935b96d67d56aa6961ca5c166403c5df4 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Mon, 13 Dec 2021 16:44:47 +0100 Subject: [PATCH 11/73] Make use of the nullsafe operator --- Tests/ProcessTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 4a20fe12..91ba7c4f 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1546,9 +1546,7 @@ private function getProcess($commandline, string $cwd = null, array $env = null, $process = new Process($commandline, $cwd, $env, $input, $timeout); } - if (self::$process) { - self::$process->stop(0); - } + self::$process?->stop(0); return self::$process = $process; } From d5aec60005880c4144520ff74ede0fb8638c9e25 Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Tue, 15 Feb 2022 16:42:18 +0100 Subject: [PATCH 12/73] Leverage the match expression --- Exception/ProcessTimedOutException.php | 15 +++++---------- Tests/NonStopableProcess.php | 16 +++++----------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/Exception/ProcessTimedOutException.php b/Exception/ProcessTimedOutException.php index 94391a45..b052d72c 100644 --- a/Exception/ProcessTimedOutException.php +++ b/Exception/ProcessTimedOutException.php @@ -55,15 +55,10 @@ public function isIdleTimeout() public function getExceededTimeout() { - switch ($this->timeoutType) { - case self::TYPE_GENERAL: - return $this->process->getTimeout(); - - case self::TYPE_IDLE: - return $this->process->getIdleTimeout(); - - default: - throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)); - } + return match ($this->timeoutType) { + self::TYPE_GENERAL => $this->process->getTimeout(), + self::TYPE_IDLE => $this->process->getIdleTimeout(), + default => throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)), + }; } } diff --git a/Tests/NonStopableProcess.php b/Tests/NonStopableProcess.php index c695f544..846718f0 100644 --- a/Tests/NonStopableProcess.php +++ b/Tests/NonStopableProcess.php @@ -18,17 +18,11 @@ */ function handleSignal($signal) { - switch ($signal) { - case \SIGTERM: - $name = 'SIGTERM'; - break; - case \SIGINT: - $name = 'SIGINT'; - break; - default: - $name = $signal.' (unknown)'; - break; - } + $name = match ($signal) { + \SIGTERM => 'SIGTERM', + \SIGINT => 'SIGINT', + default => $signal . ' (unknown)', + }; echo "signal $name\n"; } From e0d623c582ab4f9833398bc6068a5721593923f1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 9 Feb 2022 15:00:38 +0100 Subject: [PATCH 13/73] Bump minimum version of PHP to 8.1 --- Pipes/UnixPipes.php | 2 +- Process.php | 4 ++-- Tests/ProcessTest.php | 2 -- composer.json | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Pipes/UnixPipes.php b/Pipes/UnixPipes.php index 063aa6ad..09c82f35 100644 --- a/Pipes/UnixPipes.php +++ b/Pipes/UnixPipes.php @@ -109,7 +109,7 @@ public function readAndWrite(bool $blocking, bool $close = false): array unset($r[0]); // let's have a look if something changed in streams - set_error_handler([$this, 'handleError']); + set_error_handler($this->handleError(...)); if (($r || $w) && false === stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { restore_error_handler(); // if a system call has been interrupted, forget about it, let's try again diff --git a/Process.php b/Process.php index f26c28a6..5009f88e 100644 --- a/Process.php +++ b/Process.php @@ -308,7 +308,7 @@ public function start(callable $callback = null, array $env = []) $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)); + $commandline = implode(' ', array_map($this->escapeArgument(...), $commandline)); if ('\\' !== \DIRECTORY_SEPARATOR) { // exec is mandatory to deal with sending a signal to the process @@ -946,7 +946,7 @@ public function getLastOutputTime(): ?float */ public function getCommandLine(): string { - return \is_array($this->commandline) ? implode(' ', array_map([$this, 'escapeArgument'], $this->commandline)) : $this->commandline; + return \is_array($this->commandline) ? implode(' ', array_map($this->escapeArgument(...), $this->commandline)) : $this->commandline; } /** diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index fc4de7bc..25c88024 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -176,7 +176,6 @@ public function testAllOutputIsActuallyReadOnTermination() // Don't call Process::run nor Process::wait to avoid any read of pipes $h = new \ReflectionProperty($p, 'process'); - $h->setAccessible(true); $h = $h->getValue($p); $s = @proc_get_status($h); @@ -568,7 +567,6 @@ public function testExitCodeText() $process = $this->getProcess(''); $r = new \ReflectionObject($process); $p = $r->getProperty('exitcode'); - $p->setAccessible(true); $p->setValue($process, 2); $this->assertEquals('Misuse of shell builtins', $process->getExitCodeText()); diff --git a/composer.json b/composer.json index 9f1aa8cf..317c07e7 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": ">=8.0.2" + "php": ">=8.1" }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" }, From 318718453c2be58266f1a9e74063d13cb8dd4165 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 11 May 2022 14:12:29 +0200 Subject: [PATCH 14/73] Update sponsors of components v6.1 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8777de4a..a371d286 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The Process component executes commands in sub-processes. Sponsor ------- -The Process component for Symfony 5.4/6.0 is [backed][1] by [SensioLabs][2]. +The Process component for Symfony 6.1 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 From 18036fe5b3b446a408fc720a38b64f3f7c51263f Mon Sep 17 00:00:00 2001 From: Yurun Date: Mon, 18 Jul 2022 16:59:48 +0800 Subject: [PATCH 15/73] Refactor isTtySupported() --- Process.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Process.php b/Process.php index 972fe703..7df95fc4 100644 --- a/Process.php +++ b/Process.php @@ -1205,7 +1205,7 @@ public static function isTtySupported(): bool static $isTtySupported; if (null === $isTtySupported) { - $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes); + $isTtySupported = ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)); } return $isTtySupported; From 00db88f2b2c541c5c61e05056d9d18eaa11493a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Thu, 25 Aug 2022 16:59:21 +0200 Subject: [PATCH 16/73] [CS] Remove @inheritdoc PHPDoc --- PhpProcess.php | 6 ------ Pipes/AbstractPipes.php | 3 --- Pipes/UnixPipes.php | 15 --------------- Pipes/WindowsPipes.php | 18 ------------------ 4 files changed, 42 deletions(-) diff --git a/PhpProcess.php b/PhpProcess.php index cb749f9b..486bc1da 100644 --- a/PhpProcess.php +++ b/PhpProcess.php @@ -50,17 +50,11 @@ public function __construct(string $script, string $cwd = null, array $env = nul parent::__construct($php, $cwd, $env, $script, $timeout); } - /** - * {@inheritdoc} - */ public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, mixed $input = null, ?float $timeout = 60): static { throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } - /** - * {@inheritdoc} - */ public function start(callable $callback = null, array $env = []) { if (null === $this->getCommandLine()) { diff --git a/Pipes/AbstractPipes.php b/Pipes/AbstractPipes.php index bc3a7a68..51d3af0b 100644 --- a/Pipes/AbstractPipes.php +++ b/Pipes/AbstractPipes.php @@ -41,9 +41,6 @@ public function __construct(mixed $input) } } - /** - * {@inheritdoc} - */ public function close() { foreach ($this->pipes as $pipe) { diff --git a/Pipes/UnixPipes.php b/Pipes/UnixPipes.php index 09c82f35..aba0efce 100644 --- a/Pipes/UnixPipes.php +++ b/Pipes/UnixPipes.php @@ -50,9 +50,6 @@ public function __destruct() $this->close(); } - /** - * {@inheritdoc} - */ public function getDescriptors(): array { if (!$this->haveReadSupport) { @@ -88,17 +85,11 @@ public function getDescriptors(): array ]; } - /** - * {@inheritdoc} - */ public function getFiles(): array { return []; } - /** - * {@inheritdoc} - */ public function readAndWrite(bool $blocking, bool $close = false): array { $this->unblock(); @@ -145,17 +136,11 @@ public function readAndWrite(bool $blocking, bool $close = false): array return $read; } - /** - * {@inheritdoc} - */ public function haveReadSupport(): bool { return $this->haveReadSupport; } - /** - * {@inheritdoc} - */ public function areOpen(): bool { return (bool) $this->pipes; diff --git a/Pipes/WindowsPipes.php b/Pipes/WindowsPipes.php index e68ed951..9f4dedb2 100644 --- a/Pipes/WindowsPipes.php +++ b/Pipes/WindowsPipes.php @@ -103,9 +103,6 @@ public function __destruct() $this->close(); } - /** - * {@inheritdoc} - */ public function getDescriptors(): array { if (!$this->haveReadSupport) { @@ -128,17 +125,11 @@ public function getDescriptors(): array ]; } - /** - * {@inheritdoc} - */ public function getFiles(): array { return $this->files; } - /** - * {@inheritdoc} - */ public function readAndWrite(bool $blocking, bool $close = false): array { $this->unblock(); @@ -171,25 +162,16 @@ public function readAndWrite(bool $blocking, bool $close = false): array return $read; } - /** - * {@inheritdoc} - */ public function haveReadSupport(): bool { return $this->haveReadSupport; } - /** - * {@inheritdoc} - */ public function areOpen(): bool { return $this->pipes && $this->fileHandles; } - /** - * {@inheritdoc} - */ public function close() { parent::close(); From ba6e55359f8f755fe996c58a81e00eaa67a35877 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 1 Nov 2022 22:49:27 +0100 Subject: [PATCH 17/73] Use ??= more --- Process.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Process.php b/Process.php index 7df95fc4..bdeab7e7 100644 --- a/Process.php +++ b/Process.php @@ -1204,11 +1204,7 @@ public static function isTtySupported(): bool { static $isTtySupported; - if (null === $isTtySupported) { - $isTtySupported = ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)); - } - - return $isTtySupported; + return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)); } /** From 21b4d0c1c5074e04c436f4fe3f84403c58d67f46 Mon Sep 17 00:00:00 2001 From: tigitz Date: Sun, 1 Jan 2023 19:45:34 +0100 Subject: [PATCH 18/73] Leverage arrow function syntax for closure --- Process.php | 4 +--- Tests/ProcessTest.php | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Process.php b/Process.php index bdeab7e7..3c70be2a 100644 --- a/Process.php +++ b/Process.php @@ -1253,9 +1253,7 @@ private function getDescriptors(): array protected function buildCallback(callable $callback = null): \Closure { if ($this->outputDisabled) { - return function ($type, $data) use ($callback): bool { - return null !== $callback && $callback($type, $data); - }; + return fn ($type, $data): bool => null !== $callback && $callback($type, $data); } $out = self::OUT; diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 013bca9b..979d72a9 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -155,7 +155,7 @@ public function testWaitUntilCanReturnFalse() { $p = $this->getProcess('echo foo'); $p->start(); - $this->assertFalse($p->waitUntil(function () { return false; })); + $this->assertFalse($p->waitUntil(fn () => false)); } public function testAllOutputIsActuallyReadOnTermination() From 9d12538212a5fabb8caadda1952eb7a53843aec7 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 25 Jan 2023 16:30:46 +0100 Subject: [PATCH 19/73] [6.2] Remove mentions of v6.1 backers --- README.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/README.md b/README.md index a371d286..afce5e45 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,6 @@ Process Component The Process component executes commands in sub-processes. -Sponsor -------- - -The Process component for Symfony 6.1 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 --------- @@ -22,7 +11,3 @@ 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 From 01e6198dc55bf4862856e16490a91a9c860c1762 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Mon, 13 Feb 2023 00:00:11 +0100 Subject: [PATCH 20/73] Add missing PHPdoc return types --- Exception/ProcessTimedOutException.php | 6 ++++++ InputStream.php | 2 ++ 2 files changed, 8 insertions(+) diff --git a/Exception/ProcessTimedOutException.php b/Exception/ProcessTimedOutException.php index b052d72c..27ce7fd4 100644 --- a/Exception/ProcessTimedOutException.php +++ b/Exception/ProcessTimedOutException.php @@ -43,11 +43,17 @@ public function getProcess() return $this->process; } + /** + * @return bool + */ public function isGeneralTimeout() { return self::TYPE_GENERAL === $this->timeoutType; } + /** + * @return bool + */ public function isIdleTimeout() { return self::TYPE_IDLE === $this->timeoutType; diff --git a/InputStream.php b/InputStream.php index b8682bae..d7036608 100644 --- a/InputStream.php +++ b/InputStream.php @@ -62,6 +62,8 @@ public function close() /** * Tells whether the write buffer is closed or not. + * + * @return bool */ public function isClosed() { From b040da61c95c2672590dfa10c25c917665bed737 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Mon, 13 Feb 2023 00:00:27 +0100 Subject: [PATCH 21/73] Add PHP types to private methods and functions --- Process.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Process.php b/Process.php index 3c70be2a..82f89006 100644 --- a/Process.php +++ b/Process.php @@ -1567,7 +1567,7 @@ private function escapeArgument(?string $argument): string return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"'; } - private function replacePlaceholders(string $commandline, array $env) + private function replacePlaceholders(string $commandline, array $env): string { return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) { if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) { From ae2af87f40c3b49f4cfa9b0bca771bd09871cecd Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 12 Feb 2023 23:57:18 +0100 Subject: [PATCH 22/73] Add void return types --- ExecutableFinder.php | 4 ++++ InputStream.php | 6 ++++++ PhpProcess.php | 3 +++ Pipes/AbstractPipes.php | 6 +++--- Pipes/PipesInterface.php | 2 +- Pipes/WindowsPipes.php | 2 +- 6 files changed, 18 insertions(+), 5 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index d9d11102..e3387dfe 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -23,6 +23,8 @@ class ExecutableFinder /** * Replaces default suffixes of executable. + * + * @return void */ public function setSuffixes(array $suffixes) { @@ -31,6 +33,8 @@ public function setSuffixes(array $suffixes) /** * Adds new possible suffix to check for executable. + * + * @return void */ public function addSuffix(string $suffix) { diff --git a/InputStream.php b/InputStream.php index d7036608..25f574f7 100644 --- a/InputStream.php +++ b/InputStream.php @@ -29,6 +29,8 @@ class InputStream implements \IteratorAggregate /** * Sets a callback that is called when the write buffer becomes empty. + * + * @return void */ public function onEmpty(callable $onEmpty = null) { @@ -40,6 +42,8 @@ public function onEmpty(callable $onEmpty = null) * * @param resource|string|int|float|bool|\Traversable|null $input The input to append as scalar, * stream resource or \Traversable + * + * @return void */ public function write(mixed $input) { @@ -54,6 +58,8 @@ public function write(mixed $input) /** * Closes the write buffer. + * + * @return void */ public function close() { diff --git a/PhpProcess.php b/PhpProcess.php index 486bc1da..ef54a3d2 100644 --- a/PhpProcess.php +++ b/PhpProcess.php @@ -55,6 +55,9 @@ public static function fromShellCommandline(string $command, string $cwd = null, throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } + /** + * @return void + */ public function start(callable $callback = null, array $env = []) { if (null === $this->getCommandLine()) { diff --git a/Pipes/AbstractPipes.php b/Pipes/AbstractPipes.php index 51d3af0b..ba3a97a3 100644 --- a/Pipes/AbstractPipes.php +++ b/Pipes/AbstractPipes.php @@ -41,7 +41,7 @@ public function __construct(mixed $input) } } - public function close() + public function close(): void { foreach ($this->pipes as $pipe) { if (\is_resource($pipe)) { @@ -66,7 +66,7 @@ protected function hasSystemCallBeenInterrupted(): bool /** * Unblocks streams. */ - protected function unblock() + protected function unblock(): void { if (!$this->blocked) { return; @@ -170,7 +170,7 @@ protected function write(): ?array /** * @internal */ - public function handleError(int $type, string $msg) + public function handleError(int $type, string $msg): void { $this->lastError = $msg; } diff --git a/Pipes/PipesInterface.php b/Pipes/PipesInterface.php index 50eb5c47..967f8de7 100644 --- a/Pipes/PipesInterface.php +++ b/Pipes/PipesInterface.php @@ -57,5 +57,5 @@ public function haveReadSupport(): bool; /** * Closes file handles and pipes. */ - public function close(); + public function close(): void; } diff --git a/Pipes/WindowsPipes.php b/Pipes/WindowsPipes.php index 9f4dedb2..0d6ab12d 100644 --- a/Pipes/WindowsPipes.php +++ b/Pipes/WindowsPipes.php @@ -172,7 +172,7 @@ public function areOpen(): bool return $this->pipes && $this->fileHandles; } - public function close() + public function close(): void { parent::close(); foreach ($this->fileHandles as $type => $handle) { From 4d13cb8981e6509d1adfcf9d2019a86b631d2e2c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 21 Mar 2023 22:03:43 +0100 Subject: [PATCH 23/73] Replace "use-by-ref" by static vars when possible in closures --- Process.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Process.php b/Process.php index 909a2476..7defb879 100644 --- a/Process.php +++ b/Process.php @@ -1480,8 +1480,6 @@ private function doSignal(int $signal, bool $throwException): bool private function prepareWindowsCommandLine(string $cmd, array &$env): string { $uid = uniqid('', true); - $varCount = 0; - $varCache = []; $cmd = preg_replace_callback( '/"(?:( [^"%!^]*+ @@ -1490,7 +1488,9 @@ private function prepareWindowsCommandLine(string $cmd, array &$env): string [^"%!^]*+ )++ ) | [^"]*+ )"/x', - function ($m) use (&$env, &$varCache, &$varCount, $uid) { + function ($m) use (&$env, $uid) { + static $varCount = 0; + static $varCache = []; if (!isset($m[1])) { return $m[0]; } From 32ac96fb0c7ae1194c9e660701e73d6868469bba Mon Sep 17 00:00:00 2001 From: Yassine Guedidi Date: Sun, 2 Apr 2023 02:55:08 +0200 Subject: [PATCH 24/73] Apply no_null_property_initialization PHP-CS-Fixer rule --- InputStream.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InputStream.php b/InputStream.php index 25f574f7..086f5a9e 100644 --- a/InputStream.php +++ b/InputStream.php @@ -23,7 +23,7 @@ class InputStream implements \IteratorAggregate { /** @var callable|null */ - private $onEmpty = null; + private $onEmpty; private $input = []; private $open = true; From 39cbf15e9e1c1742a556de19fbd5191944c97668 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 22 Apr 2023 23:52:28 +0200 Subject: [PATCH 25/73] Add remaining missing return types to safe methods --- Process.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Process.php b/Process.php index 7defb879..6a38e965 100644 --- a/Process.php +++ b/Process.php @@ -912,7 +912,7 @@ public function stop(float $timeout = 10, int $signal = null): ?int * * @internal */ - public function addOutput(string $line) + public function addOutput(string $line): void { $this->lastOutputTime = microtime(true); @@ -926,7 +926,7 @@ public function addOutput(string $line) * * @internal */ - public function addErrorOutput(string $line) + public function addErrorOutput(string $line): void { $this->lastOutputTime = microtime(true); From 89ac295dc344cc0cd802dd303cb2598e184e7014 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Fri, 28 Apr 2023 14:21:20 +0200 Subject: [PATCH 26/73] Add missing return types --- Exception/ProcessFailedException.php | 3 +++ Exception/ProcessTimedOutException.php | 5 ++++- Process.php | 18 +++++++++++++----- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Exception/ProcessFailedException.php b/Exception/ProcessFailedException.php index 328acfde..cf006dae 100644 --- a/Exception/ProcessFailedException.php +++ b/Exception/ProcessFailedException.php @@ -47,6 +47,9 @@ public function __construct(Process $process) $this->process = $process; } + /** + * @return Process + */ public function getProcess() { return $this->process; diff --git a/Exception/ProcessTimedOutException.php b/Exception/ProcessTimedOutException.php index 27ce7fd4..e507ca30 100644 --- a/Exception/ProcessTimedOutException.php +++ b/Exception/ProcessTimedOutException.php @@ -38,6 +38,9 @@ public function __construct(Process $process, int $timeoutType) )); } + /** + * @return Process + */ public function getProcess() { return $this->process; @@ -59,7 +62,7 @@ public function isIdleTimeout() return self::TYPE_IDLE === $this->timeoutType; } - public function getExceededTimeout() + public function getExceededTimeout(): ?float { return match ($this->timeoutType) { self::TYPE_GENERAL => $this->process->getTimeout(), diff --git a/Process.php b/Process.php index 6a38e965..2f4b5a24 100644 --- a/Process.php +++ b/Process.php @@ -285,6 +285,8 @@ public function mustRun(callable $callback = null, array $env = []): static * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * + * @return void + * * @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 @@ -1140,6 +1142,8 @@ public function setInput(mixed $input): static * In case you run a background process (with the start method), you should * trigger this method regularly to ensure the process timeout * + * @return void + * * @throws ProcessTimedOutException In case the timeout was reached */ public function checkTimeout() @@ -1180,6 +1184,8 @@ public function getStartTime(): float * * Enabling the "create_new_console" option allows a subprocess to continue * to run after the main process exited, on both Windows and *nix + * + * @return void */ public function setOptions(array $options) { @@ -1275,6 +1281,8 @@ protected function buildCallback(callable $callback = null): \Closure * Updates the status of the process, reads pipes. * * @param bool $blocking Whether to use a blocking read call + * + * @return void */ protected function updateStatus(bool $blocking) { @@ -1323,7 +1331,7 @@ protected function isSigchildEnabled(): bool * * @throws LogicException in case output has been disabled or process is not started */ - private function readPipesForOutput(string $caller, bool $blocking = false) + private function readPipesForOutput(string $caller, bool $blocking = false): void { if ($this->outputDisabled) { throw new LogicException('Output has been disabled.'); @@ -1358,7 +1366,7 @@ private function validateTimeout(?float $timeout): ?float * @param bool $blocking Whether to use blocking calls or not * @param bool $close Whether to close file handles or not */ - private function readPipes(bool $blocking, bool $close) + private function readPipes(bool $blocking, bool $close): void { $result = $this->processPipes->readAndWrite($blocking, $close); @@ -1407,7 +1415,7 @@ private function close(): int /** * Resets data related to the latest run of the process. */ - private function resetProcessData() + private function resetProcessData(): void { $this->starttime = null; $this->callback = null; @@ -1528,7 +1536,7 @@ function ($m) use (&$env, $uid) { * * @throws LogicException if the process has not run */ - private function requireProcessIsStarted(string $functionName) + private function requireProcessIsStarted(string $functionName): void { if (!$this->isStarted()) { throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName)); @@ -1540,7 +1548,7 @@ private function requireProcessIsStarted(string $functionName) * * @throws LogicException if the process is not yet terminated */ - private function requireProcessIsTerminated(string $functionName) + private function requireProcessIsTerminated(string $functionName): void { if (!$this->isTerminated()) { throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName)); From ed6595d58305b67686251b47713e40d48abb47e8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 23 May 2023 17:24:39 +0200 Subject: [PATCH 27/73] [7.0] Bump to PHP 8.2 minimum --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 317c07e7..dda5575e 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": ">=8.1" + "php": ">=8.2" }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" }, From 4525b60911c144a1af23def8fc06d9f6e3b27be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Pr=C3=A9vot?= Date: Sat, 1 Jul 2023 20:52:48 +0200 Subject: [PATCH 28/73] Fix executable bit --- Tests/ErrorProcessInitiator.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 Tests/ErrorProcessInitiator.php diff --git a/Tests/ErrorProcessInitiator.php b/Tests/ErrorProcessInitiator.php old mode 100755 new mode 100644 From bf13cb34243603dc11d21f283e34bdf1bbe60f44 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 1 Jul 2023 14:03:11 +0200 Subject: [PATCH 29/73] Add missing return types to magic methods --- Pipes/UnixPipes.php | 2 +- Pipes/WindowsPipes.php | 2 +- Process.php | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Pipes/UnixPipes.php b/Pipes/UnixPipes.php index aba0efce..d381d57b 100644 --- a/Pipes/UnixPipes.php +++ b/Pipes/UnixPipes.php @@ -40,7 +40,7 @@ public function __sleep(): array throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - public function __wakeup() + public function __wakeup(): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } diff --git a/Pipes/WindowsPipes.php b/Pipes/WindowsPipes.php index 0d6ab12d..793ccb15 100644 --- a/Pipes/WindowsPipes.php +++ b/Pipes/WindowsPipes.php @@ -93,7 +93,7 @@ public function __sleep(): array throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - public function __wakeup() + public function __wakeup(): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } diff --git a/Process.php b/Process.php index 40dbd416..f330d405 100644 --- a/Process.php +++ b/Process.php @@ -200,6 +200,9 @@ public function __sleep(): array throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } + /** + * @return void + */ public function __wakeup() { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); From 2bea96bf209885d1a6d5908967ac1a875f2e3141 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 2 Jul 2023 23:52:21 +0200 Subject: [PATCH 30/73] [Components] Convert to native return types --- Exception/ProcessFailedException.php | 5 +---- Exception/ProcessTimedOutException.php | 15 +++------------ ExecutableFinder.php | 8 ++------ InputStream.php | 16 ++++------------ PhpProcess.php | 5 +---- Process.php | 21 +++++---------------- 6 files changed, 16 insertions(+), 54 deletions(-) diff --git a/Exception/ProcessFailedException.php b/Exception/ProcessFailedException.php index cf006dae..ddb89559 100644 --- a/Exception/ProcessFailedException.php +++ b/Exception/ProcessFailedException.php @@ -47,10 +47,7 @@ public function __construct(Process $process) $this->process = $process; } - /** - * @return Process - */ - public function getProcess() + public function getProcess(): Process { return $this->process; } diff --git a/Exception/ProcessTimedOutException.php b/Exception/ProcessTimedOutException.php index e507ca30..bf2775a1 100644 --- a/Exception/ProcessTimedOutException.php +++ b/Exception/ProcessTimedOutException.php @@ -38,26 +38,17 @@ public function __construct(Process $process, int $timeoutType) )); } - /** - * @return Process - */ - public function getProcess() + public function getProcess(): Process { return $this->process; } - /** - * @return bool - */ - public function isGeneralTimeout() + public function isGeneralTimeout(): bool { return self::TYPE_GENERAL === $this->timeoutType; } - /** - * @return bool - */ - public function isIdleTimeout() + public function isIdleTimeout(): bool { return self::TYPE_IDLE === $this->timeoutType; } diff --git a/ExecutableFinder.php b/ExecutableFinder.php index e3387dfe..f127054b 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -23,20 +23,16 @@ class ExecutableFinder /** * Replaces default suffixes of executable. - * - * @return void */ - public function setSuffixes(array $suffixes) + public function setSuffixes(array $suffixes): void { $this->suffixes = $suffixes; } /** * Adds new possible suffix to check for executable. - * - * @return void */ - public function addSuffix(string $suffix) + public function addSuffix(string $suffix): void { $this->suffixes[] = $suffix; } diff --git a/InputStream.php b/InputStream.php index 086f5a9e..74618d4d 100644 --- a/InputStream.php +++ b/InputStream.php @@ -29,10 +29,8 @@ class InputStream implements \IteratorAggregate /** * Sets a callback that is called when the write buffer becomes empty. - * - * @return void */ - public function onEmpty(callable $onEmpty = null) + public function onEmpty(callable $onEmpty = null): void { $this->onEmpty = $onEmpty; } @@ -42,10 +40,8 @@ public function onEmpty(callable $onEmpty = null) * * @param resource|string|int|float|bool|\Traversable|null $input The input to append as scalar, * stream resource or \Traversable - * - * @return void */ - public function write(mixed $input) + public function write(mixed $input): void { if (null === $input) { return; @@ -58,20 +54,16 @@ public function write(mixed $input) /** * Closes the write buffer. - * - * @return void */ - public function close() + public function close(): void { $this->open = false; } /** * Tells whether the write buffer is closed or not. - * - * @return bool */ - public function isClosed() + public function isClosed(): bool { return !$this->open; } diff --git a/PhpProcess.php b/PhpProcess.php index ef54a3d2..0d31e26f 100644 --- a/PhpProcess.php +++ b/PhpProcess.php @@ -55,10 +55,7 @@ public static function fromShellCommandline(string $command, string $cwd = null, throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } - /** - * @return void - */ - public function start(callable $callback = null, array $env = []) + public function start(callable $callback = null, array $env = []): void { if (null === $this->getCommandLine()) { throw new RuntimeException('Unable to find the PHP executable.'); diff --git a/Process.php b/Process.php index f330d405..944931fd 100644 --- a/Process.php +++ b/Process.php @@ -200,10 +200,7 @@ public function __sleep(): array throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - /** - * @return void - */ - public function __wakeup() + public function __wakeup(): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } @@ -288,13 +285,11 @@ public function mustRun(callable $callback = null, array $env = []): static * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * - * @return void - * * @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, array $env = []) + public function start(callable $callback = null, array $env = []): void { if ($this->isRunning()) { throw new RuntimeException('Process is already running.'); @@ -1145,11 +1140,9 @@ public function setInput(mixed $input): static * In case you run a background process (with the start method), you should * trigger this method regularly to ensure the process timeout * - * @return void - * * @throws ProcessTimedOutException In case the timeout was reached */ - public function checkTimeout() + public function checkTimeout(): void { if (self::STATUS_STARTED !== $this->status) { return; @@ -1187,10 +1180,8 @@ public function getStartTime(): float * * Enabling the "create_new_console" option allows a subprocess to continue * to run after the main process exited, on both Windows and *nix - * - * @return void */ - public function setOptions(array $options) + public function setOptions(array $options): void { if ($this->isRunning()) { throw new RuntimeException('Setting options while the process is running is not possible.'); @@ -1284,10 +1275,8 @@ protected function buildCallback(callable $callback = null): \Closure * Updates the status of the process, reads pipes. * * @param bool $blocking Whether to use a blocking read call - * - * @return void */ - protected function updateStatus(bool $blocking) + protected function updateStatus(bool $blocking): void { if (self::STATUS_STARTED !== $this->status) { return; From 1132f6e0988f4872da081e7df2105729981f6ee8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 21 Jul 2023 15:28:24 +0200 Subject: [PATCH 31/73] Use typed properties in tests as much as possible --- Tests/ExecutableFinderTest.php | 2 +- Tests/ProcessTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 5c63cf0f..155c5ee2 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -19,7 +19,7 @@ */ class ExecutableFinderTest extends TestCase { - private $path; + private string|false $path = false; protected function tearDown(): void { diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index d326fe8e..cca2b529 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -28,9 +28,9 @@ */ class ProcessTest extends TestCase { - private static $phpBin; - private static $process; - private static $sigchild; + private static string $phpBin; + private static ?Process $process = null; + private static bool $sigchild; public static function setUpBeforeClass(): void { From 8f38ed0bd6164cad8f0a3b40df0dc65bd070b66b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 21 Jul 2023 18:41:43 +0200 Subject: [PATCH 32/73] Add types to private and internal properties --- Exception/ProcessFailedException.php | 2 +- Exception/ProcessSignaledException.php | 2 +- Exception/ProcessTimedOutException.php | 4 +- ExecutableFinder.php | 2 +- InputStream.php | 9 ++--- PhpExecutableFinder.php | 2 +- Pipes/AbstractPipes.php | 6 +-- Pipes/UnixPipes.php | 6 +-- Pipes/WindowsPipes.php | 10 ++--- Process.php | 54 ++++++++++++-------------- 10 files changed, 46 insertions(+), 51 deletions(-) diff --git a/Exception/ProcessFailedException.php b/Exception/ProcessFailedException.php index cf006dae..19b40570 100644 --- a/Exception/ProcessFailedException.php +++ b/Exception/ProcessFailedException.php @@ -20,7 +20,7 @@ */ class ProcessFailedException extends RuntimeException { - private $process; + private Process $process; public function __construct(Process $process) { diff --git a/Exception/ProcessSignaledException.php b/Exception/ProcessSignaledException.php index d4d32275..0fed8ac3 100644 --- a/Exception/ProcessSignaledException.php +++ b/Exception/ProcessSignaledException.php @@ -20,7 +20,7 @@ */ final class ProcessSignaledException extends RuntimeException { - private $process; + private Process $process; public function __construct(Process $process) { diff --git a/Exception/ProcessTimedOutException.php b/Exception/ProcessTimedOutException.php index e507ca30..1cecdae7 100644 --- a/Exception/ProcessTimedOutException.php +++ b/Exception/ProcessTimedOutException.php @@ -23,8 +23,8 @@ class ProcessTimedOutException extends RuntimeException public const TYPE_GENERAL = 1; public const TYPE_IDLE = 2; - private $process; - private $timeoutType; + private Process $process; + private int $timeoutType; public function __construct(Process $process, int $timeoutType) { diff --git a/ExecutableFinder.php b/ExecutableFinder.php index e3387dfe..b31f7530 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -19,7 +19,7 @@ */ class ExecutableFinder { - private $suffixes = ['.exe', '.bat', '.cmd', '.com']; + private array $suffixes = ['.exe', '.bat', '.cmd', '.com']; /** * Replaces default suffixes of executable. diff --git a/InputStream.php b/InputStream.php index 086f5a9e..cf277325 100644 --- a/InputStream.php +++ b/InputStream.php @@ -22,10 +22,9 @@ */ class InputStream implements \IteratorAggregate { - /** @var callable|null */ - private $onEmpty; - private $input = []; - private $open = true; + private ?\Closure $onEmpty = null; + private array $input = []; + private bool $open = true; /** * Sets a callback that is called when the write buffer becomes empty. @@ -34,7 +33,7 @@ class InputStream implements \IteratorAggregate */ public function onEmpty(callable $onEmpty = null) { - $this->onEmpty = $onEmpty; + $this->onEmpty = null !== $onEmpty ? $onEmpty(...) : null; } /** diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index 9ab8ac23..09f15008 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -19,7 +19,7 @@ */ class PhpExecutableFinder { - private $executableFinder; + private ExecutableFinder $executableFinder; public function __construct() { diff --git a/Pipes/AbstractPipes.php b/Pipes/AbstractPipes.php index ba3a97a3..c54d4b43 100644 --- a/Pipes/AbstractPipes.php +++ b/Pipes/AbstractPipes.php @@ -22,10 +22,10 @@ abstract class AbstractPipes implements PipesInterface { public array $pipes = []; - private $inputBuffer = ''; + private string $inputBuffer = ''; private $input; - private $blocked = true; - private $lastError; + private bool $blocked = true; + private ?string $lastError = null; /** * @param resource|string|int|float|bool|\Iterator|null $input diff --git a/Pipes/UnixPipes.php b/Pipes/UnixPipes.php index d381d57b..7bd0db0e 100644 --- a/Pipes/UnixPipes.php +++ b/Pipes/UnixPipes.php @@ -22,9 +22,9 @@ */ class UnixPipes extends AbstractPipes { - private $ttyMode; - private $ptyMode; - private $haveReadSupport; + private ?bool $ttyMode; + private bool $ptyMode; + private bool $haveReadSupport; public function __construct(?bool $ttyMode, bool $ptyMode, mixed $input, bool $haveReadSupport) { diff --git a/Pipes/WindowsPipes.php b/Pipes/WindowsPipes.php index 793ccb15..637c8f38 100644 --- a/Pipes/WindowsPipes.php +++ b/Pipes/WindowsPipes.php @@ -26,14 +26,14 @@ */ class WindowsPipes extends AbstractPipes { - private $files = []; - private $fileHandles = []; - private $lockHandles = []; - private $readBytes = [ + private array $files = []; + private array $fileHandles = []; + private array $lockHandles = []; + private array $readBytes = [ Process::STDOUT => 0, Process::STDERR => 0, ]; - private $haveReadSupport; + private bool $haveReadSupport; public function __construct(mixed $input, bool $haveReadSupport) { diff --git a/Process.php b/Process.php index f330d405..1d30d27b 100644 --- a/Process.php +++ b/Process.php @@ -17,7 +17,6 @@ use Symfony\Component\Process\Exception\ProcessSignaledException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; -use Symfony\Component\Process\Pipes\PipesInterface; use Symfony\Component\Process\Pipes\UnixPipes; use Symfony\Component\Process\Pipes\WindowsPipes; @@ -51,37 +50,35 @@ class Process implements \IteratorAggregate public const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating public const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating - private $callback; - private $hasCallback = false; - private $commandline; - private $cwd; - private $env = []; + private ?\Closure $callback = null; + private array|string $commandline; + private ?string $cwd; + private array $env = []; private $input; - private $starttime; - private $lastOutputTime; - private $timeout; - private $idleTimeout; - private $exitcode; - private $fallbackStatus = []; - private $processInformation; - private $outputDisabled = false; + private ?float $starttime = null; + private ?float $lastOutputTime = null; + private ?float $timeout = null; + private ?float $idleTimeout = null; + private ?int $exitcode = null; + private array $fallbackStatus = []; + private array $processInformation; + private bool $outputDisabled = false; private $stdout; private $stderr; private $process; - private $status = self::STATUS_READY; - private $incrementalOutputOffset = 0; - private $incrementalErrorOutputOffset = 0; - private $tty = false; - private $pty; - private $options = ['suppress_errors' => true, 'bypass_shell' => true]; + private string $status = self::STATUS_READY; + private int $incrementalOutputOffset = 0; + private int $incrementalErrorOutputOffset = 0; + private bool $tty = false; + private bool $pty; + private array $options = ['suppress_errors' => true, 'bypass_shell' => true]; - private $useFileHandles = false; - /** @var PipesInterface */ - private $processPipes; + private bool $useFileHandles; + private WindowsPipes|UnixPipes $processPipes; - private $latestSignal; + private ?int $latestSignal = null; - private static $sigchild; + private static ?bool $sigchild = null; /** * Exit codes translation table. @@ -303,7 +300,6 @@ public function start(callable $callback = null, array $env = []) $this->resetProcessData(); $this->starttime = $this->lastOutputTime = microtime(true); $this->callback = $this->buildCallback($callback); - $this->hasCallback = null !== $callback; $descriptors = $this->getDescriptors(); if ($this->env) { @@ -1245,9 +1241,9 @@ private function getDescriptors(): array $this->input->rewind(); } if ('\\' === \DIRECTORY_SEPARATOR) { - $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback); + $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->callback); } else { - $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback); + $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->callback); } return $this->processPipes->getDescriptors(); @@ -1424,7 +1420,7 @@ private function resetProcessData(): void $this->callback = null; $this->exitcode = null; $this->fallbackStatus = []; - $this->processInformation = null; + $this->processInformation = []; $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); $this->process = null; From 0d38e48b5e63d84fa20c0b2fb6ec201a9a2dd2a9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 27 Jul 2023 11:02:28 +0200 Subject: [PATCH 33/73] Ensure all properties have a type --- Pipes/AbstractPipes.php | 7 +++---- Process.php | 11 ++++++++--- ProcessUtils.php | 3 --- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Pipes/AbstractPipes.php b/Pipes/AbstractPipes.php index c54d4b43..cbbb7277 100644 --- a/Pipes/AbstractPipes.php +++ b/Pipes/AbstractPipes.php @@ -23,19 +23,18 @@ abstract class AbstractPipes implements PipesInterface public array $pipes = []; private string $inputBuffer = ''; + /** @var resource|string|\Iterator */ private $input; private bool $blocked = true; private ?string $lastError = null; /** - * @param resource|string|int|float|bool|\Iterator|null $input + * @param resource|string|\Iterator $input */ - public function __construct(mixed $input) + public function __construct($input) { if (\is_resource($input) || $input instanceof \Iterator) { $this->input = $input; - } elseif (\is_string($input)) { - $this->inputBuffer = $input; } else { $this->inputBuffer = (string) $input; } diff --git a/Process.php b/Process.php index 1d30d27b..8e520d04 100644 --- a/Process.php +++ b/Process.php @@ -54,6 +54,7 @@ class Process implements \IteratorAggregate private array|string $commandline; private ?string $cwd; private array $env = []; + /** @var resource|string|\Iterator|null */ private $input; private ?float $starttime = null; private ?float $lastOutputTime = null; @@ -63,8 +64,11 @@ class Process implements \IteratorAggregate private array $fallbackStatus = []; private array $processInformation; private bool $outputDisabled = false; + /** @var resource */ private $stdout; + /** @var resource */ private $stderr; + /** @var resource|null */ private $process; private string $status = self::STATUS_READY; private int $incrementalOutputOffset = 0; @@ -345,11 +349,12 @@ public function start(callable $callback = null, array $env = []) throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd)); } - $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); - if (!\is_resource($this->process)) { + if (!\is_resource($process)) { throw new RuntimeException('Unable to launch a new process.'); } + $this->process = $process; $this->status = self::STATUS_STARTED; if (isset($descriptors[3])) { @@ -1118,7 +1123,7 @@ public function getInput() * * This content will be passed to the underlying process standard input. * - * @param string|int|float|bool|resource|\Traversable|null $input The content + * @param string|resource|\Traversable|self|null $input The content * * @return $this * diff --git a/ProcessUtils.php b/ProcessUtils.php index 744399d9..092c5ccf 100644 --- a/ProcessUtils.php +++ b/ProcessUtils.php @@ -43,9 +43,6 @@ public static function validateInput(string $caller, mixed $input): mixed if (\is_resource($input)) { return $input; } - if (\is_string($input)) { - return $input; - } if (\is_scalar($input)) { return (string) $input; } From 8d97ce8ed84704534f1aceea05ae302b13ec53a2 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 21 Jul 2023 15:36:26 +0200 Subject: [PATCH 34/73] Add types to public and protected properties --- Process.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Process.php b/Process.php index 5a5cfe9f..b5705a8e 100644 --- a/Process.php +++ b/Process.php @@ -89,7 +89,7 @@ class Process implements \IteratorAggregate * * User-defined errors must use exit codes in the 64-113 range. */ - public static $exitCodes = [ + public static array $exitCodes = [ 0 => 'OK', 1 => 'General error', 2 => 'Misuse of shell builtins', From 1276cfea910ffa53ae4057dc77ce4101e73f5329 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sat, 25 Mar 2023 13:40:25 -0400 Subject: [PATCH 35/73] [Messenger][Process] add `RunProcessMessage` and `RunProcessMessageHandler` --- CHANGELOG.md | 5 +++ Exception/RunProcessFailedException.php | 25 +++++++++++ Messenger/RunProcessContext.php | 33 ++++++++++++++ Messenger/RunProcessMessage.php | 32 ++++++++++++++ Messenger/RunProcessMessageHandler.php | 33 ++++++++++++++ .../RunProcessMessageHandlerTest.php | 43 +++++++++++++++++++ 6 files changed, 171 insertions(+) create mode 100644 Exception/RunProcessFailedException.php create mode 100644 Messenger/RunProcessContext.php create mode 100644 Messenger/RunProcessMessage.php create mode 100644 Messenger/RunProcessMessageHandler.php create mode 100644 Tests/Messenger/RunProcessMessageHandlerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 31b9ee6a..78241ab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +6.4 +--- + + * Add `RunProcessMessage` and `RunProcessMessageHandler` + 5.2.0 ----- diff --git a/Exception/RunProcessFailedException.php b/Exception/RunProcessFailedException.php new file mode 100644 index 00000000..e7219d35 --- /dev/null +++ b/Exception/RunProcessFailedException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Messenger\RunProcessContext; + +/** + * @author Kevin Bond + */ +final class RunProcessFailedException extends RuntimeException +{ + public function __construct(ProcessFailedException $exception, public readonly RunProcessContext $context) + { + parent::__construct($exception->getMessage(), $exception->getCode()); + } +} diff --git a/Messenger/RunProcessContext.php b/Messenger/RunProcessContext.php new file mode 100644 index 00000000..3c7da369 --- /dev/null +++ b/Messenger/RunProcessContext.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Messenger; + +use Symfony\Component\Process\Process; + +/** + * @author Kevin Bond + */ +final class RunProcessContext extends RunProcessMessage +{ + public readonly ?int $exitCode; + public readonly ?string $output; + public readonly ?string $errorOutput; + + public function __construct(RunProcessMessage $message, Process $process) + { + parent::__construct($message->command, $message->cwd, $message->env, $message->input, $message->timeout); + + $this->exitCode = $process->getExitCode(); + $this->output = $process->isOutputDisabled() ? null : $process->getOutput(); + $this->errorOutput = $process->isOutputDisabled() ? null : $process->getErrorOutput(); + } +} diff --git a/Messenger/RunProcessMessage.php b/Messenger/RunProcessMessage.php new file mode 100644 index 00000000..1d87e9c4 --- /dev/null +++ b/Messenger/RunProcessMessage.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Messenger; + +/** + * @author Kevin Bond + */ +class RunProcessMessage implements \Stringable +{ + public function __construct( + public readonly array $command, + public readonly ?string $cwd = null, + public readonly ?array $env = null, + public readonly mixed $input = null, + public readonly ?float $timeout = 60.0, + ) { + } + + public function __toString(): string + { + return \implode(' ', $this->command); + } +} diff --git a/Messenger/RunProcessMessageHandler.php b/Messenger/RunProcessMessageHandler.php new file mode 100644 index 00000000..41c1934c --- /dev/null +++ b/Messenger/RunProcessMessageHandler.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Messenger; + +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Exception\RunProcessFailedException; +use Symfony\Component\Process\Process; + +/** + * @author Kevin Bond + */ +final class RunProcessMessageHandler +{ + public function __invoke(RunProcessMessage $message): RunProcessContext + { + $process = new Process($message->command, $message->cwd, $message->env, $message->input, $message->timeout); + + try { + return new RunProcessContext($message, $process->mustRun()); + } catch (ProcessFailedException $e) { + throw new RunProcessFailedException($e, new RunProcessContext($message, $e->getProcess())); + } + } +} diff --git a/Tests/Messenger/RunProcessMessageHandlerTest.php b/Tests/Messenger/RunProcessMessageHandlerTest.php new file mode 100644 index 00000000..10ed9bb2 --- /dev/null +++ b/Tests/Messenger/RunProcessMessageHandlerTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests\Messenger; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Exception\RunProcessFailedException; +use Symfony\Component\Process\Messenger\RunProcessMessage; +use Symfony\Component\Process\Messenger\RunProcessMessageHandler; + +class RunProcessMessageHandlerTest extends TestCase +{ + public function testRunSuccessfulProcess() + { + $context = (new RunProcessMessageHandler())(new RunProcessMessage(['ls'], cwd: __DIR__)); + + $this->assertSame(['ls'], $context->command); + $this->assertSame(0, $context->exitCode); + $this->assertStringContainsString(basename(__FILE__), $context->output); + } + + public function testRunFailedProcess() + { + try { + (new RunProcessMessageHandler())(new RunProcessMessage(['invalid'])); + } catch (RunProcessFailedException $e) { + $this->assertSame(['invalid'], $e->context->command); + $this->assertSame(127, $e->context->exitCode); + + return; + } + + $this->fail('Exception not thrown'); + } +} From 1f07ae60a23f12edec74a58d91c509f73ce0c00e Mon Sep 17 00:00:00 2001 From: Jan Walther Date: Tue, 1 Aug 2023 16:37:55 +0200 Subject: [PATCH 36/73] [Process] Support finding executables independently of open_basedir --- CHANGELOG.md | 1 + ExecutableFinder.php | 32 +++++++++++++------------------- PhpExecutableFinder.php | 2 +- Tests/ExecutableFinderTest.php | 24 ++++++------------------ 4 files changed, 21 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78241ab2..d6ec2032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add `RunProcessMessage` and `RunProcessMessageHandler` + * Support using `Process::findExecutable()` independently of `open_basedir` 5.2.0 ----- diff --git a/ExecutableFinder.php b/ExecutableFinder.php index b31f7530..3681e356 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -50,25 +50,10 @@ public function addSuffix(string $suffix) */ public function find(string $name, string $default = null, array $extraDirs = []): ?string { - 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 - ); - } + $dirs = array_merge( + explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), + $extraDirs + ); $suffixes = ['']; if ('\\' === \DIRECTORY_SEPARATOR) { @@ -80,9 +65,18 @@ public function find(string $name, string $default = null, array $extraDirs = [] 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; + } } } + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v'; + if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && is_executable($executablePath)) { + return $executablePath; + } + return $default; } } diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index 09f15008..ae053641 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -34,7 +34,7 @@ public function find(bool $includeArgs = true): string|false 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 (\function_exists('exec') && $php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { if (!is_executable($php)) { return false; } diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 155c5ee2..54e740ec 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -19,20 +19,9 @@ */ class ExecutableFinderTest extends TestCase { - private string|false $path = false; - 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() @@ -41,7 +30,7 @@ public function testFind() $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()); @@ -57,7 +46,7 @@ public function testFindWithDefault() $expected = 'defaultValue'; - $this->setPath(''); + putenv('PATH='); $finder = new ExecutableFinder(); $result = $finder->find('foo', $expected); @@ -71,7 +60,7 @@ public function testFindWithNullAsDefault() $this->markTestSkipped('Cannot test when open_basedir is set'); } - $this->setPath(''); + putenv('PATH='); $finder = new ExecutableFinder(); @@ -86,7 +75,7 @@ public function testFindWithExtraDirs() $this->markTestSkipped('Cannot test when open_basedir is set'); } - $this->setPath(''); + putenv('PATH='); $extraDirs = [\dirname(\PHP_BINARY)]; @@ -129,7 +118,6 @@ public function testFindProcessInOpenBasedir() $this->markTestSkipped('Cannot run test on windows'); } - $this->setPath(''); $this->iniSet('open_basedir', \PHP_BINARY.\PATH_SEPARATOR.'/'); $finder = new ExecutableFinder(); @@ -154,7 +142,7 @@ public function testFindBatchExecutableOnWindows() $this->assertFalse(is_executable($target)); - $this->setPath(sys_get_temp_dir()); + putenv('PATH='.sys_get_temp_dir()); $finder = new ExecutableFinder(); $result = $finder->find(basename($target), false); From 7dea4fddc00bd9e23f494927caf0af73f2becd8f Mon Sep 17 00:00:00 2001 From: Yanick Witschi Date: Mon, 5 Dec 2022 15:48:16 +0100 Subject: [PATCH 37/73] Introducing a new PhpSubprocess handler --- CHANGELOG.md | 2 + PhpSubprocess.php | 164 +++++++++++++++++++++++++++++ Tests/Fixtures/memory.php | 3 + Tests/OutputMemoryLimitProcess.php | 28 +++++ Tests/PhpSubprocessTest.php | 75 +++++++++++++ 5 files changed, 272 insertions(+) create mode 100644 PhpSubprocess.php create mode 100644 Tests/Fixtures/memory.php create mode 100755 Tests/OutputMemoryLimitProcess.php create mode 100644 Tests/PhpSubprocessTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ec2032..e26819b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ CHANGELOG 6.4 --- + * Add `PhpSubprocess` to handle PHP subprocesses that take over the + configuration from their parent * Add `RunProcessMessage` and `RunProcessMessageHandler` * Support using `Process::findExecutable()` independently of `open_basedir` diff --git a/PhpSubprocess.php b/PhpSubprocess.php new file mode 100644 index 00000000..5467e9ba --- /dev/null +++ b/PhpSubprocess.php @@ -0,0 +1,164 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * PhpSubprocess runs a PHP command as a subprocess while keeping the original php.ini settings. + * + * For this, it generates a temporary php.ini file taking over all the current settings and disables + * loading additional .ini files. Basically, your command gets prefixed using "php -n -c /tmp/temp.ini". + * + * Given your php.ini contains "memory_limit=-1" and you have a "MemoryTest.php" with the following content: + * + * run(); + * print $p->getOutput()."\n"; + * + * This will output "string(2) "-1", because the process is started with the default php.ini settings. + * + * $p = new PhpSubprocess(['MemoryTest.php'], null, null, 60, ['php', '-d', 'memory_limit=256M']); + * $p->run(); + * print $p->getOutput()."\n"; + * + * This will output "string(4) "256M"", because the process is started with the temporarily created php.ini settings. + * + * @author Yanick Witschi + * @author Partially copied and heavily inspired from composer/xdebug-handler by John Stevenson + */ +class PhpSubprocess extends Process +{ + /** + * @param array $command The command to run and its arguments listed as separate entries. They will automatically + * get prefixed with the PHP binary + * @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 int $timeout The timeout in seconds + * @param array|null $php Path to the PHP binary to use with any additional arguments + */ + public function __construct(array $command, string $cwd = null, array $env = null, int $timeout = 60, array $php = null) + { + if (null === $php) { + $executableFinder = new PhpExecutableFinder(); + $php = $executableFinder->find(false); + $php = false === $php ? null : array_merge([$php], $executableFinder->findArguments()); + } + + if (null === $php) { + throw new RuntimeException('Unable to find PHP binary.'); + } + + $tmpIni = $this->writeTmpIni($this->getAllIniFiles(), sys_get_temp_dir()); + + $php = array_merge($php, ['-n', '-c', $tmpIni]); + register_shutdown_function('unlink', $tmpIni); + + $command = array_merge($php, $command); + + parent::__construct($command, $cwd, $env, null, $timeout); + } + + public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, mixed $input = null, ?float $timeout = 60): static + { + throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); + } + + public function start(callable $callback = null, array $env = []) + { + if (null === $this->getCommandLine()) { + throw new RuntimeException('Unable to find the PHP executable.'); + } + + parent::start($callback, $env); + } + + private function writeTmpIni(array $iniFiles, string $tmpDir): string + { + if (false === $tmpfile = @tempnam($tmpDir, '')) { + throw new RuntimeException('Unable to create temporary ini file.'); + } + + // $iniFiles has at least one item and it may be empty + if ('' === $iniFiles[0]) { + array_shift($iniFiles); + } + + $content = ''; + + foreach ($iniFiles as $file) { + // Check for inaccessible ini files + if (($data = @file_get_contents($file)) === false) { + throw new RuntimeException('Unable to read ini: '.$file); + } + // Check and remove directives after HOST and PATH sections + if (preg_match('/^\s*\[(?:PATH|HOST)\s*=/mi', $data, $matches)) { + $data = substr($data, 0, $matches[0][1]); + } + + $content .= $data."\n"; + } + + // Merge loaded settings into our ini content, if it is valid + $config = parse_ini_string($content); + $loaded = ini_get_all(null, false); + + if (false === $config || false === $loaded) { + throw new RuntimeException('Unable to parse ini data.'); + } + + $content .= $this->mergeLoadedConfig($loaded, $config); + + // Work-around for https://bugs.php.net/bug.php?id=75932 + $content .= "opcache.enable_cli=0\n"; + + if (false === @file_put_contents($tmpfile, $content)) { + throw new RuntimeException('Unable to write temporary ini file.'); + } + + return $tmpfile; + } + + private function mergeLoadedConfig(array $loadedConfig, array $iniConfig): string + { + $content = ''; + + foreach ($loadedConfig as $name => $value) { + if (!\is_string($value)) { + continue; + } + + if (!isset($iniConfig[$name]) || $iniConfig[$name] !== $value) { + // Double-quote escape each value + $content .= $name.'="'.addcslashes($value, '\\"')."\"\n"; + } + } + + return $content; + } + + private function getAllIniFiles(): array + { + $paths = [(string) php_ini_loaded_file()]; + + if (false !== $scanned = php_ini_scanned_files()) { + $paths = array_merge($paths, array_map('trim', explode(',', $scanned))); + } + + return $paths; + } +} diff --git a/Tests/Fixtures/memory.php b/Tests/Fixtures/memory.php new file mode 100644 index 00000000..1d3b2487 --- /dev/null +++ b/Tests/Fixtures/memory.php @@ -0,0 +1,3 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\PhpSubprocess; +use Symfony\Component\Process\Process; + +require is_file(\dirname(__DIR__).'/vendor/autoload.php') ? \dirname(__DIR__).'/vendor/autoload.php' : \dirname(__DIR__, 5).'/vendor/autoload.php'; + +['e' => $php, 'p' => $process] = getopt('e:p:') + ['e' => 'php', 'p' => 'Process']; + +if ('Process' === $process) { + $p = new Process([$php, __DIR__.'/Fixtures/memory.php']); +} else { + $p = new PhpSubprocess([__DIR__.'/Fixtures/memory.php'], null, null, 60, [$php]); +} + +$p->mustRun(); +echo $p->getOutput(); diff --git a/Tests/PhpSubprocessTest.php b/Tests/PhpSubprocessTest.php new file mode 100644 index 00000000..56b32ae8 --- /dev/null +++ b/Tests/PhpSubprocessTest.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Process; + +class PhpSubprocessTest extends TestCase +{ + private static $phpBin; + + public static function setUpBeforeClass(): void + { + $phpBin = new PhpExecutableFinder(); + self::$phpBin = getenv('SYMFONY_PROCESS_PHP_TEST_BINARY') ?: ('phpdbg' === \PHP_SAPI ? 'php' : $phpBin->find()); + } + + /** + * @dataProvider subprocessProvider + */ + public function testSubprocess(string $processClass, string $memoryLimit, string $expectedMemoryLimit) + { + $process = new Process([self::$phpBin, + '-d', + 'memory_limit='.$memoryLimit, + __DIR__.'/OutputMemoryLimitProcess.php', + '-e', self::$phpBin, + '-p', $processClass, + ]); + + $process->mustRun(); + $this->assertEquals($expectedMemoryLimit, trim($process->getOutput())); + } + + public static function subprocessProvider(): \Generator + { + yield 'Process does ignore dynamic memory_limit' => [ + 'Process', + self::getRandomMemoryLimit(), + self::getCurrentMemoryLimit(), + ]; + + yield 'PhpSubprocess does not ignore dynamic memory_limit' => [ + 'PhpSubprocess', + self::getRandomMemoryLimit(), + self::getRandomMemoryLimit(), + ]; + } + + private static function getCurrentMemoryLimit(): string + { + return trim(\ini_get('memory_limit')); + } + + private static function getRandomMemoryLimit(): string + { + $memoryLimit = 123; // Take something that's really unlikely to be configured on a user system. + + while (($formatted = $memoryLimit.'M') === self::getCurrentMemoryLimit()) { + ++$memoryLimit; + } + + return $formatted; + } +} From 273f0c851127b7a379f725b334ac2636f643d29f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 3 Aug 2023 12:52:11 +0200 Subject: [PATCH 38/73] [Process] Fix return type --- PhpSubprocess.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PhpSubprocess.php b/PhpSubprocess.php index 5467e9ba..0720520f 100644 --- a/PhpSubprocess.php +++ b/PhpSubprocess.php @@ -78,7 +78,7 @@ public static function fromShellCommandline(string $command, string $cwd = null, throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } - public function start(callable $callback = null, array $env = []) + public function start(callable $callback = null, array $env = []): void { if (null === $this->getCommandLine()) { throw new RuntimeException('Unable to find the PHP executable.'); From 4fc282273da68a11cb6ba17e8946300480bad82d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 7 Aug 2023 11:50:57 +0200 Subject: [PATCH 39/73] [Process] fix tests --- Tests/ExecutableFinderTest.php | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 54e740ec..a0f62273 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -98,6 +98,7 @@ public function testFindWithOpenBaseDir() $this->markTestSkipped('Cannot test when open_basedir is set'); } + putenv('PATH='.\dirname(\PHP_BINARY)); $this->iniSet('open_basedir', \dirname(\PHP_BINARY).\PATH_SEPARATOR.'/'); $finder = new ExecutableFinder(); @@ -106,26 +107,6 @@ public function testFindWithOpenBaseDir() $this->assertSamePath(\PHP_BINARY, $result); } - /** - * @runInSeparateProcess - */ - public function testFindProcessInOpenBasedir() - { - if (\ini_get('open_basedir')) { - $this->markTestSkipped('Cannot test when open_basedir is set'); - } - if ('\\' === \DIRECTORY_SEPARATOR) { - $this->markTestSkipped('Cannot run test on windows'); - } - - $this->iniSet('open_basedir', \PHP_BINARY.\PATH_SEPARATOR.'/'); - - $finder = new ExecutableFinder(); - $result = $finder->find($this->getPhpBinaryName(), false); - - $this->assertSamePath(\PHP_BINARY, $result); - } - public function testFindBatchExecutableOnWindows() { if (\ini_get('open_basedir')) { From 83264b6a59a114522e4107cc249172aaa78b9720 Mon Sep 17 00:00:00 2001 From: Uladzimir Tsykun Date: Mon, 11 Sep 2023 18:39:34 +0200 Subject: [PATCH 40/73] Make tests green again --- Tests/Messenger/RunProcessMessageHandlerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Messenger/RunProcessMessageHandlerTest.php b/Tests/Messenger/RunProcessMessageHandlerTest.php index 10ed9bb2..d406d243 100644 --- a/Tests/Messenger/RunProcessMessageHandlerTest.php +++ b/Tests/Messenger/RunProcessMessageHandlerTest.php @@ -33,7 +33,7 @@ public function testRunFailedProcess() (new RunProcessMessageHandler())(new RunProcessMessage(['invalid'])); } catch (RunProcessFailedException $e) { $this->assertSame(['invalid'], $e->context->command); - $this->assertSame(127, $e->context->exitCode); + $this->assertSame('\\' === \DIRECTORY_SEPARATOR ? 1 : 127, $e->context->exitCode); return; } From 7a98cdf5045b5b98ef2589b7d074f0c27eea4948 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 25 Sep 2023 14:52:38 +0200 Subject: [PATCH 41/73] Minor CS fixes --- Messenger/RunProcessMessage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Messenger/RunProcessMessage.php b/Messenger/RunProcessMessage.php index 1d87e9c4..b2c33fe3 100644 --- a/Messenger/RunProcessMessage.php +++ b/Messenger/RunProcessMessage.php @@ -27,6 +27,6 @@ public function __construct( public function __toString(): string { - return \implode(' ', $this->command); + return implode(' ', $this->command); } } From b8f2424f056ced2e286737bb3b9e4aec9bba40db Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Thu, 24 Aug 2023 11:25:40 -0400 Subject: [PATCH 42/73] [Process] Fix bug where $this->callback is never null, resulting in bad argument --- Process.php | 8 ++++---- Tests/ProcessTest.php | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Process.php b/Process.php index 1fac6844..a5bcc8d6 100644 --- a/Process.php +++ b/Process.php @@ -304,7 +304,7 @@ public function start(callable $callback = null, array $env = []) $this->resetProcessData(); $this->starttime = $this->lastOutputTime = microtime(true); $this->callback = $this->buildCallback($callback); - $descriptors = $this->getDescriptors(); + $descriptors = $this->getDescriptors(null !== $callback); if ($this->env) { $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env; @@ -1240,15 +1240,15 @@ public static function isPtySupported(): bool /** * Creates the descriptors needed by the proc_open. */ - private function getDescriptors(): array + private function getDescriptors(bool $hasCallback): array { if ($this->input instanceof \Iterator) { $this->input->rewind(); } if ('\\' === \DIRECTORY_SEPARATOR) { - $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->callback); + $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $hasCallback); } else { - $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->callback); + $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $hasCallback); } return $this->processPipes->getDescriptors(); diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 02df607a..44fb54ee 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -200,6 +200,20 @@ public function testCallbacksAreExecutedWithStart() $this->assertSame('foo'.\PHP_EOL, $data); } + public function testReadSupportIsDisabledWithoutCallback() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait".'); + + $process = $this->getProcess('echo foo'); + // disabling output + not passing a callback to start() => read support disabled + $process->disableOutput(); + $process->start(); + $process->wait(function ($type, $buffer) use (&$data) { + $data .= $buffer; + }); + } + /** * tests results from sub processes. * From 12f9ebe6c117798f5be8692c8abfdf1810b52671 Mon Sep 17 00:00:00 2001 From: "a.dmitryuk" Date: Fri, 27 Oct 2023 15:43:31 +0600 Subject: [PATCH 43/73] [Process] remove fixing of legacy bug, when PTS functionality is enabled --- Process.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Process.php b/Process.php index a5bcc8d6..a0fb03f1 100644 --- a/Process.php +++ b/Process.php @@ -332,10 +332,6 @@ 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 2>/dev/null; code=$?; echo $code >&3; exit $code'; - - // Workaround for the bug, when PTS functionality is enabled. - // @see : https://bugs.php.net/69442 - $ptsWorkaround = fopen(__FILE__, 'r'); } $envPairs = []; From 146e8ea7710c0444a077f73707199217b85a7dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Auswo=CC=88ger?= Date: Tue, 31 Oct 2023 23:48:40 +0100 Subject: [PATCH 44/73] Fix memory limit in PhpSubprocess unit test --- Tests/PhpSubprocessTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/PhpSubprocessTest.php b/Tests/PhpSubprocessTest.php index 56b32ae8..3406e649 100644 --- a/Tests/PhpSubprocessTest.php +++ b/Tests/PhpSubprocessTest.php @@ -47,7 +47,7 @@ public static function subprocessProvider(): \Generator yield 'Process does ignore dynamic memory_limit' => [ 'Process', self::getRandomMemoryLimit(), - self::getCurrentMemoryLimit(), + self::getDefaultMemoryLimit(), ]; yield 'PhpSubprocess does not ignore dynamic memory_limit' => [ @@ -57,16 +57,16 @@ public static function subprocessProvider(): \Generator ]; } - private static function getCurrentMemoryLimit(): string + private static function getDefaultMemoryLimit(): string { - return trim(\ini_get('memory_limit')); + return trim(ini_get_all()['memory_limit']['global_value']); } private static function getRandomMemoryLimit(): string { $memoryLimit = 123; // Take something that's really unlikely to be configured on a user system. - while (($formatted = $memoryLimit.'M') === self::getCurrentMemoryLimit()) { + while (($formatted = $memoryLimit.'M') === self::getDefaultMemoryLimit()) { ++$memoryLimit; } From 0f2afc8bf36248bbf77abe2f0cbb6ec76384b22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Auswo=CC=88ger?= Date: Tue, 31 Oct 2023 23:11:00 +0100 Subject: [PATCH 45/73] [Process] Remove dead code from Process --- Process.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Process.php b/Process.php index a0fb03f1..00790a55 100644 --- a/Process.php +++ b/Process.php @@ -77,7 +77,6 @@ class Process implements \IteratorAggregate private bool $pty; private array $options = ['suppress_errors' => true, 'bypass_shell' => true]; - private bool $useFileHandles; private WindowsPipes|UnixPipes $processPipes; private ?int $latestSignal = null; @@ -163,7 +162,6 @@ public function __construct(array $command, string $cwd = null, array $env = nul $this->setInput($input); $this->setTimeout($timeout); - $this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR; $this->pty = false; } @@ -325,7 +323,7 @@ public function start(callable $callback = null, array $env = []) if ('\\' === \DIRECTORY_SEPARATOR) { $commandline = $this->prepareWindowsCommandLine($commandline, $env); - } elseif (!$this->useFileHandles && $this->isSigchildEnabled()) { + } elseif ($this->isSigchildEnabled()) { // last exit code is output on the fourth pipe and caught to work around --enable-sigchild $descriptors[3] = ['pipe', 'w']; From 0b93f32e16c7eb7094d26887f8657fe84cfd0a4f Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 2 Nov 2023 14:06:08 +0100 Subject: [PATCH 46/73] do not let context classes extend the message classes --- Messenger/RunProcessContext.php | 10 +++++----- Tests/Messenger/RunProcessMessageHandlerTest.php | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Messenger/RunProcessContext.php b/Messenger/RunProcessContext.php index 3c7da369..b5ade072 100644 --- a/Messenger/RunProcessContext.php +++ b/Messenger/RunProcessContext.php @@ -16,16 +16,16 @@ /** * @author Kevin Bond */ -final class RunProcessContext extends RunProcessMessage +final class RunProcessContext { public readonly ?int $exitCode; public readonly ?string $output; public readonly ?string $errorOutput; - public function __construct(RunProcessMessage $message, Process $process) - { - parent::__construct($message->command, $message->cwd, $message->env, $message->input, $message->timeout); - + public function __construct( + public readonly RunProcessMessage $message, + Process $process, + ) { $this->exitCode = $process->getExitCode(); $this->output = $process->isOutputDisabled() ? null : $process->getOutput(); $this->errorOutput = $process->isOutputDisabled() ? null : $process->getErrorOutput(); diff --git a/Tests/Messenger/RunProcessMessageHandlerTest.php b/Tests/Messenger/RunProcessMessageHandlerTest.php index d406d243..049da77a 100644 --- a/Tests/Messenger/RunProcessMessageHandlerTest.php +++ b/Tests/Messenger/RunProcessMessageHandlerTest.php @@ -22,7 +22,7 @@ public function testRunSuccessfulProcess() { $context = (new RunProcessMessageHandler())(new RunProcessMessage(['ls'], cwd: __DIR__)); - $this->assertSame(['ls'], $context->command); + $this->assertSame(['ls'], $context->message->command); $this->assertSame(0, $context->exitCode); $this->assertStringContainsString(basename(__FILE__), $context->output); } @@ -32,7 +32,7 @@ public function testRunFailedProcess() try { (new RunProcessMessageHandler())(new RunProcessMessage(['invalid'])); } catch (RunProcessFailedException $e) { - $this->assertSame(['invalid'], $e->context->command); + $this->assertSame(['invalid'], $e->context->message->command); $this->assertSame('\\' === \DIRECTORY_SEPARATOR ? 1 : 127, $e->context->exitCode); return; From a91571ff5df8825fcc74569d99cddc7242f479b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sat, 4 Nov 2023 21:16:32 +0100 Subject: [PATCH 47/73] PHP files cannot be executable without shebang --- Tests/OutputMemoryLimitProcess.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 Tests/OutputMemoryLimitProcess.php diff --git a/Tests/OutputMemoryLimitProcess.php b/Tests/OutputMemoryLimitProcess.php old mode 100755 new mode 100644 From 11ab5d7c99854aff757f01cd27e709209e72f356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Auswo=CC=88ger?= Date: Wed, 1 Nov 2023 17:49:58 +0100 Subject: [PATCH 48/73] [Process] Pass the commandline as array to `proc_open()` --- Exception/ProcessStartFailedException.php | 45 ++++++++++++++ Messenger/RunProcessContext.php | 4 +- Process.php | 61 +++++++++++++------ .../RunProcessMessageHandlerTest.php | 6 +- Tests/ProcessTest.php | 23 +++++++ 5 files changed, 116 insertions(+), 23 deletions(-) create mode 100644 Exception/ProcessStartFailedException.php diff --git a/Exception/ProcessStartFailedException.php b/Exception/ProcessStartFailedException.php new file mode 100644 index 00000000..9bd5a036 --- /dev/null +++ b/Exception/ProcessStartFailedException.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception for processes failed during startup. + */ +class ProcessStartFailedException extends ProcessFailedException +{ + private Process $process; + + public function __construct(Process $process, ?string $message) + { + if ($process->isStarted()) { + throw new InvalidArgumentException('Expected a process that failed during startup, but the given process was started successfully.'); + } + + $error = sprintf('The command "%s" failed.'."\n\nWorking directory: %s\n\nError: %s", + $process->getCommandLine(), + $process->getWorkingDirectory(), + $message ?? 'unknown' + ); + + // Skip parent constructor + RuntimeException::__construct($error); + + $this->process = $process; + } + + public function getProcess(): Process + { + return $this->process; + } +} diff --git a/Messenger/RunProcessContext.php b/Messenger/RunProcessContext.php index b5ade072..5e223040 100644 --- a/Messenger/RunProcessContext.php +++ b/Messenger/RunProcessContext.php @@ -27,7 +27,7 @@ public function __construct( Process $process, ) { $this->exitCode = $process->getExitCode(); - $this->output = $process->isOutputDisabled() ? null : $process->getOutput(); - $this->errorOutput = $process->isOutputDisabled() ? null : $process->getErrorOutput(); + $this->output = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getOutput(); + $this->errorOutput = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getErrorOutput(); } } diff --git a/Process.php b/Process.php index 6b73c31d..0b29a646 100644 --- a/Process.php +++ b/Process.php @@ -15,6 +15,7 @@ use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessSignaledException; +use Symfony\Component\Process\Exception\ProcessStartFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\Pipes\UnixPipes; @@ -233,11 +234,11 @@ public function __clone() * * @return int The exit status code * - * @throws RuntimeException When process can't be launched - * @throws RuntimeException When process is already running - * @throws ProcessTimedOutException When process timed out - * @throws ProcessSignaledException When process stopped after receiving signal - * @throws LogicException In case a callback is provided and output has been disabled + * @throws ProcessStartFailedException When process can't be launched + * @throws RuntimeException When process is already running + * @throws ProcessTimedOutException When process timed out + * @throws ProcessSignaledException When process stopped after receiving signal + * @throws LogicException In case a callback is provided and output has been disabled * * @final */ @@ -284,9 +285,9 @@ public function mustRun(callable $callback = null, array $env = []): static * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * - * @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 + * @throws ProcessStartFailedException 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, array $env = []): void { @@ -306,12 +307,7 @@ public function start(callable $callback = null, array $env = []): void $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)); - - if ('\\' !== \DIRECTORY_SEPARATOR) { - // exec is mandatory to deal with sending a signal to the process - $commandline = 'exec '.$commandline; - } + $commandline = array_values(array_map(strval(...), $commandline)); } else { $commandline = $this->replacePlaceholders($commandline, $env); } @@ -322,6 +318,11 @@ public function start(callable $callback = null, array $env = []): void // last exit code is output on the fourth pipe and caught to work around --enable-sigchild $descriptors[3] = ['pipe', 'w']; + if (\is_array($commandline)) { + // exec is mandatory to deal with sending a signal to the process + $commandline = 'exec '.$this->buildShellCommandline($commandline); + } + // 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 2>/dev/null; code=$?; echo $code >&3; exit $code'; @@ -338,10 +339,20 @@ public function start(callable $callback = null, array $env = []): void throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd)); } - $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + $lastError = null; + set_error_handler(function ($type, $msg) use (&$lastError) { + $lastError = $msg; + + return true; + }); + try { + $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + } finally { + restore_error_handler(); + } if (!\is_resource($process)) { - throw new RuntimeException('Unable to launch a new process.'); + throw new ProcessStartFailedException($this, $lastError); } $this->process = $process; $this->status = self::STATUS_STARTED; @@ -366,8 +377,8 @@ public function start(callable $callback = null, array $env = []): void * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * - * @throws RuntimeException When process can't be launched - * @throws RuntimeException When process is already running + * @throws ProcessStartFailedException When process can't be launched + * @throws RuntimeException When process is already running * * @see start() * @@ -943,7 +954,7 @@ public function getLastOutputTime(): ?float */ public function getCommandLine(): string { - return \is_array($this->commandline) ? implode(' ', array_map($this->escapeArgument(...), $this->commandline)) : $this->commandline; + return $this->buildShellCommandline($this->commandline); } /** @@ -1472,8 +1483,18 @@ private function doSignal(int $signal, bool $throwException): bool return true; } - private function prepareWindowsCommandLine(string $cmd, array &$env): string + private function buildShellCommandline(string|array $commandline): string + { + if (\is_string($commandline)) { + return $commandline; + } + + return implode(' ', array_map($this->escapeArgument(...), $commandline)); + } + + private function prepareWindowsCommandLine(string|array $cmd, array &$env): string { + $cmd = $this->buildShellCommandline($cmd); $uid = uniqid('', true); $cmd = preg_replace_callback( '/"(?:( diff --git a/Tests/Messenger/RunProcessMessageHandlerTest.php b/Tests/Messenger/RunProcessMessageHandlerTest.php index 049da77a..e095fa09 100644 --- a/Tests/Messenger/RunProcessMessageHandlerTest.php +++ b/Tests/Messenger/RunProcessMessageHandlerTest.php @@ -33,7 +33,11 @@ public function testRunFailedProcess() (new RunProcessMessageHandler())(new RunProcessMessage(['invalid'])); } catch (RunProcessFailedException $e) { $this->assertSame(['invalid'], $e->context->message->command); - $this->assertSame('\\' === \DIRECTORY_SEPARATOR ? 1 : 127, $e->context->exitCode); + $this->assertContains( + $e->context->exitCode, + [null, '\\' === \DIRECTORY_SEPARATOR ? 1 : 127], + 'Exit code should be 1 on Windows, 127 on other systems, or null', + ); return; } diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 44fb54ee..dfb4fd29 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessSignaledException; +use Symfony\Component\Process\Exception\ProcessStartFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\InputStream; @@ -66,6 +67,28 @@ public function testInvalidCwd() $cmd->run(); } + /** + * @dataProvider invalidProcessProvider + */ + public function testInvalidCommand(Process $process) + { + try { + $this->assertSame('\\' === \DIRECTORY_SEPARATOR ? 1 : 127, $process->run()); + } catch (ProcessStartFailedException $e) { + // An invalid command might already fail during start since PHP 8.3 for platforms + // supporting posix_spawn(), see https://github.com/php/php-src/issues/12589 + $this->assertStringContainsString('No such file or directory', $e->getMessage()); + } + } + + public function invalidProcessProvider() + { + return [ + [new Process(['invalid'])], + [Process::fromShellCommandline('invalid')], + ]; + } + /** * @group transient-on-windows */ From c4b1ef0bc80533d87a2e969806172f1c2a980241 Mon Sep 17 00:00:00 2001 From: Kieran Date: Fri, 22 Dec 2023 16:42:54 +0000 Subject: [PATCH 49/73] Suppress warnings from is_executable --- ExecutableFinder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 3681e356..412723a9 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -73,7 +73,7 @@ public function find(string $name, string $default = null, array $extraDirs = [] } $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v'; - if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && is_executable($executablePath)) { + if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && @is_executable($executablePath)) { return $executablePath; } From da3a37850f7d13e2aca31a687eda3ff74a93a38b Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 1 Nov 2023 09:14:07 +0100 Subject: [PATCH 50/73] [Tests] Streamline --- Tests/ProcessTest.php | 54 +++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index dfb4fd29..63ad90d9 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -328,11 +328,13 @@ public function testSetInputWhileRunningThrowsAnException() /** * @dataProvider provideInvalidInputValues */ - public function testInvalidInput($value) + public function testInvalidInput(array|object $value) { + $process = $this->getProcess('foo'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('"Symfony\Component\Process\Process::setInput" only accepts strings, Traversable objects or stream resources.'); - $process = $this->getProcess('foo'); + $process->setInput($value); } @@ -347,7 +349,7 @@ public static function provideInvalidInputValues() /** * @dataProvider provideInputValues */ - public function testValidInput($expected, $value) + public function testValidInput(?string $expected, null|float|string $value) { $process = $this->getProcess('foo'); $process->setInput($value); @@ -593,8 +595,10 @@ public function testSuccessfulMustRunHasCorrectExitCode() public function testMustRunThrowsException() { - $this->expectException(ProcessFailedException::class); $process = $this->getProcess('exit 1'); + + $this->expectException(ProcessFailedException::class); + $process->mustRun(); } @@ -972,9 +976,11 @@ public function testExitCodeIsAvailableAfterSignal() public function testSignalProcessNotRunning() { + $process = $this->getProcess('foo'); + $this->expectException(LogicException::class); $this->expectExceptionMessage('Cannot send signal on a non running process.'); - $process = $this->getProcess('foo'); + $process->signal(1); // SIGHUP } @@ -1062,20 +1068,24 @@ public function testDisableOutputDisablesTheOutput() public function testDisableOutputWhileRunningThrowsException() { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Disabling output while the process is running is not possible.'); $p = $this->getProcessForCode('sleep(39);'); $p->start(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Disabling output while the process is running is not possible.'); + $p->disableOutput(); } public function testEnableOutputWhileRunningThrowsException() { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Enabling output while the process is running is not possible.'); $p = $this->getProcessForCode('sleep(40);'); $p->disableOutput(); $p->start(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Enabling output while the process is running is not possible.'); + $p->enableOutput(); } @@ -1091,19 +1101,23 @@ public function testEnableOrDisableOutputAfterRunDoesNotThrowException() public function testDisableOutputWhileIdleTimeoutIsSet() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Output cannot be disabled while an idle timeout is set.'); $process = $this->getProcess('foo'); $process->setIdleTimeout(1); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Output cannot be disabled while an idle timeout is set.'); + $process->disableOutput(); } public function testSetIdleTimeoutWhileOutputIsDisabled() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('timeout cannot be set while the output is disabled.'); $process = $this->getProcess('foo'); $process->disableOutput(); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('timeout cannot be set while the output is disabled.'); + $process->setIdleTimeout(1); } @@ -1119,11 +1133,13 @@ public function testSetNullIdleTimeoutWhileOutputIsDisabled() */ public function testGetOutputWhileDisabled($fetchMethod) { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Output has been disabled.'); $p = $this->getProcessForCode('sleep(41);'); $p->disableOutput(); $p->start(); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Output has been disabled.'); + $p->{$fetchMethod}(); } @@ -1523,17 +1539,21 @@ public function testPreparedCommandWithQuoteInIt() public function testPreparedCommandWithMissingValue() { + $p = Process::fromShellCommandline('echo "${:abc}"'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Command line is missing a value for parameter "abc": echo "${:abc}"'); - $p = Process::fromShellCommandline('echo "${:abc}"'); + $p->run(null, ['bcd' => 'BCD']); } public function testPreparedCommandWithNoValues() { + $p = Process::fromShellCommandline('echo "${:abc}"'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Command line is missing a value for parameter "abc": echo "${:abc}"'); - $p = Process::fromShellCommandline('echo "${:abc}"'); + $p->run(null, []); } From 80826a9791754c93e06a80a348f66837aa5d0d1d Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Mon, 15 Jan 2024 20:49:54 +0100 Subject: [PATCH 51/73] CS: enable ordered_types.null_adjustment=always_last --- Tests/ProcessTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 63ad90d9..b2a34a21 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -349,7 +349,7 @@ public static function provideInvalidInputValues() /** * @dataProvider provideInputValues */ - public function testValidInput(?string $expected, null|float|string $value) + public function testValidInput(?string $expected, float|string|null $value) { $process = $this->getProcess('foo'); $process->setInput($value); From 710e27879e9be3395de2b98da3f52a946039f297 Mon Sep 17 00:00:00 2001 From: Kay Wei Date: Tue, 20 Feb 2024 16:24:14 +0800 Subject: [PATCH 52/73] Fix the `command -v` exception when the command option with a dash prefix --- ExecutableFinder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index cc789603..8c7bf58d 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -72,7 +72,7 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ } } - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v'; + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && @is_executable($executablePath)) { return $executablePath; } From cebb2aec790b0fba2489af42a6c60933203e6390 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Mon, 18 Mar 2024 20:27:13 +0100 Subject: [PATCH 53/73] chore: CS fixes --- Tests/SignalListener.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/SignalListener.php b/Tests/SignalListener.php index 618be740..7a351858 100644 --- a/Tests/SignalListener.php +++ b/Tests/SignalListener.php @@ -9,7 +9,10 @@ * file that was distributed with this source code. */ -pcntl_signal(\SIGUSR1, function () { echo 'SIGUSR1'; exit; }); +pcntl_signal(\SIGUSR1, function () { + echo 'SIGUSR1'; + exit; +}); echo 'Caught '; From 14730a20edaff1b8d5ca265b1a8e85aa8ac88993 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Fri, 16 Feb 2024 12:37:04 +0100 Subject: [PATCH 54/73] feat(process): allow to ignore signals when executing a process --- CHANGELOG.md | 5 +++++ Process.php | 34 ++++++++++++++++++++++++++++++++++ Tests/ProcessTest.php | 30 ++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e26819b5..f7b68b5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `Process::setIgnoredSignals()` to disable signal propagation to the child process + 6.4 --- diff --git a/Process.php b/Process.php index c95afabc..0b593201 100644 --- a/Process.php +++ b/Process.php @@ -77,6 +77,7 @@ class Process implements \IteratorAggregate private bool $tty = false; private bool $pty; private array $options = ['suppress_errors' => true, 'bypass_shell' => true]; + private array $ignoredSignals = []; private WindowsPipes|UnixPipes $processPipes; @@ -346,9 +347,23 @@ public function start(?callable $callback = null, array $env = []): void return true; }); + + $oldMask = []; + + if (\function_exists('pcntl_sigprocmask')) { + // we block signals we want to ignore, as proc_open will use fork / posix_spawn which will copy the signal mask this allow to block + // signals in the child process + pcntl_sigprocmask(\SIG_BLOCK, $this->ignoredSignals, $oldMask); + } + try { $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); } finally { + if (\function_exists('pcntl_sigprocmask')) { + // we restore the signal mask here to avoid any side effects + pcntl_sigprocmask(\SIG_SETMASK, $oldMask); + } + restore_error_handler(); } @@ -1206,6 +1221,20 @@ public function setOptions(array $options): void } } + /** + * Defines a list of posix signals that will not be propagated to the process. + * + * @param list<\SIG*> $signals + */ + public function setIgnoredSignals(array $signals): void + { + if ($this->isRunning()) { + throw new RuntimeException('Setting ignored signals while the process is running is not possible.'); + } + + $this->ignoredSignals = $signals; + } + /** * Returns whether TTY is supported on the current operating system. */ @@ -1455,6 +1484,11 @@ private function resetProcessData(): void */ private function doSignal(int $signal, bool $throwException): bool { + // Signal seems to be send when sigchild is enable, this allow blocking the signal correctly in this case + if ($this->isSigchildEnabled() && \in_array($signal, $this->ignoredSignals)) { + return false; + } + if (null === $pid = $this->getPid()) { if ($throwException) { throw new LogicException('Cannot send signal on a non running process.'); diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index d027cc50..653fa6d8 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1663,6 +1663,36 @@ public function testNotTerminableInputPipe() $this->assertFalse($process->isRunning()); } + public function testIgnoringSignal() + { + if (!\function_exists('pcntl_signal')) { + $this->markTestSkipped('pnctl extension is required.'); + } + + $process = $this->getProcess('sleep 10'); + $process->setIgnoredSignals([\SIGTERM]); + + $process->start(); + $process->stop(timeout: 0.2); + + $this->assertNotSame(\SIGTERM, $process->getTermSignal()); + } + + // This test ensure that the previous test is reliable, in case of the sleep command ignoring the SIGTERM signal + public function testNotIgnoringSignal() + { + if (!\function_exists('pcntl_signal')) { + $this->markTestSkipped('pnctl extension is required.'); + } + + $process = $this->getProcess('sleep 10'); + + $process->start(); + $process->stop(timeout: 0.2); + + $this->assertSame(\SIGTERM, $process->getTermSignal()); + } + private function getProcess(string|array $commandline, ?string $cwd = null, ?array $env = null, mixed $input = null, ?int $timeout = 60): Process { if (\is_string($commandline)) { From 8e99b1b15d2975b242fc089382c8f8055a85d996 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Fri, 5 Apr 2024 10:54:29 +0200 Subject: [PATCH 55/73] fix(process): don't call sigprocmask if there is no ignored signals --- Process.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Process.php b/Process.php index 0b593201..656834bd 100644 --- a/Process.php +++ b/Process.php @@ -350,7 +350,7 @@ public function start(?callable $callback = null, array $env = []): void $oldMask = []; - if (\function_exists('pcntl_sigprocmask')) { + if ($this->ignoredSignals && \function_exists('pcntl_sigprocmask')) { // we block signals we want to ignore, as proc_open will use fork / posix_spawn which will copy the signal mask this allow to block // signals in the child process pcntl_sigprocmask(\SIG_BLOCK, $this->ignoredSignals, $oldMask); @@ -359,7 +359,7 @@ public function start(?callable $callback = null, array $env = []): void try { $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); } finally { - if (\function_exists('pcntl_sigprocmask')) { + if ($this->ignoredSignals && \function_exists('pcntl_sigprocmask')) { // we restore the signal mask here to avoid any side effects pcntl_sigprocmask(\SIG_SETMASK, $oldMask); } From 1393de6f0688c254d8e3bc4933670a6700c2c64b Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 7 May 2024 13:49:07 -0400 Subject: [PATCH 56/73] Return false in isTtySupported() when open_basedir restrictions prevent access to /dev/tty. If open_basedir restrictions are in effect, checking if the file /dev/tty is writable will prevent setting tty mode on the process, and avoid failing to create a Process. --- Process.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Process.php b/Process.php index c804a179..bf2e8e85 100644 --- a/Process.php +++ b/Process.php @@ -1211,7 +1211,7 @@ public static function isTtySupported(): bool { static $isTtySupported; - return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)); + return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT) && @is_writable('/dev/tty')); } /** From f64dbc87a58cfedfc6e7f8635697a94386f2ccc4 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 24 May 2024 13:05:21 +0200 Subject: [PATCH 57/73] use constructor property promotion --- Exception/ProcessFailedException.php | 7 +++---- Exception/ProcessSignaledException.php | 9 +++------ Exception/ProcessStartFailedException.php | 10 ++++------ Exception/ProcessTimedOutException.php | 12 ++++-------- Pipes/UnixPipes.php | 16 ++++++---------- Pipes/WindowsPipes.php | 9 ++++----- 6 files changed, 24 insertions(+), 39 deletions(-) diff --git a/Exception/ProcessFailedException.php b/Exception/ProcessFailedException.php index 499809ee..6cd8a355 100644 --- a/Exception/ProcessFailedException.php +++ b/Exception/ProcessFailedException.php @@ -20,10 +20,9 @@ */ class ProcessFailedException extends RuntimeException { - private Process $process; - - public function __construct(Process $process) - { + public function __construct( + private Process $process, + ) { if ($process->isSuccessful()) { throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); } diff --git a/Exception/ProcessSignaledException.php b/Exception/ProcessSignaledException.php index 0fed8ac3..4466b0d8 100644 --- a/Exception/ProcessSignaledException.php +++ b/Exception/ProcessSignaledException.php @@ -20,12 +20,9 @@ */ final class ProcessSignaledException extends RuntimeException { - private Process $process; - - public function __construct(Process $process) - { - $this->process = $process; - + public function __construct( + private Process $process, + ) { parent::__construct(sprintf('The process has been signaled with signal "%s".', $process->getTermSignal())); } diff --git a/Exception/ProcessStartFailedException.php b/Exception/ProcessStartFailedException.php index 9bd5a036..24bd5009 100644 --- a/Exception/ProcessStartFailedException.php +++ b/Exception/ProcessStartFailedException.php @@ -18,10 +18,10 @@ */ class ProcessStartFailedException extends ProcessFailedException { - private Process $process; - - public function __construct(Process $process, ?string $message) - { + public function __construct( + private Process $process, + ?string $message, + ) { if ($process->isStarted()) { throw new InvalidArgumentException('Expected a process that failed during startup, but the given process was started successfully.'); } @@ -34,8 +34,6 @@ public function __construct(Process $process, ?string $message) // Skip parent constructor RuntimeException::__construct($error); - - $this->process = $process; } public function getProcess(): Process diff --git a/Exception/ProcessTimedOutException.php b/Exception/ProcessTimedOutException.php index 252e1112..b692e35f 100644 --- a/Exception/ProcessTimedOutException.php +++ b/Exception/ProcessTimedOutException.php @@ -23,14 +23,10 @@ class ProcessTimedOutException extends RuntimeException public const TYPE_GENERAL = 1; public const TYPE_IDLE = 2; - private Process $process; - private int $timeoutType; - - public function __construct(Process $process, int $timeoutType) - { - $this->process = $process; - $this->timeoutType = $timeoutType; - + public function __construct( + private Process $process, + private int $timeoutType, + ) { parent::__construct(sprintf( 'The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), diff --git a/Pipes/UnixPipes.php b/Pipes/UnixPipes.php index 7bd0db0e..8e95afaa 100644 --- a/Pipes/UnixPipes.php +++ b/Pipes/UnixPipes.php @@ -22,16 +22,12 @@ */ class UnixPipes extends AbstractPipes { - private ?bool $ttyMode; - private bool $ptyMode; - private bool $haveReadSupport; - - public function __construct(?bool $ttyMode, bool $ptyMode, mixed $input, bool $haveReadSupport) - { - $this->ttyMode = $ttyMode; - $this->ptyMode = $ptyMode; - $this->haveReadSupport = $haveReadSupport; - + public function __construct( + private ?bool $ttyMode, + private bool $ptyMode, + mixed $input, + private bool $haveReadSupport, + ) { parent::__construct($input); } diff --git a/Pipes/WindowsPipes.php b/Pipes/WindowsPipes.php index 8033442a..26fa7498 100644 --- a/Pipes/WindowsPipes.php +++ b/Pipes/WindowsPipes.php @@ -33,12 +33,11 @@ class WindowsPipes extends AbstractPipes Process::STDOUT => 0, Process::STDERR => 0, ]; - private bool $haveReadSupport; - - public function __construct(mixed $input, bool $haveReadSupport) - { - $this->haveReadSupport = $haveReadSupport; + public function __construct( + mixed $input, + private bool $haveReadSupport, + ) { if ($this->haveReadSupport) { // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. // Workaround for this problem is to use temporary files instead of pipes on Windows platform. From f7456b8509dacf9f6ae0f4bc6857fa6d7059e77a Mon Sep 17 00:00:00 2001 From: Travis Carden Date: Tue, 21 Nov 2023 18:24:14 -0500 Subject: [PATCH 58/73] [Process] `ExecutableFinder::addSuffix()` has no effect --- ExecutableFinder.php | 21 ++++++++++++---- Tests/ExecutableFinderTest.php | 25 +++++++++++++++++++ .../Fixtures/executable_with_added_suffix.foo | 1 + Tests/Fixtures/executable_without_suffix | 1 + 4 files changed, 43 insertions(+), 5 deletions(-) create mode 100755 Tests/Fixtures/executable_with_added_suffix.foo create mode 100755 Tests/Fixtures/executable_without_suffix diff --git a/ExecutableFinder.php b/ExecutableFinder.php index ceb7a558..9ab99606 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -19,7 +19,15 @@ */ class ExecutableFinder { - private array $suffixes = ['.exe', '.bat', '.cmd', '.com']; + private array $suffixes = []; + + public function __construct() + { + // Set common extensions on Windows. + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->suffixes = ['.exe', '.bat', '.cmd', '.com']; + } + } /** * Replaces default suffixes of executable. @@ -30,7 +38,10 @@ public function setSuffixes(array $suffixes): void } /** - * Adds new possible suffix to check for executable. + * Adds new possible suffix to check for executable, including the dot (.). + * + * $finder = new ExecutableFinder(); + * $finder->addSuffix('.foo'); */ public function addSuffix(string $suffix): void { @@ -52,10 +63,10 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ ); $suffixes = ['']; - if ('\\' === \DIRECTORY_SEPARATOR) { - $pathExt = getenv('PATHEXT'); - $suffixes = array_merge($pathExt ? explode(\PATH_SEPARATOR, $pathExt) : $this->suffixes, $suffixes); + if ('\\' === \DIRECTORY_SEPARATOR && $pathExt = getenv('PATHEXT')) { + $suffixes = array_merge(explode(\PATH_SEPARATOR, $pathExt), $suffixes); } + $suffixes = array_merge($suffixes, $this->suffixes); foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index a1b8d6d5..56cb3d51 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -85,6 +85,31 @@ public function testFindWithExtraDirs() $this->assertSamePath(\PHP_BINARY, $result); } + public function testFindWithoutSuffix() + { + $fixturesDir = __DIR__.\DIRECTORY_SEPARATOR.'Fixtures'; + $name = 'executable_without_suffix'; + + $finder = new ExecutableFinder(); + $result = $finder->find($name, null, [$fixturesDir]); + + $this->assertSamePath($fixturesDir.\DIRECTORY_SEPARATOR.$name, $result); + } + + public function testFindWithAddedSuffixes() + { + $fixturesDir = __DIR__.\DIRECTORY_SEPARATOR.'Fixtures'; + $name = 'executable_with_added_suffix'; + $suffix = '.foo'; + + $finder = new ExecutableFinder(); + $finder->addSuffix($suffix); + + $result = $finder->find($name, null, [$fixturesDir]); + + $this->assertSamePath($fixturesDir.\DIRECTORY_SEPARATOR.$name.$suffix, $result); + } + /** * @runInSeparateProcess */ diff --git a/Tests/Fixtures/executable_with_added_suffix.foo b/Tests/Fixtures/executable_with_added_suffix.foo new file mode 100755 index 00000000..471a493a --- /dev/null +++ b/Tests/Fixtures/executable_with_added_suffix.foo @@ -0,0 +1 @@ +See \Symfony\Component\Process\Tests\ExecutableFinderTest::testFindWithAddedSuffixes() diff --git a/Tests/Fixtures/executable_without_suffix b/Tests/Fixtures/executable_without_suffix new file mode 100755 index 00000000..9bf8b4db --- /dev/null +++ b/Tests/Fixtures/executable_without_suffix @@ -0,0 +1 @@ +See \Symfony\Component\Process\Tests\ExecutableFinderTest::testFindWithoutSuffix() From 6f5cb98173a20111362d5767158226a42bc61d0d Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 20 Jun 2024 17:52:34 +0200 Subject: [PATCH 59/73] Prefix all sprintf() calls --- Exception/ProcessFailedException.php | 4 ++-- Exception/ProcessSignaledException.php | 2 +- Exception/ProcessStartFailedException.php | 2 +- Exception/ProcessTimedOutException.php | 4 ++-- InputStream.php | 2 +- PhpProcess.php | 2 +- PhpSubprocess.php | 2 +- Pipes/AbstractPipes.php | 2 +- Pipes/WindowsPipes.php | 2 +- Process.php | 18 +++++++++--------- ProcessUtils.php | 2 +- Tests/ProcessTest.php | 8 ++++---- 12 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Exception/ProcessFailedException.php b/Exception/ProcessFailedException.php index 6cd8a355..de8a9e98 100644 --- a/Exception/ProcessFailedException.php +++ b/Exception/ProcessFailedException.php @@ -27,7 +27,7 @@ public function __construct( throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); } - $error = sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s", + $error = \sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText(), @@ -35,7 +35,7 @@ public function __construct( ); if (!$process->isOutputDisabled()) { - $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", + $error .= \sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput() ); diff --git a/Exception/ProcessSignaledException.php b/Exception/ProcessSignaledException.php index 4466b0d8..3fd13e5d 100644 --- a/Exception/ProcessSignaledException.php +++ b/Exception/ProcessSignaledException.php @@ -23,7 +23,7 @@ final class ProcessSignaledException extends RuntimeException public function __construct( private Process $process, ) { - parent::__construct(sprintf('The process has been signaled with signal "%s".', $process->getTermSignal())); + parent::__construct(\sprintf('The process has been signaled with signal "%s".', $process->getTermSignal())); } public function getProcess(): Process diff --git a/Exception/ProcessStartFailedException.php b/Exception/ProcessStartFailedException.php index 24bd5009..37254725 100644 --- a/Exception/ProcessStartFailedException.php +++ b/Exception/ProcessStartFailedException.php @@ -26,7 +26,7 @@ public function __construct( throw new InvalidArgumentException('Expected a process that failed during startup, but the given process was started successfully.'); } - $error = sprintf('The command "%s" failed.'."\n\nWorking directory: %s\n\nError: %s", + $error = \sprintf('The command "%s" failed.'."\n\nWorking directory: %s\n\nError: %s", $process->getCommandLine(), $process->getWorkingDirectory(), $message ?? 'unknown' diff --git a/Exception/ProcessTimedOutException.php b/Exception/ProcessTimedOutException.php index b692e35f..d3fe4934 100644 --- a/Exception/ProcessTimedOutException.php +++ b/Exception/ProcessTimedOutException.php @@ -27,7 +27,7 @@ public function __construct( private Process $process, private int $timeoutType, ) { - parent::__construct(sprintf( + parent::__construct(\sprintf( 'The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), $this->getExceededTimeout() @@ -54,7 +54,7 @@ public function getExceededTimeout(): ?float return match ($this->timeoutType) { self::TYPE_GENERAL => $this->process->getTimeout(), self::TYPE_IDLE => $this->process->getIdleTimeout(), - default => throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)), + default => throw new \LogicException(\sprintf('Unknown timeout type "%d".', $this->timeoutType)), }; } } diff --git a/InputStream.php b/InputStream.php index cd91029e..586e7429 100644 --- a/InputStream.php +++ b/InputStream.php @@ -46,7 +46,7 @@ public function write(mixed $input): void return; } if ($this->isClosed()) { - throw new RuntimeException(sprintf('"%s" is closed.', static::class)); + throw new RuntimeException(\sprintf('"%s" is closed.', static::class)); } $this->input[] = ProcessUtils::validateInput(__METHOD__, $input); } diff --git a/PhpProcess.php b/PhpProcess.php index 01d88954..0e7ff846 100644 --- a/PhpProcess.php +++ b/PhpProcess.php @@ -52,7 +52,7 @@ public function __construct(string $script, ?string $cwd = null, ?array $env = n public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static { - throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); + throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } public function start(?callable $callback = null, array $env = []): void diff --git a/PhpSubprocess.php b/PhpSubprocess.php index a97f8b26..38a1864f 100644 --- a/PhpSubprocess.php +++ b/PhpSubprocess.php @@ -75,7 +75,7 @@ public function __construct(array $command, ?string $cwd = null, ?array $env = n public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static { - throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); + throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } public function start(?callable $callback = null, array $env = []): void diff --git a/Pipes/AbstractPipes.php b/Pipes/AbstractPipes.php index cbbb7277..51a566f3 100644 --- a/Pipes/AbstractPipes.php +++ b/Pipes/AbstractPipes.php @@ -101,7 +101,7 @@ protected function write(): ?array } elseif (!isset($this->inputBuffer[0])) { if (!\is_string($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))); + 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; } diff --git a/Pipes/WindowsPipes.php b/Pipes/WindowsPipes.php index 26fa7498..116b8e30 100644 --- a/Pipes/WindowsPipes.php +++ b/Pipes/WindowsPipes.php @@ -52,7 +52,7 @@ public function __construct( set_error_handler(function ($type, $msg) use (&$lastError) { $lastError = $msg; }); for ($i = 0;; ++$i) { foreach ($pipes as $pipe => $name) { - $file = sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name); + $file = \sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name); if (!$h = fopen($file.'.lock', 'w')) { if (file_exists($file.'.lock')) { diff --git a/Process.php b/Process.php index fd3ad875..ce00d9b4 100644 --- a/Process.php +++ b/Process.php @@ -338,7 +338,7 @@ public function start(?callable $callback = null, array $env = []): void } if (!is_dir($this->cwd)) { - throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd)); + throw new RuntimeException(\sprintf('The provided cwd "%s" does not exist.', $this->cwd)); } $lastError = null; @@ -1215,7 +1215,7 @@ public function setOptions(array $options): void foreach ($options as $key => $value) { if (!\in_array($key, $existingOptions)) { $this->options = $defaultOptions; - throw new LogicException(sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions))); + throw new LogicException(\sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions))); } $this->options[$key] = $value; } @@ -1498,10 +1498,10 @@ private function doSignal(int $signal, bool $throwException): bool } if ('\\' === \DIRECTORY_SEPARATOR) { - exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); + exec(\sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); if ($exitCode && $this->isRunning()) { if ($throwException) { - throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output))); + throw new RuntimeException(\sprintf('Unable to kill the process (%s).', implode(' ', $output))); } return false; @@ -1511,12 +1511,12 @@ private function doSignal(int $signal, bool $throwException): bool $ok = @proc_terminate($this->process, $signal); } elseif (\function_exists('posix_kill')) { $ok = @posix_kill($pid, $signal); - } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) { + } elseif ($ok = proc_open(\sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) { $ok = false === fgets($pipes[2]); } if (!$ok) { if ($throwException) { - throw new RuntimeException(sprintf('Error while sending signal "%s".', $signal)); + throw new RuntimeException(\sprintf('Error while sending signal "%s".', $signal)); } return false; @@ -1595,7 +1595,7 @@ function ($m) use (&$env, $uid) { private function requireProcessIsStarted(string $functionName): void { if (!$this->isStarted()) { - throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName)); + throw new LogicException(\sprintf('Process must be started before calling "%s()".', $functionName)); } } @@ -1607,7 +1607,7 @@ private function requireProcessIsStarted(string $functionName): void private function requireProcessIsTerminated(string $functionName): void { if (!$this->isTerminated()) { - throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName)); + throw new LogicException(\sprintf('Process must be terminated before calling "%s()".', $functionName)); } } @@ -1637,7 +1637,7 @@ private function replacePlaceholders(string $commandline, array $env): string { return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) { if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) { - throw new InvalidArgumentException(sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline); + throw new InvalidArgumentException(\sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline); } return $this->escapeArgument($env[$matches[1]]); diff --git a/ProcessUtils.php b/ProcessUtils.php index 092c5ccf..a2dbde9f 100644 --- a/ProcessUtils.php +++ b/ProcessUtils.php @@ -56,7 +56,7 @@ public static function validateInput(string $caller, mixed $input): mixed return new \IteratorIterator($input); } - throw new InvalidArgumentException(sprintf('"%s" only accepts strings, Traversable objects or stream resources.', $caller)); + throw new InvalidArgumentException(\sprintf('"%s" only accepts strings, Traversable objects or stream resources.', $caller)); } return $input; diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 653fa6d8..cd084872 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -191,7 +191,7 @@ public function testAllOutputIsActuallyReadOnTermination() // another byte which will never be read. $expectedOutputSize = PipesInterface::CHUNK_SIZE * 2 + 2; - $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize); + $code = \sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize); $p = $this->getProcessForCode($code); $p->start(); @@ -384,7 +384,7 @@ public static function chainedCommandsOutputProvider() */ public function testChainedCommandsOutput($expected, $operator, $input) { - $process = $this->getProcess(sprintf('echo %s %s echo %s', $input, $operator, $input)); + $process = $this->getProcess(\sprintf('echo %s %s echo %s', $input, $operator, $input)); $process->run(); $this->assertEquals($expected, $process->getOutput()); } @@ -992,7 +992,7 @@ public function testMethodsThatNeedARunningProcess($method) $process = $this->getProcess('foo'); $this->expectException(LogicException::class); - $this->expectExceptionMessage(sprintf('Process must be started before calling "%s()".', $method)); + $this->expectExceptionMessage(\sprintf('Process must be started before calling "%s()".', $method)); $process->{$method}(); } @@ -1492,7 +1492,7 @@ public function testEscapeArgument($arg) public function testRawCommandLine() { - $p = Process::fromShellCommandline(sprintf('"%s" -r %s "a" "" "b"', self::$phpBin, escapeshellarg('print_r($argv);'))); + $p = Process::fromShellCommandline(\sprintf('"%s" -r %s "a" "" "b"', self::$phpBin, escapeshellarg('print_r($argv);'))); $p->run(); $expected = "Array\n(\n [0] => -\n [1] => a\n [2] => \n [3] => b\n)\n"; From ca79b6e26d679b30022f9215319d4ea8b95ef5fe Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 27 Jun 2024 09:29:05 +0200 Subject: [PATCH 60/73] [Lock][Process] Replace `strtok` calls --- ExecutableFinder.php | 8 +++++++- PhpExecutableFinder.php | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 9ab99606..dca238de 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -79,8 +79,14 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ } } + if (!\function_exists('exec')) { + return $default; + } + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && @is_executable($executablePath)) { + $execResult = @exec($command.' '.escapeshellarg($name)); + + if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) { return $executablePath; } diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index 4a882e0f..fb2f3716 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -33,8 +33,13 @@ public function find(bool $includeArgs = true): string|false { if ($php = getenv('PHP_BINARY')) { if (!is_executable($php)) { + if (!\function_exists('exec')) { + return false; + } + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if (\function_exists('exec') && $php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { + $execResult = exec($command.' '.escapeshellarg($php)); + if ($php = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) { if (!is_executable($php)) { return false; } From 5c9cf89df869bb3522744a682272126134d3ea48 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 6 Jul 2024 09:57:16 +0200 Subject: [PATCH 61/73] Update .gitattributes --- .gitattributes | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 From 7f2f542c668ad6c313dc4a5e9c3321f733197eca Mon Sep 17 00:00:00 2001 From: Thibaut THOUEMENT Date: Thu, 18 Jul 2024 11:59:33 +0200 Subject: [PATCH 62/73] Fix ProcessTest - testIgnoringSignal for local --- Tests/ProcessTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 653fa6d8..005b9175 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1669,7 +1669,7 @@ public function testIgnoringSignal() $this->markTestSkipped('pnctl extension is required.'); } - $process = $this->getProcess('sleep 10'); + $process = $this->getProcess(['sleep', '10']); $process->setIgnoredSignals([\SIGTERM]); $process->start(); @@ -1685,7 +1685,7 @@ public function testNotIgnoringSignal() $this->markTestSkipped('pnctl extension is required.'); } - $process = $this->getProcess('sleep 10'); + $process = $this->getProcess(['sleep', '10']); $process->start(); $process->stop(timeout: 0.2); From bb0a8b7772610211c2cd7d6e4e36acfcbadcb613 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 28 Jul 2024 13:18:19 +0200 Subject: [PATCH 63/73] replace uniqid() with random_bytes() to create identifiers --- Process.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Process.php b/Process.php index ce00d9b4..f81d1922 100644 --- a/Process.php +++ b/Process.php @@ -1543,7 +1543,7 @@ private function buildShellCommandline(string|array $commandline): string private function prepareWindowsCommandLine(string|array $cmd, array &$env): string { $cmd = $this->buildShellCommandline($cmd); - $uid = uniqid('', true); + $uid = bin2hex(random_bytes(4)); $cmd = preg_replace_callback( '/"(?:( [^"%!^]*+ From 82d962eed80966a41220bd28424ba6a71d3f357d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Auswo=CC=88ger?= Date: Thu, 5 Sep 2024 23:34:25 +0200 Subject: [PATCH 64/73] [Process] Fix backwards compatibility for invalid commands --- Process.php | 5 +++++ Tests/ProcessTest.php | 9 ++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Process.php b/Process.php index fd3ad875..878296b6 100644 --- a/Process.php +++ b/Process.php @@ -358,6 +358,11 @@ public function start(?callable $callback = null, array $env = []): void try { $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + + // Ensure array vs string commands behave the same + if (!$process && \is_array($commandline)) { + $process = @proc_open('exec '.$this->buildShellCommandline($commandline), $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + } } finally { if ($this->ignoredSignals && \function_exists('pcntl_sigprocmask')) { // we restore the signal mask here to avoid any side effects diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 005b9175..3b0533b7 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -72,13 +72,8 @@ public function testInvalidCwd() */ public function testInvalidCommand(Process $process) { - try { - $this->assertSame('\\' === \DIRECTORY_SEPARATOR ? 1 : 127, $process->run()); - } catch (ProcessStartFailedException $e) { - // An invalid command might already fail during start since PHP 8.3 for platforms - // supporting posix_spawn(), see https://github.com/php/php-src/issues/12589 - $this->assertStringContainsString('No such file or directory', $e->getMessage()); - } + // An invalid command should not fail during start + $this->assertSame('\\' === \DIRECTORY_SEPARATOR ? 1 : 127, $process->run()); } public function invalidProcessProvider() From 6fd79ab51c8342aa1f3395b2e704d3aa6ac68c2c Mon Sep 17 00:00:00 2001 From: Marcus <25648755+M-arcus@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:09:10 +0200 Subject: [PATCH 65/73] PhpSubprocess: Add flag PREG_OFFSET_CAPTURE to preg_match to identify the offset --- PhpSubprocess.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PhpSubprocess.php b/PhpSubprocess.php index a97f8b26..04fd8ea8 100644 --- a/PhpSubprocess.php +++ b/PhpSubprocess.php @@ -106,7 +106,7 @@ private function writeTmpIni(array $iniFiles, string $tmpDir): string throw new RuntimeException('Unable to read ini: '.$file); } // Check and remove directives after HOST and PATH sections - if (preg_match('/^\s*\[(?:PATH|HOST)\s*=/mi', $data, $matches)) { + if (preg_match('/^\s*\[(?:PATH|HOST)\s*=/mi', $data, $matches, \PREG_OFFSET_CAPTURE)) { $data = substr($data, 0, $matches[0][1]); } From 85dc2723935920dabcb8bc553e882174c4a6b344 Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Fri, 13 Sep 2024 16:26:53 +0200 Subject: [PATCH 66/73] [Process] Add Laravel Herd php detection path --- PhpExecutableFinder.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index fb2f3716..b740231a 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -86,6 +86,10 @@ public function find(bool $includeArgs = true): string|false $dirs[] = 'C:\xampp\php\\'; } + if ($herdPath = getenv('HERD_HOME')) { + $dirs[] = $herdPath.\DIRECTORY_SEPARATOR.'bin'; + } + return $this->executableFinder->find('php', false, $dirs); } From 5c03ee6369281177f07f7c68252a280beccba847 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 19 Sep 2024 23:14:15 +0200 Subject: [PATCH 67/73] Make more data providers static --- Tests/ProcessTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 3b0533b7..a639f058 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -76,7 +76,7 @@ public function testInvalidCommand(Process $process) $this->assertSame('\\' === \DIRECTORY_SEPARATOR ? 1 : 127, $process->run()); } - public function invalidProcessProvider() + public static function invalidProcessProvider(): array { return [ [new Process(['invalid'])], From 2ad775b9f17c8c9c1fe457750ce191e0f7c1fbff Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 26 Sep 2024 10:09:09 +0200 Subject: [PATCH 68/73] Remove unused imports --- Tests/ProcessTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 5532016b..290100e6 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -16,7 +16,6 @@ use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessSignaledException; -use Symfony\Component\Process\Exception\ProcessStartFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\InputStream; From 18f50a7e1ab5c0c574f820df5d7a77bf3cdd3f5a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 29 Oct 2024 21:50:35 +0100 Subject: [PATCH 69/73] [Process] On Windows, don't rely on the OS to find executables --- Process.php | 7 +++++++ Tests/ProcessFailedExceptionTest.php | 10 +++++----- Tests/ProcessTest.php | 11 +++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Process.php b/Process.php index 78b0fd35..b7d07571 100644 --- a/Process.php +++ b/Process.php @@ -85,6 +85,7 @@ class Process implements \IteratorAggregate private ?int $cachedExitCode = null; private static ?bool $sigchild = null; + private static array $executables = []; /** * Exit codes translation table. @@ -1543,6 +1544,12 @@ private function buildShellCommandline(string|array $commandline): string return $commandline; } + if ('\\' === \DIRECTORY_SEPARATOR && isset($commandline[0][0]) && \strlen($commandline[0]) === strcspn($commandline[0], ':/\\')) { + // On Windows, we don't rely on the OS to find the executable if possible to avoid lookups + // in the current directory which could be untrusted. Instead we use the ExecutableFinder. + $commandline[0] = (self::$executables[$commandline[0]] ??= (new ExecutableFinder())->find($commandline[0])) ?? $commandline[0]; + } + return implode(' ', array_map($this->escapeArgument(...), $commandline)); } diff --git a/Tests/ProcessFailedExceptionTest.php b/Tests/ProcessFailedExceptionTest.php index 259ffd63..d05beb85 100644 --- a/Tests/ProcessFailedExceptionTest.php +++ b/Tests/ProcessFailedExceptionTest.php @@ -80,8 +80,8 @@ public function testProcessFailedExceptionPopulatesInformationFromProcessOutput( $exception = new ProcessFailedException($process); - $this->assertEquals( - "The command \"$cmd\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}\n\nOutput:\n================\n{$output}\n\nError Output:\n================\n{$errorOutput}", + $this->assertStringMatchesFormat( + "The command \"%s\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}\n\nOutput:\n================\n{$output}\n\nError Output:\n================\n{$errorOutput}", str_replace("'php'", 'php', $exception->getMessage()) ); } @@ -126,9 +126,9 @@ public function testDisabledOutputInFailedExceptionDoesNotPopulateOutput() $exception = new ProcessFailedException($process); - $this->assertEquals( - "The command \"$cmd\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}", - str_replace("'php'", 'php', $exception->getMessage()) + $this->assertStringMatchesFormat( + "The command \"%s\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}", + $exception->getMessage() ); } } diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 290100e6..939b7166 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1687,6 +1687,17 @@ public function testNotIgnoringSignal() $this->assertSame(\SIGTERM, $process->getTermSignal()); } + public function testPathResolutionOnWindows() + { + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test is for Windows platform only'); + } + + $process = $this->getProcess(['where']); + + $this->assertSame('C:\\Windows\\system32\\where.EXE', $process->getCommandLine()); + } + private function getProcess(string|array $commandline, ?string $cwd = null, ?array $env = null, mixed $input = null, ?int $timeout = 60): Process { if (\is_string($commandline)) { From 7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 4 Feb 2025 11:18:48 +0100 Subject: [PATCH 70/73] [Process] Fix process status tracking --- Process.php | 18 +++--------------- Tests/ProcessTest.php | 3 +++ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/Process.php b/Process.php index 280a732d..6dfb25e7 100644 --- a/Process.php +++ b/Process.php @@ -80,7 +80,6 @@ class Process implements \IteratorAggregate private WindowsPipes|UnixPipes $processPipes; private ?int $latestSignal = null; - private ?int $cachedExitCode = null; private static ?bool $sigchild = null; @@ -1289,21 +1288,10 @@ protected function updateStatus(bool $blocking) return; } - $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; - } + if ($this->processInformation['running'] ?? true) { + $this->processInformation = proc_get_status($this->process); } + $running = $this->processInformation['running']; $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index eb0b2bcc..0f302c2a 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -711,6 +711,9 @@ public function testProcessIsSignaledIfStopped() if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('Windows does not support POSIX signals'); } + if (\PHP_VERSION_ID < 80300 && isset($_SERVER['GITHUB_ACTIONS'])) { + $this->markTestSkipped('Transient on GHA with PHP < 8.3'); + } $process = $this->getProcessForCode('sleep(32);'); $process->start(); From 9e3b9d4f5302fdd82e1b3e0f3a8542817a6f1d92 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 5 Feb 2025 09:21:03 +0100 Subject: [PATCH 71/73] skip transient test on GitHub Actions --- Tests/ProcessTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 06b2e803..8f9f131d 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -710,6 +710,9 @@ public function testProcessIsNotSignaled() if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('Windows does not support POSIX signals'); } + if (\PHP_VERSION_ID < 80300 && isset($_SERVER['GITHUB_ACTIONS'])) { + $this->markTestSkipped('Transient on GHA with PHP < 8.3'); + } $process = $this->getProcess('echo foo'); $process->run(); From d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 5 Feb 2025 09:21:03 +0100 Subject: [PATCH 72/73] skip transient test on GitHub Actions --- Tests/ProcessTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 8f9f131d..b17bfc7a 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -710,9 +710,6 @@ public function testProcessIsNotSignaled() if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('Windows does not support POSIX signals'); } - if (\PHP_VERSION_ID < 80300 && isset($_SERVER['GITHUB_ACTIONS'])) { - $this->markTestSkipped('Transient on GHA with PHP < 8.3'); - } $process = $this->getProcess('echo foo'); $process->run(); @@ -1689,6 +1686,9 @@ public function testNotIgnoringSignal() if (!\function_exists('pcntl_signal')) { $this->markTestSkipped('pnctl extension is required.'); } + if (\PHP_VERSION_ID < 80300 && isset($_SERVER['GITHUB_ACTIONS'])) { + $this->markTestSkipped('Transient on GHA with PHP < 8.3'); + } $process = $this->getProcess(['sleep', '10']); From e2a61c16af36c9a07e5c9906498b73e091949a20 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Mon, 10 Mar 2025 17:34:14 +0100 Subject: [PATCH 73/73] fix(process): use a pipe for stderr in pty mode to avoid mixed output between stdout and stderr --- Pipes/UnixPipes.php | 2 +- Tests/ProcessTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Pipes/UnixPipes.php b/Pipes/UnixPipes.php index 7bd0db0e..a0e48dd3 100644 --- a/Pipes/UnixPipes.php +++ b/Pipes/UnixPipes.php @@ -74,7 +74,7 @@ public function getDescriptors(): array return [ ['pty'], ['pty'], - ['pty'], + ['pipe', 'w'], // stderr needs to be in a pipe to correctly split error and output, since PHP will use the same stream for both ]; } diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 0f302c2a..e9c7527c 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -540,6 +540,20 @@ public function testExitCodeTextIsNullWhenExitCodeIsNull() $this->assertNull($process->getExitCodeText()); } + public function testStderrNotMixedWithStdout() + { + if (!Process::isPtySupported()) { + $this->markTestSkipped('PTY is not supported on this operating system.'); + } + + $process = $this->getProcess('echo "foo" && echo "bar" >&2'); + $process->setPty(true); + $process->run(); + + $this->assertSame("foo\r\n", $process->getOutput()); + $this->assertSame("bar\n", $process->getErrorOutput()); + } + public function testPTYCommand() { if (!Process::isPtySupported()) {