From 21b4d0c1c5074e04c436f4fe3f84403c58d67f46 Mon Sep 17 00:00:00 2001 From: tigitz Date: Sun, 1 Jan 2023 19:45:34 +0100 Subject: [PATCH 01/95] 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 01e6198dc55bf4862856e16490a91a9c860c1762 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Mon, 13 Feb 2023 00:00:11 +0100 Subject: [PATCH 02/95] 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 03/95] 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 04/95] 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 4b850da0cc3a2a9181c1ed407adbca4733dc839b Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 6 Mar 2023 21:48:01 +0100 Subject: [PATCH 05/95] [Tests] Replace `setMethods()` by `onlyMethods()` and `addMethods()` --- Tests/ProcessFailedExceptionTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ProcessFailedExceptionTest.php b/Tests/ProcessFailedExceptionTest.php index d6d7bfb0..259ffd63 100644 --- a/Tests/ProcessFailedExceptionTest.php +++ b/Tests/ProcessFailedExceptionTest.php @@ -25,7 +25,7 @@ class ProcessFailedExceptionTest extends TestCase */ public function testProcessFailedExceptionThrowsException() { - $process = $this->getMockBuilder(Process::class)->setMethods(['isSuccessful'])->setConstructorArgs([['php']])->getMock(); + $process = $this->getMockBuilder(Process::class)->onlyMethods(['isSuccessful'])->setConstructorArgs([['php']])->getMock(); $process->expects($this->once()) ->method('isSuccessful') ->willReturn(true); @@ -49,7 +49,7 @@ public function testProcessFailedExceptionPopulatesInformationFromProcessOutput( $errorOutput = 'FATAL: Unexpected error'; $workingDirectory = getcwd(); - $process = $this->getMockBuilder(Process::class)->setMethods(['isSuccessful', 'getOutput', 'getErrorOutput', 'getExitCode', 'getExitCodeText', 'isOutputDisabled', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock(); + $process = $this->getMockBuilder(Process::class)->onlyMethods(['isSuccessful', 'getOutput', 'getErrorOutput', 'getExitCode', 'getExitCodeText', 'isOutputDisabled', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock(); $process->expects($this->once()) ->method('isSuccessful') ->willReturn(false); @@ -97,7 +97,7 @@ public function testDisabledOutputInFailedExceptionDoesNotPopulateOutput() $exitText = 'General error'; $workingDirectory = getcwd(); - $process = $this->getMockBuilder(Process::class)->setMethods(['isSuccessful', 'isOutputDisabled', 'getExitCode', 'getExitCodeText', 'getOutput', 'getErrorOutput', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock(); + $process = $this->getMockBuilder(Process::class)->onlyMethods(['isSuccessful', 'isOutputDisabled', 'getExitCode', 'getExitCodeText', 'getOutput', 'getErrorOutput', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock(); $process->expects($this->once()) ->method('isSuccessful') ->willReturn(false); From 4d13cb8981e6509d1adfcf9d2019a86b631d2e2c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 21 Mar 2023 22:03:43 +0100 Subject: [PATCH 06/95] 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 07/95] 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 4b842fc4b61609e0a155a114082bd94e31e98287 Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 13 Feb 2023 17:14:55 +0400 Subject: [PATCH 08/95] for #49320 --- PhpExecutableFinder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index 998808b6..bed6c3dc 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -56,7 +56,7 @@ public function find(bool $includeArgs = true) $args = $includeArgs && $args ? ' '.implode(' ', $args) : ''; // PHP_BINARY return the current sapi executable - if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cgi-fcgi', 'cli', 'cli-server', 'phpdbg'], true)) { + if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cli', 'cli-server', 'phpdbg'], true)) { return \PHP_BINARY.$args; } From 39cbf15e9e1c1742a556de19fbd5191944c97668 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 22 Apr 2023 23:52:28 +0200 Subject: [PATCH 09/95] 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 10/95] 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 e3c46cc5689c8782944274bb30702106ecbe3b64 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Wed, 17 May 2023 13:26:05 +0200 Subject: [PATCH 11/95] [Process] Stop the process correctly even if underlying input stream is not closed: While checking a process to end, on posix system, process component only checks if pipes are still open, this fix ensure that if the process is terminated it correctly return, even if the underlying pipe is not closed. It can be useful when using \STDIN as a input stream as it will always be open --- Process.php | 2 +- Tests/ProcessTest.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Process.php b/Process.php index b47ecca1..9b19475a 100644 --- a/Process.php +++ b/Process.php @@ -428,7 +428,7 @@ public function wait(callable $callback = null) do { $this->checkTimeout(); - $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); + $running = $this->isRunning() && ('\\' === \DIRECTORY_SEPARATOR || $this->processPipes->areOpen()); $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); } while ($running); diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 790167fc..36acf02a 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1538,6 +1538,16 @@ public function testEnvCaseInsensitiveOnWindows() } } + public function testNotTerminableInputPipe() + { + $process = $this->getProcess('echo foo'); + $process->setInput(\STDIN); + $process->start(); + $process->setTimeout(2); + $process->wait(); + $this->assertFalse($process->isRunning()); + } + /** * @param string|array $commandline * @param mixed $input From ed6595d58305b67686251b47713e40d48abb47e8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 23 May 2023 17:24:39 +0200 Subject: [PATCH 12/95] [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 13/95] 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 86ca4c74afe24bc1889bc45a20adbd47c3131c08 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 14/95] 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 15/95] 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 16/95] [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 1a44dc377ec86a50fab40d066cd061e28a6b482f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 12 Jul 2023 13:45:09 +0200 Subject: [PATCH 17/95] [PhpUnitBridge] Kill the last concurrent process when it stales for more than 60s --- Tests/ProcessTest.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 36acf02a..6e6ee8a4 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -66,11 +66,11 @@ public function testInvalidCwd() $cmd->run(); } + /** + * @group transient-on-windows + */ public function testThatProcessDoesNotThrowWarningDuringRun() { - if ('\\' === \DIRECTORY_SEPARATOR) { - $this->markTestSkipped('This test is transient on Windows'); - } @trigger_error('Test Error', \E_USER_NOTICE); $process = $this->getProcessForCode('sleep(3)'); $process->run(); @@ -130,12 +130,11 @@ public function testStopWithTimeoutIsActuallyWorking() $this->assertLessThan(15, microtime(true) - $start); } + /** + * @group transient-on-windows + */ public function testWaitUntilSpecificOutput() { - if ('\\' === \DIRECTORY_SEPARATOR) { - $this->markTestIncomplete('This test is too transient on Windows, help wanted to improve it'); - } - $p = $this->getProcess([self::$phpBin, __DIR__.'/KillableProcessWithOutput.php']); $p->start(); @@ -1538,6 +1537,9 @@ public function testEnvCaseInsensitiveOnWindows() } } + /** + * @group transient-on-windows + */ public function testNotTerminableInputPipe() { $process = $this->getProcess('echo foo'); From 1132f6e0988f4872da081e7df2105729981f6ee8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 21 Jul 2023 15:28:24 +0200 Subject: [PATCH 18/95] 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 19/95] 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 20/95] 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 21/95] 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 22/95] [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 23/95] [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 ce9850f4eafd1181427f005a8b238d943878e53c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 1 Aug 2023 15:39:00 +0200 Subject: [PATCH 24/95] [Process] Fix test case --- Tests/ErrorProcessInitiator.php | 4 ++-- Tests/ProcessTest.php | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/ErrorProcessInitiator.php b/Tests/ErrorProcessInitiator.php index 4c8556ac..54168022 100644 --- a/Tests/ErrorProcessInitiator.php +++ b/Tests/ErrorProcessInitiator.php @@ -14,12 +14,12 @@ use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Process; -require \dirname(__DIR__).'/vendor/autoload.php'; +require is_file(\dirname(__DIR__).'/vendor/autoload.php') ? \dirname(__DIR__).'/vendor/autoload.php' : \dirname(__DIR__, 5).'/vendor/autoload.php'; ['e' => $php] = getopt('e:') + ['e' => 'php']; try { - $process = new Process("exec $php -r \"echo 'ready'; trigger_error('error', E_USER_ERROR);\""); + $process = new Process([$php, '-r', "echo 'ready'; trigger_error('error', E_USER_ERROR);"]); $process->start(); $process->setTimeout(0.5); while (!str_contains($process->getOutput(), 'ready')) { diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 6e6ee8a4..827c7239 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1523,6 +1523,10 @@ public function testWaitStoppedDeadProcess() $process->setTimeout(2); $process->wait(); $this->assertFalse($process->isRunning()); + + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->assertSame(0, $process->getExitCode()); + } } public function testEnvCaseInsensitiveOnWindows() From 7dea4fddc00bd9e23f494927caf0af73f2becd8f Mon Sep 17 00:00:00 2001 From: Yanick Witschi Date: Mon, 5 Dec 2022 15:48:16 +0100 Subject: [PATCH 25/95] 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 26/95] [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 27/95] [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 45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 7 Aug 2023 11:52:08 +0200 Subject: [PATCH 28/95] [Process] Fix silencing `wait` when using a sigchild-enabled binary --- Process.php | 2 +- Tests/ProcessTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Process.php b/Process.php index 9b19475a..30ebeb6b 100644 --- a/Process.php +++ b/Process.php @@ -331,7 +331,7 @@ public function start(callable $callback = null, array $env = []) // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; - $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code'; + $commandline .= 'pid=$!; echo $pid >&3; wait $pid 2>/dev/null; code=$?; echo $code >&3; exit $code'; // Workaround for the bug, when PTS functionality is enabled. // @see : https://bugs.php.net/69442 diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 827c7239..80493799 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1524,7 +1524,7 @@ public function testWaitStoppedDeadProcess() $process->wait(); $this->assertFalse($process->isRunning()); - if ('\\' !== \DIRECTORY_SEPARATOR) { + if ('\\' !== \DIRECTORY_SEPARATOR && !\Closure::bind(function () { return $this->isSigchildEnabled(); }, $process, $process)()) { $this->assertSame(0, $process->getExitCode()); } } From 83264b6a59a114522e4107cc249172aaa78b9720 Mon Sep 17 00:00:00 2001 From: Uladzimir Tsykun Date: Mon, 11 Sep 2023 18:39:34 +0200 Subject: [PATCH 29/95] 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 30/95] 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 31/95] [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 32/95] [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 33/95] 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 34/95] [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 35/95] 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 36/95] 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 37/95] [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 8fa22178dfc368911dbd513b431cd9b06f9afe7a Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 2 Dec 2023 09:38:30 +0100 Subject: [PATCH 38/95] always pass microseconds to usleep as integers --- Pipes/WindowsPipes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipes/WindowsPipes.php b/Pipes/WindowsPipes.php index bca84f57..968dd026 100644 --- a/Pipes/WindowsPipes.php +++ b/Pipes/WindowsPipes.php @@ -149,7 +149,7 @@ public function readAndWrite(bool $blocking, bool $close = false): array if ($w) { @stream_select($r, $w, $e, 0, Process::TIMEOUT_PRECISION * 1E6); } elseif ($this->fileHandles) { - usleep(Process::TIMEOUT_PRECISION * 1E6); + usleep((int) (Process::TIMEOUT_PRECISION * 1E6)); } } foreach ($this->fileHandles as $type => $fileHandle) { From c4b1ef0bc80533d87a2e969806172f1c2a980241 Mon Sep 17 00:00:00 2001 From: Kieran Date: Fri, 22 Dec 2023 16:42:54 +0000 Subject: [PATCH 39/95] 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 40/95] [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 9a2172b463c6b56dd2b44a4f664169950fe29048 Mon Sep 17 00:00:00 2001 From: Kay Wei Date: Wed, 10 Jan 2024 15:19:56 +0800 Subject: [PATCH 41/95] [Process] Fix executable finder when the command starts with a dash --- PhpExecutableFinder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index bed6c3dc..45dbcca4 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -35,7 +35,7 @@ public function find(bool $includeArgs = true) { if ($php = getenv('PHP_BINARY')) { if (!is_executable($php)) { - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v'; + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; if ($php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { if (!is_executable($php)) { return false; From 175925109fa5e76a0e6c283e3a3c9ffc0dd7968a Mon Sep 17 00:00:00 2001 From: Cornel Cruceru Date: Sat, 13 Jan 2024 01:03:15 +0200 Subject: [PATCH 42/95] [Process] Fixed inconsistent test Sometimes the process no longer appears to be running when the signal is sent which causes a LogicException to be thrown. This doesn't appear to be consistent and I can reproduce it randomly on my local machine. To avoid having tests fail at random I decided that it's better to send the signal only if the process is still marked as running. --- Tests/ErrorProcessInitiator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ErrorProcessInitiator.php b/Tests/ErrorProcessInitiator.php index 54168022..0b75add6 100644 --- a/Tests/ErrorProcessInitiator.php +++ b/Tests/ErrorProcessInitiator.php @@ -25,7 +25,7 @@ while (!str_contains($process->getOutput(), 'ready')) { usleep(1000); } - $process->signal(\SIGSTOP); + $process->isRunning() && $process->signal(\SIGSTOP); $process->wait(); return $process->getExitCode(); From 80826a9791754c93e06a80a348f66837aa5d0d1d Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Mon, 15 Jan 2024 20:49:54 +0100 Subject: [PATCH 43/95] 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 cbc28e34015ad50166fc2f9c8962d28d0fe861eb Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 23 Jan 2024 14:51:25 +0100 Subject: [PATCH 44/95] Apply php-cs-fixer fix --rules nullable_type_declaration_for_default_null_value --- ExecutableFinder.php | 2 +- InputStream.php | 2 +- PhpProcess.php | 6 +++--- Process.php | 18 +++++++++--------- Tests/ProcessTest.php | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index eb8f0629..f392c962 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -46,7 +46,7 @@ public function addSuffix(string $suffix) * * @return string|null */ - public function find(string $name, string $default = null, array $extraDirs = []) + public function find(string $name, ?string $default = null, array $extraDirs = []) { if (\ini_get('open_basedir')) { $searchPath = array_merge(explode(\PATH_SEPARATOR, \ini_get('open_basedir')), $extraDirs); diff --git a/InputStream.php b/InputStream.php index 240665f3..0c45b524 100644 --- a/InputStream.php +++ b/InputStream.php @@ -30,7 +30,7 @@ class InputStream implements \IteratorAggregate /** * Sets a callback that is called when the write buffer becomes empty. */ - public function onEmpty(callable $onEmpty = null) + public function onEmpty(?callable $onEmpty = null) { $this->onEmpty = $onEmpty; } diff --git a/PhpProcess.php b/PhpProcess.php index 2bc338e5..3a1d147c 100644 --- a/PhpProcess.php +++ b/PhpProcess.php @@ -32,7 +32,7 @@ class PhpProcess extends Process * @param int $timeout The timeout in seconds * @param array|null $php Path to the PHP binary to use with any additional arguments */ - public function __construct(string $script, string $cwd = null, array $env = null, int $timeout = 60, array $php = null) + public function __construct(string $script, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null) { if (null === $php) { $executableFinder = new PhpExecutableFinder(); @@ -53,7 +53,7 @@ public function __construct(string $script, string $cwd = null, array $env = nul /** * {@inheritdoc} */ - public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60) + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60) { throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } @@ -61,7 +61,7 @@ public static function fromShellCommandline(string $command, string $cwd = null, /** * {@inheritdoc} */ - public function start(callable $callback = null, array $env = []) + public function start(?callable $callback = null, array $env = []) { if (null === $this->getCommandLine()) { throw new RuntimeException('Unable to find the PHP executable.'); diff --git a/Process.php b/Process.php index 30ebeb6b..2b6ed9ef 100644 --- a/Process.php +++ b/Process.php @@ -140,7 +140,7 @@ class Process implements \IteratorAggregate * * @throws LogicException When proc_open is not installed */ - public function __construct(array $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60) + public function __construct(array $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60) { if (!\function_exists('proc_open')) { throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.'); @@ -189,7 +189,7 @@ public function __construct(array $command, string $cwd = null, array $env = nul * * @throws LogicException When proc_open is not installed */ - public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60) + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60) { $process = new static([], $cwd, $env, $input, $timeout); $process->commandline = $command; @@ -247,7 +247,7 @@ public function __clone() * * @final */ - public function run(callable $callback = null, array $env = []): int + public function run(?callable $callback = null, array $env = []): int { $this->start($callback, $env); @@ -266,7 +266,7 @@ public function run(callable $callback = null, array $env = []): int * * @final */ - public function mustRun(callable $callback = null, array $env = []): self + public function mustRun(?callable $callback = null, array $env = []): self { if (0 !== $this->run($callback, $env)) { throw new ProcessFailedException($this); @@ -294,7 +294,7 @@ public function mustRun(callable $callback = null, array $env = []): self * @throws RuntimeException When process is already running * @throws LogicException In case a callback is provided and output has been disabled */ - public function start(callable $callback = null, array $env = []) + public function start(?callable $callback = null, array $env = []) { if ($this->isRunning()) { throw new RuntimeException('Process is already running.'); @@ -385,7 +385,7 @@ public function start(callable $callback = null, array $env = []) * * @final */ - public function restart(callable $callback = null, array $env = []): self + public function restart(?callable $callback = null, array $env = []): self { if ($this->isRunning()) { throw new RuntimeException('Process is already running.'); @@ -412,7 +412,7 @@ public function restart(callable $callback = null, array $env = []): self * @throws ProcessSignaledException When process stopped after receiving signal * @throws LogicException When process is not yet started */ - public function wait(callable $callback = null) + public function wait(?callable $callback = null) { $this->requireProcessIsStarted(__FUNCTION__); @@ -914,7 +914,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) { $timeoutMicro = microtime(true) + $timeout; if ($this->isRunning()) { @@ -1310,7 +1310,7 @@ private function getDescriptors(): array * * @return \Closure */ - protected function buildCallback(callable $callback = null) + protected function buildCallback(?callable $callback = null) { if ($this->outputDisabled) { return function ($type, $data) use ($callback): bool { diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 80493799..daf842e1 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1558,7 +1558,7 @@ public function testNotTerminableInputPipe() * @param string|array $commandline * @param mixed $input */ - private function getProcess($commandline, string $cwd = null, array $env = null, $input = null, ?int $timeout = 60): Process + private function getProcess($commandline, ?string $cwd = null, ?array $env = null, $input = null, ?int $timeout = 60): Process { if (\is_string($commandline)) { $process = Process::fromShellCommandline($commandline, $cwd, $env, $input, $timeout); @@ -1573,7 +1573,7 @@ private function getProcess($commandline, string $cwd = null, array $env = null, return self::$process = $process; } - private function getProcessForCode(string $code, string $cwd = null, array $env = null, $input = null, ?int $timeout = 60): Process + private function getProcessForCode(string $code, ?string $cwd = null, ?array $env = null, $input = null, ?int $timeout = 60): Process { return $this->getProcess([self::$phpBin, '-r', $code], $cwd, $env, $input, $timeout); } From 7e2c857ee885cada866ae5c9a43613f002ec11c4 Mon Sep 17 00:00:00 2001 From: Lucas Bustamante Date: Wed, 7 Feb 2024 00:44:15 -0300 Subject: [PATCH 45/95] [Process] Fix Inconsistent Exit Status in proc_get_status for PHP Versions Below 8.3 --- Process.php | 14 +++++++++++ Tests/ProcessTest.php | 55 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/Process.php b/Process.php index 2b6ed9ef..a4b0a784 100644 --- a/Process.php +++ b/Process.php @@ -80,6 +80,7 @@ class Process implements \IteratorAggregate private $processPipes; private $latestSignal; + private $cachedExitCode; private static $sigchild; @@ -1345,6 +1346,19 @@ protected function updateStatus(bool $blocking) $this->processInformation = proc_get_status($this->process); $running = $this->processInformation['running']; + // In PHP < 8.3, "proc_get_status" only returns the correct exit status on the first call. + // Subsequent calls return -1 as the process is discarded. This workaround caches the first + // retrieved exit status for consistent results in later calls, mimicking PHP 8.3 behavior. + if (\PHP_VERSION_ID < 80300) { + if (!isset($this->cachedExitCode) && !$running && -1 !== $this->processInformation['exitcode']) { + $this->cachedExitCode = $this->processInformation['exitcode']; + } + + if (isset($this->cachedExitCode) && !$running && -1 === $this->processInformation['exitcode']) { + $this->processInformation['exitcode'] = $this->cachedExitCode; + } + } + $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); if ($this->fallbackStatus && $this->isSigchildEnabled()) { diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index daf842e1..059d59a4 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1541,6 +1541,60 @@ public function testEnvCaseInsensitiveOnWindows() } } + public function testMultipleCallsToProcGetStatus() + { + $process = $this->getProcess('echo foo'); + $process->start(static function () use ($process) { + return $process->isRunning(); + }); + while ($process->isRunning()) { + usleep(1000); + } + $this->assertSame(0, $process->getExitCode()); + } + + public function testFailingProcessWithMultipleCallsToProcGetStatus() + { + $process = $this->getProcess('exit 123'); + $process->start(static function () use ($process) { + return $process->isRunning(); + }); + while ($process->isRunning()) { + usleep(1000); + } + $this->assertSame(123, $process->getExitCode()); + } + + /** + * @group slow + */ + public function testLongRunningProcessWithMultipleCallsToProcGetStatus() + { + $process = $this->getProcess('php -r "sleep(1); echo \'done\';"'); + $process->start(static function () use ($process) { + return $process->isRunning(); + }); + while ($process->isRunning()) { + usleep(1000); + } + $this->assertSame(0, $process->getExitCode()); + } + + /** + * @group slow + */ + public function testLongRunningProcessWithMultipleCallsToProcGetStatusError() + { + $process = $this->getProcess('php -r "sleep(1); echo \'failure\'; exit(123);"'); + $process->start(static function () use ($process) { + return $process->isRunning(); + }); + while ($process->isRunning()) { + usleep(1000); + } + $this->assertSame(123, $process->getExitCode()); + } + /** * @group transient-on-windows */ @@ -1556,7 +1610,6 @@ public function testNotTerminableInputPipe() /** * @param string|array $commandline - * @param mixed $input */ private function getProcess($commandline, ?string $cwd = null, ?array $env = null, $input = null, ?int $timeout = 60): Process { From 4fdf34004f149cc20b2f51d7d119aa500caad975 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:55:24 +0100 Subject: [PATCH 46/95] [Process] Fix failing tests causing segfaults --- Tests/ProcessTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 059d59a4..a2e370de 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1570,7 +1570,7 @@ public function testFailingProcessWithMultipleCallsToProcGetStatus() */ public function testLongRunningProcessWithMultipleCallsToProcGetStatus() { - $process = $this->getProcess('php -r "sleep(1); echo \'done\';"'); + $process = $this->getProcess('sleep 1 && echo "done" && php -r "exit(0);"'); $process->start(static function () use ($process) { return $process->isRunning(); }); @@ -1585,7 +1585,7 @@ public function testLongRunningProcessWithMultipleCallsToProcGetStatus() */ public function testLongRunningProcessWithMultipleCallsToProcGetStatusError() { - $process = $this->getProcess('php -r "sleep(1); echo \'failure\'; exit(123);"'); + $process = $this->getProcess('sleep 1 && echo "failure" && php -r "exit(123);"'); $process->start(static function () use ($process) { return $process->isRunning(); }); From 710e27879e9be3395de2b98da3f52a946039f297 Mon Sep 17 00:00:00 2001 From: Kay Wei Date: Tue, 20 Feb 2024 16:24:14 +0800 Subject: [PATCH 47/95] 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 48/95] 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 49/95] 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 50/95] 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 85a554acd7c28522241faf2e97b9541247a0d3d5 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 18 Apr 2024 09:55:03 +0200 Subject: [PATCH 51/95] Auto-close PRs on subtree-splits --- .gitattributes | 3 +- .github/PULL_REQUEST_TEMPLATE.md | 8 +++++ .github/workflows/check-subtree-split.yml | 37 +++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/check-subtree-split.yml diff --git a/.gitattributes b/.gitattributes index 84c7add0..14c3c359 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4689c4da --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/.github/workflows/check-subtree-split.yml b/.github/workflows/check-subtree-split.yml new file mode 100644 index 00000000..16be48ba --- /dev/null +++ b/.github/workflows/check-subtree-split.yml @@ -0,0 +1,37 @@ +name: Check subtree split + +on: + pull_request_target: + +jobs: + close-pull-request: + runs-on: ubuntu-latest + + steps: + - name: Close pull request + uses: actions/github-script@v6 + with: + script: | + if (context.repo.owner === "symfony") { + github.rest.issues.createComment({ + owner: "symfony", + repo: context.repo.repo, + issue_number: context.issue.number, + body: ` + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! + ` + }); + + github.rest.pulls.update({ + owner: "symfony", + repo: context.repo.repo, + pull_number: context.issue.number, + state: "closed" + }); + } From b3da76c30c3f33c21356ceeb631f8958b2b932cd Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 29 Apr 2024 16:31:15 +0200 Subject: [PATCH 52/95] Remove calls to `TestCase::iniSet()` and calls to deprecated methods of `MockBuilder` --- Tests/ExecutableFinderTest.php | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 5c63cf0f..6d089def 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -109,12 +109,16 @@ public function testFindWithOpenBaseDir() $this->markTestSkipped('Cannot test when open_basedir is set'); } - $this->iniSet('open_basedir', \dirname(\PHP_BINARY).\PATH_SEPARATOR.'/'); + $initialOpenBaseDir = ini_set('open_basedir', \dirname(\PHP_BINARY).\PATH_SEPARATOR.'/'); - $finder = new ExecutableFinder(); - $result = $finder->find($this->getPhpBinaryName()); + try { + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName()); - $this->assertSamePath(\PHP_BINARY, $result); + $this->assertSamePath(\PHP_BINARY, $result); + } finally { + ini_set('open_basedir', $initialOpenBaseDir); + } } /** @@ -130,12 +134,17 @@ public function testFindProcessInOpenBasedir() } $this->setPath(''); - $this->iniSet('open_basedir', \PHP_BINARY.\PATH_SEPARATOR.'/'); - $finder = new ExecutableFinder(); - $result = $finder->find($this->getPhpBinaryName(), false); + $initialOpenBaseDir = ini_set('open_basedir', \PHP_BINARY.\PATH_SEPARATOR.'/'); - $this->assertSamePath(\PHP_BINARY, $result); + try { + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName(), false); + + $this->assertSamePath(\PHP_BINARY, $result); + } finally { + ini_set('open_basedir', $initialOpenBaseDir); + } } public function testFindBatchExecutableOnWindows() From 1393de6f0688c254d8e3bc4933670a6700c2c64b Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 7 May 2024 13:49:07 -0400 Subject: [PATCH 53/95] 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 54/95] 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 deedcb3bb4669cae2148bc920eafd2b16dc7c046 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 31 May 2024 16:33:22 +0200 Subject: [PATCH 55/95] Revert "minor #54653 Auto-close PRs on subtree-splits (nicolas-grekas)" This reverts commit 2c9352dd91ebaf37b8a3e3c26fd8e1306df2fb73, reversing changes made to 18c3e87f1512be2cc50e90235b144b13bc347258. --- .gitattributes | 3 +- .github/PULL_REQUEST_TEMPLATE.md | 8 ----- .github/workflows/check-subtree-split.yml | 37 ----------------------- 3 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/workflows/check-subtree-split.yml diff --git a/.gitattributes b/.gitattributes index 14c3c359..84c7add0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.git* export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 4689c4da..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,8 +0,0 @@ -Please do not submit any Pull Requests here. They will be closed. ---- - -Please submit your PR here instead: -https://github.com/symfony/symfony - -This repository is what we call a "subtree split": a read-only subset of that main repository. -We're looking forward to your PR there! diff --git a/.github/workflows/check-subtree-split.yml b/.github/workflows/check-subtree-split.yml deleted file mode 100644 index 16be48ba..00000000 --- a/.github/workflows/check-subtree-split.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Check subtree split - -on: - pull_request_target: - -jobs: - close-pull-request: - runs-on: ubuntu-latest - - steps: - - name: Close pull request - uses: actions/github-script@v6 - with: - script: | - if (context.repo.owner === "symfony") { - github.rest.issues.createComment({ - owner: "symfony", - repo: context.repo.repo, - issue_number: context.issue.number, - body: ` - Thanks for your Pull Request! We love contributions. - - However, you should instead open your PR on the main repository: - https://github.com/symfony/symfony - - This repository is what we call a "subtree split": a read-only subset of that main repository. - We're looking forward to your PR there! - ` - }); - - github.rest.pulls.update({ - owner: "symfony", - repo: context.repo.repo, - pull_number: context.issue.number, - state: "closed" - }); - } From f7456b8509dacf9f6ae0f4bc6857fa6d7059e77a Mon Sep 17 00:00:00 2001 From: Travis Carden Date: Tue, 21 Nov 2023 18:24:14 -0500 Subject: [PATCH 56/95] [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 57/95] 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 58/95] [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 59/95] 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 60/95] 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 61/95] 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 32354f62488486b6efcbcd61a1dc8a619287fd29 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 5 Sep 2024 18:13:22 +0200 Subject: [PATCH 62/95] Don't use is_resource() on non-streams --- Process.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Process.php b/Process.php index a4b0a784..62addf1e 100644 --- a/Process.php +++ b/Process.php @@ -352,7 +352,7 @@ public function start(?callable $callback = null, array $env = []) $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); - if (!\is_resource($this->process)) { + if (!$this->process) { throw new RuntimeException('Unable to launch a new process.'); } $this->status = self::STATUS_STARTED; @@ -1456,8 +1456,9 @@ private function readPipes(bool $blocking, bool $close) private function close(): int { $this->processPipes->close(); - if (\is_resource($this->process)) { + if ($this->process) { proc_close($this->process); + $this->process = null; } $this->exitcode = $this->processInformation['exitcode']; $this->status = self::STATUS_TERMINATED; 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 63/95] [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 64/95] 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 65/95] [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 6f16c626e9fcf3fc7ce9c79ef34432adcf792282 Mon Sep 17 00:00:00 2001 From: Jan Walther Date: Tue, 1 Aug 2023 16:37:55 +0200 Subject: [PATCH 66/95] [Process] Fix finding executables independently of open_basedir --- ExecutableFinder.php | 32 +++++++++------------- PhpExecutableFinder.php | 2 +- Tests/ExecutableFinderTest.php | 50 +++++----------------------------- 3 files changed, 21 insertions(+), 63 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index f392c962..a2f184d5 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -48,25 +48,10 @@ public function addSuffix(string $suffix) */ public function find(string $name, ?string $default = null, array $extraDirs = []) { - if (\ini_get('open_basedir')) { - $searchPath = array_merge(explode(\PATH_SEPARATOR, \ini_get('open_basedir')), $extraDirs); - $dirs = []; - foreach ($searchPath as $path) { - // Silencing against https://bugs.php.net/69240 - if (@is_dir($path)) { - $dirs[] = $path; - } else { - if (basename($path) == $name && @is_executable($path)) { - return $path; - } - } - } - } else { - $dirs = array_merge( - explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), - $extraDirs - ); - } + $dirs = array_merge( + explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), + $extraDirs + ); $suffixes = ['']; if ('\\' === \DIRECTORY_SEPARATOR) { @@ -78,9 +63,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 45dbcca4..54fe7443 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -36,7 +36,7 @@ public function find(bool $includeArgs = true) if ($php = getenv('PHP_BINARY')) { if (!is_executable($php)) { $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if ($php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { + if (\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 6d089def..a1b8d6d5 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -19,20 +19,9 @@ */ class ExecutableFinderTest extends TestCase { - private $path; - protected function tearDown(): void { - if ($this->path) { - // Restore path if it was changed. - putenv('PATH='.$this->path); - } - } - - private function setPath($path) - { - $this->path = getenv('PATH'); - putenv('PATH='.$path); + putenv('PATH='.($_SERVER['PATH'] ?? $_SERVER['Path'])); } public function testFind() @@ -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)]; @@ -109,6 +98,7 @@ public function testFindWithOpenBaseDir() $this->markTestSkipped('Cannot test when open_basedir is set'); } + putenv('PATH='.\dirname(\PHP_BINARY)); $initialOpenBaseDir = ini_set('open_basedir', \dirname(\PHP_BINARY).\PATH_SEPARATOR.'/'); try { @@ -121,32 +111,6 @@ public function testFindWithOpenBaseDir() } } - /** - * @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->setPath(''); - - $initialOpenBaseDir = ini_set('open_basedir', \PHP_BINARY.\PATH_SEPARATOR.'/'); - - try { - $finder = new ExecutableFinder(); - $result = $finder->find($this->getPhpBinaryName(), false); - - $this->assertSamePath(\PHP_BINARY, $result); - } finally { - ini_set('open_basedir', $initialOpenBaseDir); - } - } - public function testFindBatchExecutableOnWindows() { if (\ini_get('open_basedir')) { @@ -163,7 +127,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 1b9fa82b5c62cd49da8c9e3952dd8531ada65096 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 17 Sep 2024 14:46:43 +0200 Subject: [PATCH 67/95] [Process] minor fix --- ExecutableFinder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index a2f184d5..6dc00b7c 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -71,7 +71,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 5c03ee6369281177f07f7c68252a280beccba847 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 19 Sep 2024 23:14:15 +0200 Subject: [PATCH 68/95] 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 95f3f19d0f8f06e4253c66a0828ddb69f8b8ede4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 23 Sep 2024 11:24:18 +0200 Subject: [PATCH 69/95] Add PR template and auto-close PR on subtree split repositories --- .gitattributes | 3 +-- .github/PULL_REQUEST_TEMPLATE.md | 8 ++++++++ .github/workflows/close-pull-request.yml | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/close-pull-request.yml diff --git a/.gitattributes b/.gitattributes index 84c7add0..14c3c359 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4689c4da --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml new file mode 100644 index 00000000..e55b4781 --- /dev/null +++ b/.github/workflows/close-pull-request.yml @@ -0,0 +1,20 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: | + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! From 2ad775b9f17c8c9c1fe457750ce191e0f7c1fbff Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 26 Sep 2024 10:09:09 +0200 Subject: [PATCH 70/95] 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 e2d11b6ca03e3041ca2f53a4da3f16d2f8e45c5a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 29 Oct 2024 21:56:12 +0100 Subject: [PATCH 71/95] [Process] Fix handling empty path found in the PATH env var with ExecutableFinder --- ExecutableFinder.php | 3 +++ Tests/ExecutableFinderTest.php | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 6dc00b7c..45d91e4a 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -60,6 +60,9 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ } foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { + if ('' === $dir) { + $dir = '.'; + } if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { return $file; } diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index a1b8d6d5..c4876e47 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -111,6 +111,9 @@ public function testFindWithOpenBaseDir() } } + /** + * @runInSeparateProcess + */ public function testFindBatchExecutableOnWindows() { if (\ini_get('open_basedir')) { @@ -138,6 +141,24 @@ public function testFindBatchExecutableOnWindows() $this->assertSamePath($target.'.BAT', $result); } + /** + * @runInSeparateProcess + */ + public function testEmptyDirInPath() + { + putenv(sprintf('PATH=%s:', \dirname(\PHP_BINARY))); + + touch('executable'); + chmod('executable', 0700); + + $finder = new ExecutableFinder(); + $result = $finder->find('executable'); + + $this->assertSame('./executable', $result); + + unlink('executable'); + } + private function assertSamePath($expected, $tested) { if ('\\' === \DIRECTORY_SEPARATOR) { From 18f50a7e1ab5c0c574f820df5d7a77bf3cdd3f5a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 29 Oct 2024 21:50:35 +0100 Subject: [PATCH 72/95] [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 651830b1a3cbae1b58bc63c8ba75c5a735abe522 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 30 Oct 2024 22:56:41 +0100 Subject: [PATCH 73/95] [Process] Properly deal with not-found executables on Windows --- ExecutableFinder.php | 10 ++++++++-- PhpExecutableFinder.php | 16 ++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 6dc00b7c..d446bb65 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -70,8 +70,14 @@ 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') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { + return $default; + } + + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where %s 2> NUL' : 'command -v -- %s'; + $execResult = exec(\sprintf($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 54fe7443..b9aff690 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -35,12 +35,16 @@ public function find(bool $includeArgs = true) { if ($php = getenv('PHP_BINARY')) { if (!is_executable($php)) { - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if (\function_exists('exec') && $php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { - if (!is_executable($php)) { - return false; - } - } else { + if (!\function_exists('exec') || \strlen($php) !== strcspn($php, '/'.\DIRECTORY_SEPARATOR)) { + return false; + } + + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where %s 2> NUL' : 'command -v -- %s'; + $execResult = exec(\sprintf($command, escapeshellarg($php))); + if (!$php = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) { + return false; + } + if (!is_executable($php)) { return false; } } From 46c203f382b73a2575d043e49a17073d3c808fad Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 2 Nov 2024 14:14:29 +0100 Subject: [PATCH 74/95] [Process] Return built-in cmd.exe commands directly in ExecutableFinder --- ExecutableFinder.php | 12 ++++++++++++ Tests/ExecutableFinderTest.php | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 2293595c..1604b6f0 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -20,6 +20,13 @@ class ExecutableFinder { private $suffixes = ['.exe', '.bat', '.cmd', '.com']; + private const CMD_BUILTINS = [ + 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', + 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', + 'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause', + 'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set', + 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', + ]; /** * Replaces default suffixes of executable. @@ -48,6 +55,11 @@ public function addSuffix(string $suffix) */ public function find(string $name, ?string $default = null, array $extraDirs = []) { + // windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes + if ('\\' === \DIRECTORY_SEPARATOR && \in_array(strtolower($name), self::CMD_BUILTINS, true)) { + return $name; + } + $dirs = array_merge( explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), $extraDirs diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index c4876e47..adb5556d 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -159,6 +159,18 @@ public function testEmptyDirInPath() unlink('executable'); } + public function testFindBuiltInCommandOnWindows() + { + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Can be only tested on windows'); + } + + $finder = new ExecutableFinder(); + $this->assertSame('rmdir', $finder->find('RMDIR')); + $this->assertSame('cd', $finder->find('cd')); + $this->assertSame('move', $finder->find('MoVe')); + } + private function assertSamePath($expected, $tested) { if ('\\' === \DIRECTORY_SEPARATOR) { From b61fb1c70392905d5f5f99824324983124a1dd08 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 4 Nov 2024 09:44:46 +0100 Subject: [PATCH 75/95] [Process] Improve test cleanup by unlinking in a `finally` block --- Tests/ExecutableFinderTest.php | 36 +++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index c4876e47..3995e73a 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -125,18 +125,20 @@ public function testFindBatchExecutableOnWindows() $target = tempnam(sys_get_temp_dir(), 'example-windows-executable'); - touch($target); - touch($target.'.BAT'); - - $this->assertFalse(is_executable($target)); + try { + touch($target); + touch($target.'.BAT'); - putenv('PATH='.sys_get_temp_dir()); + $this->assertFalse(is_executable($target)); - $finder = new ExecutableFinder(); - $result = $finder->find(basename($target), false); + putenv('PATH='.sys_get_temp_dir()); - unlink($target); - unlink($target.'.BAT'); + $finder = new ExecutableFinder(); + $result = $finder->find(basename($target), false); + } finally { + unlink($target); + unlink($target.'.BAT'); + } $this->assertSamePath($target.'.BAT', $result); } @@ -148,15 +150,17 @@ public function testEmptyDirInPath() { putenv(sprintf('PATH=%s:', \dirname(\PHP_BINARY))); - touch('executable'); - chmod('executable', 0700); - - $finder = new ExecutableFinder(); - $result = $finder->find('executable'); + try { + touch('executable'); + chmod('executable', 0700); - $this->assertSame('./executable', $result); + $finder = new ExecutableFinder(); + $result = $finder->find('executable'); - unlink('executable'); + $this->assertSame('./executable', $result); + } finally { + unlink('executable'); + } } private function assertSamePath($expected, $tested) From a56fe7b6066efd82037aedfbd1c657e3bcce1810 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 4 Nov 2024 10:27:52 +0100 Subject: [PATCH 76/95] ignore case of built-in cmd.exe commands --- Tests/ExecutableFinderTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index adb5556d..e335e47c 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -166,9 +166,9 @@ public function testFindBuiltInCommandOnWindows() } $finder = new ExecutableFinder(); - $this->assertSame('rmdir', $finder->find('RMDIR')); - $this->assertSame('cd', $finder->find('cd')); - $this->assertSame('move', $finder->find('MoVe')); + $this->assertSame('rmdir', strtolower($finder->find('RMDIR'))); + $this->assertSame('cd', strtolower($finder->find('cd'))); + $this->assertSame('move', strtolower($finder->find('MoVe'))); } private function assertSamePath($expected, $tested) From 7be8366a553b0ea5ec03d01f68c2214b1ce82e89 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 4 Nov 2024 10:25:02 +0100 Subject: [PATCH 77/95] fix the directory separator being used --- Tests/ExecutableFinderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index c4876e47..f85d8c9a 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -146,7 +146,7 @@ public function testFindBatchExecutableOnWindows() */ public function testEmptyDirInPath() { - putenv(sprintf('PATH=%s:', \dirname(\PHP_BINARY))); + putenv(sprintf('PATH=%s%s', \dirname(\PHP_BINARY), \PATH_SEPARATOR)); touch('executable'); chmod('executable', 0700); From 81e1a0cdac68330b5acec27c427cf59be49c73f7 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 4 Nov 2024 11:01:19 +0100 Subject: [PATCH 78/95] fix the path separator being used --- Tests/ExecutableFinderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 4a6c2c4b..fbeb7f07 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -157,7 +157,7 @@ public function testEmptyDirInPath() $finder = new ExecutableFinder(); $result = $finder->find('executable'); - $this->assertSame('./executable', $result); + $this->assertSame(sprintf('.%sexecutable', \PATH_SEPARATOR), $result); } finally { unlink('executable'); } From 72baf6b0591f07b051450bdf2608f93fb5c0a6e5 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 4 Nov 2024 11:14:40 +0100 Subject: [PATCH 79/95] fix the constant being used --- Tests/ExecutableFinderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index fbeb7f07..4aadd9b2 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -157,7 +157,7 @@ public function testEmptyDirInPath() $finder = new ExecutableFinder(); $result = $finder->find('executable'); - $this->assertSame(sprintf('.%sexecutable', \PATH_SEPARATOR), $result); + $this->assertSame(sprintf('.%sexecutable', \DIRECTORY_SEPARATOR), $result); } finally { unlink('executable'); } From d94dda5a49f8e43523d6966ab705a754001d42fe Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 4 Nov 2024 11:43:26 +0100 Subject: [PATCH 80/95] [Process] Fix escaping /X arguments on Windows --- Process.php | 2 +- Tests/ProcessTest.php | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Process.php b/Process.php index 62addf1e..b8012dda 100644 --- a/Process.php +++ b/Process.php @@ -1638,7 +1638,7 @@ private function escapeArgument(?string $argument): string if (str_contains($argument, "\0")) { $argument = str_replace("\0", '?', $argument); } - if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) { + if (!preg_match('/[()%!^"<>&|\s]/', $argument)) { return $argument; } $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index a2e370de..e4d92874 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1424,7 +1424,12 @@ public function testGetCommandLine() { $p = new Process(['/usr/bin/php']); - $expected = '\\' === \DIRECTORY_SEPARATOR ? '"/usr/bin/php"' : "'/usr/bin/php'"; + $expected = '\\' === \DIRECTORY_SEPARATOR ? '/usr/bin/php' : "'/usr/bin/php'"; + $this->assertSame($expected, $p->getCommandLine()); + + $p = new Process(['cd', '/d']); + + $expected = '\\' === \DIRECTORY_SEPARATOR ? 'cd /d' : "'cd' '/d'"; $this->assertSame($expected, $p->getCommandLine()); } From 05c2ccc705cb0336becfdc10f6dd67896d9ba91a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 28 Oct 2024 12:35:32 +0100 Subject: [PATCH 81/95] [Process] Use %PATH% before %CD% to load the shell on Windows --- ExecutableFinder.php | 14 ++++++++------ PhpExecutableFinder.php | 15 ++------------- Process.php | 9 ++++++++- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 1604b6f0..89edd22f 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -19,7 +19,6 @@ */ class ExecutableFinder { - private $suffixes = ['.exe', '.bat', '.cmd', '.com']; private const CMD_BUILTINS = [ 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', @@ -28,6 +27,8 @@ class ExecutableFinder 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', ]; + private $suffixes = []; + /** * Replaces default suffixes of executable. */ @@ -65,11 +66,13 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ $extraDirs ); - $suffixes = ['']; + $suffixes = []; if ('\\' === \DIRECTORY_SEPARATOR) { $pathExt = getenv('PATHEXT'); - $suffixes = array_merge($pathExt ? explode(\PATH_SEPARATOR, $pathExt) : $this->suffixes, $suffixes); + $suffixes = $this->suffixes; + $suffixes = array_merge($suffixes, $pathExt ? explode(\PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com']); } + $suffixes = '' !== pathinfo($name, PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']); foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { if ('' === $dir) { @@ -85,12 +88,11 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ } } - if (!\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { + if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { return $default; } - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where %s 2> NUL' : 'command -v -- %s'; - $execResult = exec(\sprintf($command, escapeshellarg($name))); + $execResult = exec('command -v -- '.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 b9aff690..c3a9680d 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -34,19 +34,8 @@ public function __construct() public function find(bool $includeArgs = true) { if ($php = getenv('PHP_BINARY')) { - if (!is_executable($php)) { - if (!\function_exists('exec') || \strlen($php) !== strcspn($php, '/'.\DIRECTORY_SEPARATOR)) { - return false; - } - - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where %s 2> NUL' : 'command -v -- %s'; - $execResult = exec(\sprintf($command, escapeshellarg($php))); - if (!$php = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) { - return false; - } - if (!is_executable($php)) { - return false; - } + if (!is_executable($php) && !$php = $this->executableFinder->find($php)) { + return false; } if (@is_dir($php)) { diff --git a/Process.php b/Process.php index 62addf1e..0f3457f3 100644 --- a/Process.php +++ b/Process.php @@ -1592,7 +1592,14 @@ function ($m) use (&$env, &$varCache, &$varCount, $uid) { $cmd ); - $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; + static $comSpec; + + if (!$comSpec && $comSpec = (new ExecutableFinder())->find('cmd.exe')) { + // Escape according to CommandLineToArgvW rules + $comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec) .'"'; + } + + $cmd = ($comSpec ?? 'cmd').' /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; foreach ($this->processPipes->getFiles() as $offset => $filename) { $cmd .= ' '.$offset.'>"'.$filename.'"'; } From 01906871cb9b5e3cf872863b91aba4ec9767daf4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 6 Nov 2024 10:18:28 +0100 Subject: [PATCH 82/95] [Process] Fix test --- Tests/ExecutableFinderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 4aadd9b2..84e5b3c3 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -123,7 +123,7 @@ public function testFindBatchExecutableOnWindows() $this->markTestSkipped('Can be only tested on windows'); } - $target = tempnam(sys_get_temp_dir(), 'example-windows-executable'); + $target = str_replace('.tmp', '_tmp', tempnam(sys_get_temp_dir(), 'example-windows-executable')); try { touch($target); From 5d1662fb32ebc94f17ddb8d635454a776066733d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 5 Nov 2024 10:24:24 +0100 Subject: [PATCH 83/95] normalize paths to avoid failures if a path is referenced by different names --- Tests/ExecutableFinderTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 84e5b3c3..c102ab68 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -123,7 +123,8 @@ public function testFindBatchExecutableOnWindows() $this->markTestSkipped('Can be only tested on windows'); } - $target = str_replace('.tmp', '_tmp', tempnam(sys_get_temp_dir(), 'example-windows-executable')); + $tempDir = realpath(sys_get_temp_dir()); + $target = str_replace('.tmp', '_tmp', tempnam($tempDir, 'example-windows-executable')); try { touch($target); @@ -131,7 +132,7 @@ public function testFindBatchExecutableOnWindows() $this->assertFalse(is_executable($target)); - putenv('PATH='.sys_get_temp_dir()); + putenv('PATH='.$tempDir); $finder = new ExecutableFinder(); $result = $finder->find(basename($target), false); From 724e6c68cc3cb7c618860a824e5cbda91815e374 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Wed, 11 Dec 2024 14:08:35 +0100 Subject: [PATCH 84/95] chore: PHP CS Fixer fixes --- ExecutableFinder.php | 2 +- Process.php | 2 +- Tests/ExecutableFinderTest.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 5cc65251..6aa2d4d7 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -72,7 +72,7 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ $pathExt = getenv('PATHEXT'); $suffixes = array_merge($suffixes, $pathExt ? explode(\PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com']); } - $suffixes = '' !== pathinfo($name, PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']); + $suffixes = '' !== pathinfo($name, \PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']); foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { if ('' === $dir) { diff --git a/Process.php b/Process.php index 03ce70e5..f9d4e814 100644 --- a/Process.php +++ b/Process.php @@ -1596,7 +1596,7 @@ function ($m) use (&$env, $uid) { if (!$comSpec && $comSpec = (new ExecutableFinder())->find('cmd.exe')) { // Escape according to CommandLineToArgvW rules - $comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec) .'"'; + $comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec).'"'; } $cmd = ($comSpec ?? 'cmd').' /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 87288360..cdc60a92 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -174,7 +174,7 @@ public function testFindBatchExecutableOnWindows() */ public function testEmptyDirInPath() { - putenv(sprintf('PATH=%s%s', \dirname(\PHP_BINARY), \PATH_SEPARATOR)); + putenv(\sprintf('PATH=%s%s', \dirname(\PHP_BINARY), \PATH_SEPARATOR)); try { touch('executable'); @@ -183,7 +183,7 @@ public function testEmptyDirInPath() $finder = new ExecutableFinder(); $result = $finder->find('executable'); - $this->assertSame(sprintf('.%sexecutable', \DIRECTORY_SEPARATOR), $result); + $this->assertSame(\sprintf('.%sexecutable', \DIRECTORY_SEPARATOR), $result); } finally { unlink('executable'); } From 7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 4 Feb 2025 11:18:48 +0100 Subject: [PATCH 85/95] [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 86/95] 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 87/95] 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 2b42be26326a4fae4313751b40a2ecb60e619311 Mon Sep 17 00:00:00 2001 From: Staormin Date: Thu, 13 Feb 2025 14:38:00 +0100 Subject: [PATCH 88/95] [Messenger][Process] add `fromShellCommandline` to `RunProcessMessage` Allows using the Process::fromShellCommandline when using a RunProcessMessage --- CHANGELOG.md | 6 +++++ Messenger/RunProcessMessage.php | 17 ++++++++++++- Messenger/RunProcessMessageHandler.php | 5 +++- .../RunProcessMessageHandlerTest.php | 24 +++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e33cd0b..d7308566 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= + +7.3 +--- + + * Add `RunProcessMessage::fromShellCommandline()` to instantiate a Process via the fromShellCommandline method + 7.1 --- diff --git a/Messenger/RunProcessMessage.php b/Messenger/RunProcessMessage.php index b2c33fe3..d14ac236 100644 --- a/Messenger/RunProcessMessage.php +++ b/Messenger/RunProcessMessage.php @@ -16,6 +16,8 @@ */ class RunProcessMessage implements \Stringable { + public ?string $commandLine = null; + public function __construct( public readonly array $command, public readonly ?string $cwd = null, @@ -27,6 +29,19 @@ public function __construct( public function __toString(): string { - return implode(' ', $this->command); + return $this->commandLine ?? implode(' ', $this->command); + } + + /** + * Create a process message instance that will instantiate a Process using the fromShellCommandline method. + * + * @see Process::fromShellCommandline + */ + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): self + { + $message = new self([], $cwd, $env, $input, $timeout); + $message->commandLine = $command; + + return $message; } } diff --git a/Messenger/RunProcessMessageHandler.php b/Messenger/RunProcessMessageHandler.php index 41c1934c..69bfa6a1 100644 --- a/Messenger/RunProcessMessageHandler.php +++ b/Messenger/RunProcessMessageHandler.php @@ -22,7 +22,10 @@ final class RunProcessMessageHandler { public function __invoke(RunProcessMessage $message): RunProcessContext { - $process = new Process($message->command, $message->cwd, $message->env, $message->input, $message->timeout); + $process = match ($message->commandLine) { + null => new Process($message->command, $message->cwd, $message->env, $message->input, $message->timeout), + default => Process::fromShellCommandline($message->commandLine, $message->cwd, $message->env, $message->input, $message->timeout), + }; try { return new RunProcessContext($message, $process->mustRun()); diff --git a/Tests/Messenger/RunProcessMessageHandlerTest.php b/Tests/Messenger/RunProcessMessageHandlerTest.php index e095fa09..b5b9ab14 100644 --- a/Tests/Messenger/RunProcessMessageHandlerTest.php +++ b/Tests/Messenger/RunProcessMessageHandlerTest.php @@ -44,4 +44,28 @@ public function testRunFailedProcess() $this->fail('Exception not thrown'); } + + public function testRunSuccessfulProcessFromShellCommandline() + { + $context = (new RunProcessMessageHandler())(RunProcessMessage::fromShellCommandline('ls | grep Test', cwd: __DIR__)); + + $this->assertSame('ls | grep Test', $context->message->commandLine); + $this->assertSame(0, $context->exitCode); + $this->assertStringContainsString(basename(__FILE__), $context->output); + } + + public function testRunFailedProcessFromShellCommandline() + { + try { + (new RunProcessMessageHandler())(RunProcessMessage::fromShellCommandline('invalid')); + $this->fail('Exception not thrown'); + } catch (RunProcessFailedException $e) { + $this->assertSame('invalid', $e->context->message->commandLine); + $this->assertContains( + $e->context->exitCode, + [null, '\\' === \DIRECTORY_SEPARATOR ? 1 : 127], + 'Exit code should be 1 on Windows, 127 on other systems, or null', + ); + } + } } From 0596c465ef3aa0452f954c9f442c4f45536b8534 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 2 Mar 2025 16:03:52 +0100 Subject: [PATCH 89/95] replace assertEmpty() with stricter assertions --- Tests/ProcessTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index b17bfc7a..77fc2613 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -422,7 +422,7 @@ public function testFlushErrorOutput() $p->run(); $p->clearErrorOutput(); - $this->assertEmpty($p->getErrorOutput()); + $this->assertSame('', $p->getErrorOutput()); } /** @@ -475,7 +475,7 @@ public function testFlushOutput() $p->run(); $p->clearOutput(); - $this->assertEmpty($p->getOutput()); + $this->assertSame('', $p->getOutput()); } public function testZeroAsOutput() From e2a61c16af36c9a07e5c9906498b73e091949a20 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Mon, 10 Mar 2025 17:34:14 +0100 Subject: [PATCH 90/95] 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()) { From 40c295f2deb408d5e9d2d32b8ba1dd61e36f05af Mon Sep 17 00:00:00 2001 From: Filippo Tessarotto Date: Thu, 17 Apr 2025 08:03:48 +0200 Subject: [PATCH 91/95] [Process] Narrow `PhpExecutableFinder` return types --- PhpExecutableFinder.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index 9f9218f9..f9ed79e4 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -83,6 +83,8 @@ public function find(bool $includeArgs = true): string|false /** * Finds the PHP executable arguments. + * + * @return list */ public function findArguments(): array { From 8eb6dc555bfb49b2703438d5de65cc9f138ff50b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 10 Jul 2025 09:12:18 +0200 Subject: [PATCH 92/95] CS fixes --- Exception/ProcessFailedException.php | 4 ++-- Exception/ProcessSignaledException.php | 2 +- Exception/ProcessTimedOutException.php | 4 ++-- ExecutableFinder.php | 2 +- InputStream.php | 2 +- PhpProcess.php | 2 +- PhpSubprocess.php | 2 +- Pipes/AbstractPipes.php | 2 +- Pipes/WindowsPipes.php | 2 +- Process.php | 20 ++++++++++---------- ProcessUtils.php | 2 +- Tests/ExecutableFinderTest.php | 4 ++-- Tests/ProcessTest.php | 8 ++++---- Tests/SignalListener.php | 5 ++++- 14 files changed, 32 insertions(+), 29 deletions(-) diff --git a/Exception/ProcessFailedException.php b/Exception/ProcessFailedException.php index 19b40570..29cd386b 100644 --- a/Exception/ProcessFailedException.php +++ b/Exception/ProcessFailedException.php @@ -28,7 +28,7 @@ public function __construct(Process $process) 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(), @@ -36,7 +36,7 @@ public function __construct(Process $process) ); 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 0fed8ac3..12eb4b3b 100644 --- a/Exception/ProcessSignaledException.php +++ b/Exception/ProcessSignaledException.php @@ -26,7 +26,7 @@ public function __construct(Process $process) { $this->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/ProcessTimedOutException.php b/Exception/ProcessTimedOutException.php index 1cecdae7..94c1a33a 100644 --- a/Exception/ProcessTimedOutException.php +++ b/Exception/ProcessTimedOutException.php @@ -31,7 +31,7 @@ public function __construct(Process $process, int $timeoutType) $this->process = $process; $this->timeoutType = $timeoutType; - parent::__construct(sprintf( + parent::__construct(\sprintf( 'The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), $this->getExceededTimeout() @@ -67,7 +67,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/ExecutableFinder.php b/ExecutableFinder.php index 1838d54b..4d820262 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -74,7 +74,7 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ $suffixes = $this->suffixes; $suffixes = array_merge($suffixes, $pathExt ? explode(\PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com']); } - $suffixes = '' !== pathinfo($name, PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']); + $suffixes = '' !== pathinfo($name, \PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']); foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { if ('' === $dir) { diff --git a/InputStream.php b/InputStream.php index 931217c8..3bcbfe84 100644 --- a/InputStream.php +++ b/InputStream.php @@ -50,7 +50,7 @@ public function write(mixed $input) 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 6e2ab59f..db6ebf2a 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)); } /** diff --git a/PhpSubprocess.php b/PhpSubprocess.php index 04fd8ea8..bdd4173c 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 8033442a..9124f412 100644 --- a/Pipes/WindowsPipes.php +++ b/Pipes/WindowsPipes.php @@ -53,7 +53,7 @@ public function __construct(mixed $input, bool $haveReadSupport) 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 6dfb25e7..dda43d4a 100644 --- a/Process.php +++ b/Process.php @@ -340,7 +340,7 @@ public function start(?callable $callback = null, array $env = []) } 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)); } $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); @@ -1197,7 +1197,7 @@ public function setOptions(array $options) 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; } @@ -1453,10 +1453,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; @@ -1466,12 +1466,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; @@ -1528,7 +1528,7 @@ function ($m) use (&$env, $uid) { if (!$comSpec && $comSpec = (new ExecutableFinder())->find('cmd.exe')) { // Escape according to CommandLineToArgvW rules - $comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec) .'"'; + $comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec).'"'; } $cmd = ($comSpec ?? 'cmd').' /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; @@ -1547,7 +1547,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)); } } @@ -1559,7 +1559,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)); } } @@ -1589,7 +1589,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/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index c102ab68..3dc621cf 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -149,7 +149,7 @@ public function testFindBatchExecutableOnWindows() */ public function testEmptyDirInPath() { - putenv(sprintf('PATH=%s%s', \dirname(\PHP_BINARY), \PATH_SEPARATOR)); + putenv(\sprintf('PATH=%s%s', \dirname(\PHP_BINARY), \PATH_SEPARATOR)); try { touch('executable'); @@ -158,7 +158,7 @@ public function testEmptyDirInPath() $finder = new ExecutableFinder(); $result = $finder->find('executable'); - $this->assertSame(sprintf('.%sexecutable', \DIRECTORY_SEPARATOR), $result); + $this->assertSame(\sprintf('.%sexecutable', \DIRECTORY_SEPARATOR), $result); } finally { unlink('executable'); } diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index e9c7527c..3497dd5b 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -168,7 +168,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(); @@ -359,7 +359,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()); } @@ -980,7 +980,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}(); } @@ -1475,7 +1475,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"; 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 6e0026e55e327c91694d23b3480e989680fbe140 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 1 Aug 2025 17:44:34 +0200 Subject: [PATCH 93/95] Fix wrong boolean values --- Pipes/AbstractPipes.php | 6 +++--- Tests/PipeStdinInStdoutStdErrStreamSelect.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Pipes/AbstractPipes.php b/Pipes/AbstractPipes.php index 51a566f3..19eea16f 100644 --- a/Pipes/AbstractPipes.php +++ b/Pipes/AbstractPipes.php @@ -72,10 +72,10 @@ protected function unblock(): void } foreach ($this->pipes as $pipe) { - stream_set_blocking($pipe, 0); + stream_set_blocking($pipe, false); } if (\is_resource($this->input)) { - stream_set_blocking($this->input, 0); + stream_set_blocking($this->input, false); } $this->blocked = false; @@ -97,7 +97,7 @@ protected function write(): ?array if (!$input->valid()) { $input = null; } elseif (\is_resource($input = $input->current())) { - stream_set_blocking($input, 0); + stream_set_blocking($input, false); } elseif (!isset($this->inputBuffer[0])) { if (!\is_string($input)) { if (!\is_scalar($input)) { diff --git a/Tests/PipeStdinInStdoutStdErrStreamSelect.php b/Tests/PipeStdinInStdoutStdErrStreamSelect.php index 09124a4b..fa0901e2 100644 --- a/Tests/PipeStdinInStdoutStdErrStreamSelect.php +++ b/Tests/PipeStdinInStdoutStdErrStreamSelect.php @@ -17,9 +17,9 @@ $read = [\STDIN]; $write = [\STDOUT, \STDERR]; -stream_set_blocking(\STDIN, 0); -stream_set_blocking(\STDOUT, 0); -stream_set_blocking(\STDERR, 0); +stream_set_blocking(\STDIN, false); +stream_set_blocking(\STDOUT, false); +stream_set_blocking(\STDERR, false); $out = $err = ''; while ($read || $write) { From 6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8 Mon Sep 17 00:00:00 2001 From: Christian Seel Date: Wed, 13 Aug 2025 09:45:00 +0200 Subject: [PATCH 94/95] [Process] Enhance hasSystemCallBeenInterrupted function for non-english locale --- Pipes/AbstractPipes.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Pipes/AbstractPipes.php b/Pipes/AbstractPipes.php index 19eea16f..158f0487 100644 --- a/Pipes/AbstractPipes.php +++ b/Pipes/AbstractPipes.php @@ -52,14 +52,26 @@ public function close(): void /** * Returns true if a system call has been interrupted. + * + * stream_select() returns false when the `select` system call is interrupted by an incoming signal. */ protected function hasSystemCallBeenInterrupted(): bool { $lastError = $this->lastError; $this->lastError = null; - // stream_select returns false when the `select` system call is interrupted by an incoming signal - return null !== $lastError && false !== stripos($lastError, 'interrupted system call'); + if (null === $lastError) { + return false; + } + + if (false !== stripos($lastError, 'interrupted system call')) { + return true; + } + + // on applications with a different locale than english, the message above is not found because + // it's translated. So we also check for the SOCKET_EINTR constant which is defined under + // Windows and UNIX-like platforms (if available on the platform). + return \defined('SOCKET_EINTR') && str_starts_with($lastError, 'stream_select(): Unable to select ['.\SOCKET_EINTR.']'); } /** From 48bad913268c8cafabbf7034b39c8bb24fbc5ab8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 11 Sep 2025 10:16:56 +0200 Subject: [PATCH 95/95] Replace __sleep/wakeup() by __(un)serialize() for throwing and internal usages --- Pipes/UnixPipes.php | 4 ++-- Pipes/WindowsPipes.php | 4 ++-- Process.php | 7 ++----- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Pipes/UnixPipes.php b/Pipes/UnixPipes.php index a0e48dd3..8838c68a 100644 --- a/Pipes/UnixPipes.php +++ b/Pipes/UnixPipes.php @@ -35,12 +35,12 @@ public function __construct(?bool $ttyMode, bool $ptyMode, mixed $input, bool $h parent::__construct($input); } - public function __sleep(): array + public function __serialize(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - public function __wakeup(): void + public function __unserialize(array $data): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } diff --git a/Pipes/WindowsPipes.php b/Pipes/WindowsPipes.php index 9124f412..bec37358 100644 --- a/Pipes/WindowsPipes.php +++ b/Pipes/WindowsPipes.php @@ -88,12 +88,12 @@ public function __construct(mixed $input, bool $haveReadSupport) parent::__construct($input); } - public function __sleep(): array + public function __serialize(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - public function __wakeup(): void + public function __unserialize(array $data): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } diff --git a/Process.php b/Process.php index dda43d4a..ce730f98 100644 --- a/Process.php +++ b/Process.php @@ -194,15 +194,12 @@ public static function fromShellCommandline(string $command, ?string $cwd = null return $process; } - public function __sleep(): array + public function __serialize(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - /** - * @return void - */ - public function __wakeup() + public function __unserialize(array $data): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); }