This commit is contained in:
2025-05-12 14:25:25 +02:00
parent ab2db755ef
commit 9e378ca2b7
2719 changed files with 46505 additions and 60181 deletions

View File

@@ -7,7 +7,6 @@ CHANGELOG
* 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`
5.2.0
-----

View File

@@ -19,7 +19,15 @@ namespace Symfony\Component\Process;
*/
class ExecutableFinder
{
private array $suffixes = ['.exe', '.bat', '.cmd', '.com'];
private const CMD_BUILTINS = [
'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date',
'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto',
'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause',
'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set',
'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol',
];
private array $suffixes = [];
/**
* Replaces default suffixes of executable.
@@ -48,20 +56,30 @@ class ExecutableFinder
* @param string|null $default The default to return if no executable is found
* @param array $extraDirs Additional dirs to check into
*/
public function find(string $name, string $default = null, array $extraDirs = []): ?string
public function find(string $name, ?string $default = null, array $extraDirs = []): ?string
{
// 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
);
$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) {
$dir = '.';
}
if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) {
return $file;
}
@@ -72,8 +90,13 @@ class ExecutableFinder
}
}
$command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v';
if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && @is_executable($executablePath)) {
if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) {
return $default;
}
$execResult = exec('command -v -- '.escapeshellarg($name));
if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) {
return $executablePath;
}

View File

@@ -31,7 +31,7 @@ class InputStream implements \IteratorAggregate
*
* @return void
*/
public function onEmpty(callable $onEmpty = null)
public function onEmpty(?callable $onEmpty = null)
{
$this->onEmpty = null !== $onEmpty ? $onEmpty(...) : null;
}

View File

@@ -32,15 +32,8 @@ class PhpExecutableFinder
public function find(bool $includeArgs = true): string|false
{
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 {
return false;
}
if (!is_executable($php) && !$php = $this->executableFinder->find($php)) {
return false;
}
if (@is_dir($php)) {

View File

@@ -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();
@@ -50,7 +50,7 @@ class PhpProcess extends Process
parent::__construct($php, $cwd, $env, $script, $timeout);
}
public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, mixed $input = null, ?float $timeout = 60): static
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));
}
@@ -58,7 +58,7 @@ class PhpProcess extends Process
/**
* @return void
*/
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.');

View File

@@ -51,7 +51,7 @@ class PhpSubprocess 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(array $command, string $cwd = null, array $env = null, int $timeout = 60, array $php = null)
public function __construct(array $command, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null)
{
if (null === $php) {
$executableFinder = new PhpExecutableFinder();
@@ -73,12 +73,12 @@ class PhpSubprocess extends Process
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
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 = []): void
public function start(?callable $callback = null, array $env = []): void
{
if (null === $this->getCommandLine()) {
throw new RuntimeException('Unable to find the PHP executable.');
@@ -106,7 +106,7 @@ class PhpSubprocess extends Process
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]);
}

View File

@@ -74,7 +74,7 @@ class UnixPipes extends AbstractPipes
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
];
}

View File

@@ -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, mixed $input = null, ?float $timeout = 60)
public function __construct(array $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60)
{
if (!\function_exists('proc_open')) {
throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.');
@@ -186,7 +186,7 @@ class Process implements \IteratorAggregate
*
* @throws LogicException When proc_open is not installed
*/
public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, mixed $input = null, ?float $timeout = 60): static
public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static
{
$process = new static([], $cwd, $env, $input, $timeout);
$process->commandline = $command;
@@ -244,7 +244,7 @@ class Process implements \IteratorAggregate
*
* @final
*/
public function run(callable $callback = null, array $env = []): int
public function run(?callable $callback = null, array $env = []): int
{
$this->start($callback, $env);
@@ -263,7 +263,7 @@ class Process implements \IteratorAggregate
*
* @final
*/
public function mustRun(callable $callback = null, array $env = []): static
public function mustRun(?callable $callback = null, array $env = []): static
{
if (0 !== $this->run($callback, $env)) {
throw new ProcessFailedException($this);
@@ -293,7 +293,7 @@ class Process implements \IteratorAggregate
* @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.');
@@ -345,7 +345,7 @@ class Process implements \IteratorAggregate
$process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options);
if (!\is_resource($process)) {
if (!$process) {
throw new RuntimeException('Unable to launch a new process.');
}
$this->process = $process;
@@ -378,7 +378,7 @@ class Process implements \IteratorAggregate
*
* @final
*/
public function restart(callable $callback = null, array $env = []): static
public function restart(?callable $callback = null, array $env = []): static
{
if ($this->isRunning()) {
throw new RuntimeException('Process is already running.');
@@ -405,7 +405,7 @@ class Process implements \IteratorAggregate
* @throws ProcessSignaledException When process stopped after receiving signal
* @throws LogicException When process is not yet started
*/
public function wait(callable $callback = null): int
public function wait(?callable $callback = null): int
{
$this->requireProcessIsStarted(__FUNCTION__);
@@ -878,7 +878,7 @@ class Process implements \IteratorAggregate
*
* @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): ?int
public function stop(float $timeout = 10, ?int $signal = null): ?int
{
$timeoutMicro = microtime(true) + $timeout;
if ($this->isRunning()) {
@@ -1210,7 +1210,7 @@ class Process implements \IteratorAggregate
{
static $isTtySupported;
return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT));
return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT) && @is_writable('/dev/tty'));
}
/**
@@ -1256,7 +1256,7 @@ class Process implements \IteratorAggregate
*
* @param callable|null $callback The user defined PHP callback
*/
protected function buildCallback(callable $callback = null): \Closure
protected function buildCallback(?callable $callback = null): \Closure
{
if ($this->outputDisabled) {
return fn ($type, $data): bool => null !== $callback && $callback($type, $data);
@@ -1288,7 +1288,9 @@ class Process implements \IteratorAggregate
return;
}
$this->processInformation = proc_get_status($this->process);
if ($this->processInformation['running'] ?? true) {
$this->processInformation = proc_get_status($this->process);
}
$running = $this->processInformation['running'];
$this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running);
@@ -1386,8 +1388,9 @@ class Process implements \IteratorAggregate
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;
@@ -1521,7 +1524,14 @@ class Process implements \IteratorAggregate
$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.'"';
}
@@ -1567,7 +1577,7 @@ class Process implements \IteratorAggregate
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);