diff --git a/Exception/ProcessFailedException.php b/Exception/ProcessFailedException.php index 499809ee..de8a9e98 100644 --- a/Exception/ProcessFailedException.php +++ b/Exception/ProcessFailedException.php @@ -20,15 +20,14 @@ */ 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.'); } - $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 +35,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..3fd13e5d 100644 --- a/Exception/ProcessSignaledException.php +++ b/Exception/ProcessSignaledException.php @@ -20,13 +20,10 @@ */ final class ProcessSignaledException extends RuntimeException { - private Process $process; - - public function __construct(Process $process) - { - $this->process = $process; - - parent::__construct(sprintf('The process has been signaled with signal "%s".', $process->getTermSignal())); + public function __construct( + private Process $process, + ) { + 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 9bd5a036..37254725 100644 --- a/Exception/ProcessStartFailedException.php +++ b/Exception/ProcessStartFailedException.php @@ -18,15 +18,15 @@ */ 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.'); } - $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' @@ -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..d3fe4934 100644 --- a/Exception/ProcessTimedOutException.php +++ b/Exception/ProcessTimedOutException.php @@ -23,15 +23,11 @@ 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; - - parent::__construct(sprintf( + public function __construct( + private Process $process, + private int $timeoutType, + ) { + parent::__construct(\sprintf( 'The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), $this->getExceededTimeout() @@ -58,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/ExecutableFinder.php b/ExecutableFinder.php index ceb7a558..dca238de 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))) { @@ -68,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/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/PhpExecutableFinder.php b/PhpExecutableFinder.php index 4a882e0f..b740231a 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; } @@ -81,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); } 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 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/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..116b8e30 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. @@ -53,7 +52,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 baa7d955..78b0fd35 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; @@ -1220,7 +1220,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; } @@ -1504,10 +1504,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; @@ -1517,12 +1517,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; @@ -1549,7 +1549,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( '/"(?:( [^"%!^]*+ @@ -1601,7 +1601,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)); } } @@ -1613,7 +1613,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)); } } @@ -1643,7 +1643,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 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() diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index a639f058..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; @@ -186,7 +185,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(); @@ -379,7 +378,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()); } @@ -987,7 +986,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}(); } @@ -1487,7 +1486,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";