From f64dbc87a58cfedfc6e7f8635697a94386f2ccc4 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 24 May 2024 13:05:21 +0200 Subject: [PATCH 01/13] use constructor property promotion --- Exception/ProcessFailedException.php | 7 +++---- Exception/ProcessSignaledException.php | 9 +++------ Exception/ProcessStartFailedException.php | 10 ++++------ Exception/ProcessTimedOutException.php | 12 ++++-------- Pipes/UnixPipes.php | 16 ++++++---------- Pipes/WindowsPipes.php | 9 ++++----- 6 files changed, 24 insertions(+), 39 deletions(-) diff --git a/Exception/ProcessFailedException.php b/Exception/ProcessFailedException.php index 499809ee..6cd8a355 100644 --- a/Exception/ProcessFailedException.php +++ b/Exception/ProcessFailedException.php @@ -20,10 +20,9 @@ */ class ProcessFailedException extends RuntimeException { - private Process $process; - - public function __construct(Process $process) - { + public function __construct( + private Process $process, + ) { if ($process->isSuccessful()) { throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); } diff --git a/Exception/ProcessSignaledException.php b/Exception/ProcessSignaledException.php index 0fed8ac3..4466b0d8 100644 --- a/Exception/ProcessSignaledException.php +++ b/Exception/ProcessSignaledException.php @@ -20,12 +20,9 @@ */ final class ProcessSignaledException extends RuntimeException { - private Process $process; - - public function __construct(Process $process) - { - $this->process = $process; - + public function __construct( + private Process $process, + ) { parent::__construct(sprintf('The process has been signaled with signal "%s".', $process->getTermSignal())); } diff --git a/Exception/ProcessStartFailedException.php b/Exception/ProcessStartFailedException.php index 9bd5a036..24bd5009 100644 --- a/Exception/ProcessStartFailedException.php +++ b/Exception/ProcessStartFailedException.php @@ -18,10 +18,10 @@ */ class ProcessStartFailedException extends ProcessFailedException { - private Process $process; - - public function __construct(Process $process, ?string $message) - { + public function __construct( + private Process $process, + ?string $message, + ) { if ($process->isStarted()) { throw new InvalidArgumentException('Expected a process that failed during startup, but the given process was started successfully.'); } @@ -34,8 +34,6 @@ public function __construct(Process $process, ?string $message) // Skip parent constructor RuntimeException::__construct($error); - - $this->process = $process; } public function getProcess(): Process diff --git a/Exception/ProcessTimedOutException.php b/Exception/ProcessTimedOutException.php index 252e1112..b692e35f 100644 --- a/Exception/ProcessTimedOutException.php +++ b/Exception/ProcessTimedOutException.php @@ -23,14 +23,10 @@ class ProcessTimedOutException extends RuntimeException public const TYPE_GENERAL = 1; public const TYPE_IDLE = 2; - private Process $process; - private int $timeoutType; - - public function __construct(Process $process, int $timeoutType) - { - $this->process = $process; - $this->timeoutType = $timeoutType; - + public function __construct( + private Process $process, + private int $timeoutType, + ) { parent::__construct(sprintf( 'The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), diff --git a/Pipes/UnixPipes.php b/Pipes/UnixPipes.php index 7bd0db0e..8e95afaa 100644 --- a/Pipes/UnixPipes.php +++ b/Pipes/UnixPipes.php @@ -22,16 +22,12 @@ */ class UnixPipes extends AbstractPipes { - private ?bool $ttyMode; - private bool $ptyMode; - private bool $haveReadSupport; - - public function __construct(?bool $ttyMode, bool $ptyMode, mixed $input, bool $haveReadSupport) - { - $this->ttyMode = $ttyMode; - $this->ptyMode = $ptyMode; - $this->haveReadSupport = $haveReadSupport; - + public function __construct( + private ?bool $ttyMode, + private bool $ptyMode, + mixed $input, + private bool $haveReadSupport, + ) { parent::__construct($input); } diff --git a/Pipes/WindowsPipes.php b/Pipes/WindowsPipes.php index 8033442a..26fa7498 100644 --- a/Pipes/WindowsPipes.php +++ b/Pipes/WindowsPipes.php @@ -33,12 +33,11 @@ class WindowsPipes extends AbstractPipes Process::STDOUT => 0, Process::STDERR => 0, ]; - private bool $haveReadSupport; - - public function __construct(mixed $input, bool $haveReadSupport) - { - $this->haveReadSupport = $haveReadSupport; + public function __construct( + mixed $input, + private bool $haveReadSupport, + ) { if ($this->haveReadSupport) { // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. // Workaround for this problem is to use temporary files instead of pipes on Windows platform. From f7456b8509dacf9f6ae0f4bc6857fa6d7059e77a Mon Sep 17 00:00:00 2001 From: Travis Carden Date: Tue, 21 Nov 2023 18:24:14 -0500 Subject: [PATCH 02/13] [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 03/13] 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 04/13] [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 05/13] 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 bb0a8b7772610211c2cd7d6e4e36acfcbadcb613 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 28 Jul 2024 13:18:19 +0200 Subject: [PATCH 06/13] 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 85dc2723935920dabcb8bc553e882174c4a6b344 Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Fri, 13 Sep 2024 16:26:53 +0200 Subject: [PATCH 07/13] [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 2ad775b9f17c8c9c1fe457750ce191e0f7c1fbff Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 26 Sep 2024 10:09:09 +0200 Subject: [PATCH 08/13] Remove unused imports --- Tests/ProcessTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 5532016b..290100e6 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -16,7 +16,6 @@ use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessSignaledException; -use Symfony\Component\Process\Exception\ProcessStartFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\InputStream; From 18f50a7e1ab5c0c574f820df5d7a77bf3cdd3f5a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 29 Oct 2024 21:50:35 +0100 Subject: [PATCH 09/13] [Process] On Windows, don't rely on the OS to find executables --- Process.php | 7 +++++++ Tests/ProcessFailedExceptionTest.php | 10 +++++----- Tests/ProcessTest.php | 11 +++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Process.php b/Process.php index 78b0fd35..b7d07571 100644 --- a/Process.php +++ b/Process.php @@ -85,6 +85,7 @@ class Process implements \IteratorAggregate private ?int $cachedExitCode = null; private static ?bool $sigchild = null; + private static array $executables = []; /** * Exit codes translation table. @@ -1543,6 +1544,12 @@ private function buildShellCommandline(string|array $commandline): string return $commandline; } + if ('\\' === \DIRECTORY_SEPARATOR && isset($commandline[0][0]) && \strlen($commandline[0]) === strcspn($commandline[0], ':/\\')) { + // On Windows, we don't rely on the OS to find the executable if possible to avoid lookups + // in the current directory which could be untrusted. Instead we use the ExecutableFinder. + $commandline[0] = (self::$executables[$commandline[0]] ??= (new ExecutableFinder())->find($commandline[0])) ?? $commandline[0]; + } + return implode(' ', array_map($this->escapeArgument(...), $commandline)); } diff --git a/Tests/ProcessFailedExceptionTest.php b/Tests/ProcessFailedExceptionTest.php index 259ffd63..d05beb85 100644 --- a/Tests/ProcessFailedExceptionTest.php +++ b/Tests/ProcessFailedExceptionTest.php @@ -80,8 +80,8 @@ public function testProcessFailedExceptionPopulatesInformationFromProcessOutput( $exception = new ProcessFailedException($process); - $this->assertEquals( - "The command \"$cmd\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}\n\nOutput:\n================\n{$output}\n\nError Output:\n================\n{$errorOutput}", + $this->assertStringMatchesFormat( + "The command \"%s\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}\n\nOutput:\n================\n{$output}\n\nError Output:\n================\n{$errorOutput}", str_replace("'php'", 'php', $exception->getMessage()) ); } @@ -126,9 +126,9 @@ public function testDisabledOutputInFailedExceptionDoesNotPopulateOutput() $exception = new ProcessFailedException($process); - $this->assertEquals( - "The command \"$cmd\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}", - str_replace("'php'", 'php', $exception->getMessage()) + $this->assertStringMatchesFormat( + "The command \"%s\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}", + $exception->getMessage() ); } } diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 290100e6..939b7166 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1687,6 +1687,17 @@ public function testNotIgnoringSignal() $this->assertSame(\SIGTERM, $process->getTermSignal()); } + public function testPathResolutionOnWindows() + { + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test is for Windows platform only'); + } + + $process = $this->getProcess(['where']); + + $this->assertSame('C:\\Windows\\system32\\where.EXE', $process->getCommandLine()); + } + private function getProcess(string|array $commandline, ?string $cwd = null, ?array $env = null, mixed $input = null, ?int $timeout = 60): Process { if (\is_string($commandline)) { From 7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 4 Feb 2025 11:18:48 +0100 Subject: [PATCH 10/13] [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 11/13] 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 12/13] skip transient test on GitHub Actions --- Tests/ProcessTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 8f9f131d..b17bfc7a 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -710,9 +710,6 @@ public function testProcessIsNotSignaled() if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('Windows does not support POSIX signals'); } - if (\PHP_VERSION_ID < 80300 && isset($_SERVER['GITHUB_ACTIONS'])) { - $this->markTestSkipped('Transient on GHA with PHP < 8.3'); - } $process = $this->getProcess('echo foo'); $process->run(); @@ -1689,6 +1686,9 @@ public function testNotIgnoringSignal() if (!\function_exists('pcntl_signal')) { $this->markTestSkipped('pnctl extension is required.'); } + if (\PHP_VERSION_ID < 80300 && isset($_SERVER['GITHUB_ACTIONS'])) { + $this->markTestSkipped('Transient on GHA with PHP < 8.3'); + } $process = $this->getProcess(['sleep', '10']); From e2a61c16af36c9a07e5c9906498b73e091949a20 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Mon, 10 Mar 2025 17:34:14 +0100 Subject: [PATCH 13/13] 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()) {