Updated Symfony/Process to version 7.1.3

This commit is contained in:
netkas 2024-09-19 15:14:53 -04:00
parent 910477df8e
commit dd48100f06
21 changed files with 547 additions and 178 deletions

View file

@ -69,6 +69,7 @@ This update introduces a refactored code-base, code quality improvements, and be
- Refactor project constants handling in NccCompiler
- Updated Symfony/Yaml to version 7.1.4
- Updated Symfony/Uid to version 7.1.4
- Updated Symfony/Process to version 7.1.3
### Fixed
- Fixed Division by zero in PackageManager

View file

@ -1,6 +1,18 @@
CHANGELOG
=========
7.1
---
* Add `Process::setIgnoredSignals()` to disable signal propagation to the child process
6.4
---
* Add `PhpSubprocess` to handle PHP subprocesses that take over the
configuration from their parent
* Add `RunProcessMessage` and `RunProcessMessageHandler`
5.2.0
-----

View file

@ -20,7 +20,7 @@ use ncc\ThirdParty\Symfony\Process\Process;
*/
class ProcessFailedException extends RuntimeException
{
private $process;
private Process $process;
public function __construct(Process $process)
{
@ -47,10 +47,7 @@ class ProcessFailedException extends RuntimeException
$this->process = $process;
}
/**
* @return Process
*/
public function getProcess()
public function getProcess(): Process
{
return $this->process;
}

View file

@ -20,7 +20,7 @@ use ncc\ThirdParty\Symfony\Process\Process;
*/
final class ProcessSignaledException extends RuntimeException
{
private $process;
private Process $process;
public function __construct(Process $process)
{

View file

@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace ncc\ThirdParty\Symfony\Process\Exception;
use ncc\ThirdParty\Symfony\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;
}
}

View file

@ -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)
{
@ -38,26 +38,17 @@ class ProcessTimedOutException extends RuntimeException
));
}
/**
* @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;
}

View file

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace ncc\ThirdParty\Symfony\Process\Exception;
use ncc\ThirdParty\Symfony\Process\Messenger\RunProcessContext;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class RunProcessFailedException extends RuntimeException
{
public function __construct(ProcessFailedException $exception, public readonly RunProcessContext $context)
{
parent::__construct($exception->getMessage(), $exception->getCode());
}
}

View file

@ -19,24 +19,20 @@ namespace ncc\ThirdParty\Symfony\Process;
*/
class ExecutableFinder
{
private $suffixes = ['.exe', '.bat', '.cmd', '.com'];
private array $suffixes = ['.exe', '.bat', '.cmd', '.com'];
/**
* 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;
}
@ -48,27 +44,12 @@ 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
{
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 +61,18 @@ class ExecutableFinder
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;
}
}

View file

@ -22,19 +22,16 @@ use ncc\ThirdParty\Symfony\Process\Exception\RuntimeException;
*/
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.
*
* @return void
*/
public function onEmpty(callable $onEmpty = null)
public function onEmpty(?callable $onEmpty = null): void
{
$this->onEmpty = $onEmpty;
$this->onEmpty = null !== $onEmpty ? $onEmpty(...) : null;
}
/**
@ -42,10 +39,8 @@ class InputStream implements \IteratorAggregate
*
* @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 +53,16 @@ class InputStream implements \IteratorAggregate
/**
* 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;
}

View file

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace ncc\ThirdParty\Symfony\Process\Messenger;
use ncc\ThirdParty\Symfony\Process\Process;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class RunProcessContext
{
public readonly ?int $exitCode;
public readonly ?string $output;
public readonly ?string $errorOutput;
public function __construct(
public readonly RunProcessMessage $message,
Process $process,
) {
$this->exitCode = $process->getExitCode();
$this->output = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getOutput();
$this->errorOutput = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getErrorOutput();
}
}

View file

@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace ncc\ThirdParty\Symfony\Process\Messenger;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
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);
}
}

View file

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace ncc\ThirdParty\Symfony\Process\Messenger;
use ncc\ThirdParty\Symfony\Process\Exception\ProcessFailedException;
use ncc\ThirdParty\Symfony\Process\Exception\RunProcessFailedException;
use ncc\ThirdParty\Symfony\Process\Process;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
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()));
}
}
}

View file

@ -19,7 +19,7 @@ namespace ncc\ThirdParty\Symfony\Process;
*/
class PhpExecutableFinder
{
private $executableFinder;
private ExecutableFinder $executableFinder;
public function __construct()
{
@ -33,8 +33,8 @@ class PhpExecutableFinder
{
if ($php = getenv('PHP_BINARY')) {
if (!is_executable($php)) {
$command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v';
if ($php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) {
$command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --';
if (\function_exists('exec') && $php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) {
if (!is_executable($php)) {
return false;
}

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,15 +50,12 @@ 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));
}
/**
* @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.');

View file

@ -0,0 +1,164 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace ncc\ThirdParty\Symfony\Process;
use ncc\ThirdParty\Symfony\Process\Exception\LogicException;
use ncc\ThirdParty\Symfony\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:
*
* <?php var_dump(ini_get('memory_limit'));
*
* These are the differences between the regular Process and PhpSubprocess classes:
*
* $p = new Process(['php', '-d', 'memory_limit=256M', 'MemoryTest.php']);
* $p->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 <yanick.witschi@terminal42.ch>
* @author Partially copied and heavily inspired from composer/xdebug-handler by John Stevenson <john-stevenson@blueyonder.co.uk>
*/
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 = []): void
{
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, \PREG_OFFSET_CAPTURE)) {
$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;
}
}

View file

@ -22,20 +22,19 @@ abstract class AbstractPipes implements PipesInterface
{
public array $pipes = [];
private $inputBuffer = '';
private string $inputBuffer = '';
/** @var resource|string|\Iterator */
private $input;
private $blocked = true;
private $lastError;
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;
}

View file

@ -22,9 +22,9 @@ use ncc\ThirdParty\Symfony\Process\Process;
*/
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)
{
@ -40,7 +40,7 @@ class UnixPipes extends AbstractPipes
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
public function __wakeup(): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}

View file

@ -26,14 +26,14 @@ use ncc\ThirdParty\Symfony\Process\Process;
*/
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)
{
@ -93,7 +93,7 @@ class WindowsPipes extends AbstractPipes
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
public function __wakeup(): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
@ -140,7 +140,7 @@ class WindowsPipes extends AbstractPipes
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) {

View file

@ -15,9 +15,9 @@ use ncc\ThirdParty\Symfony\Process\Exception\InvalidArgumentException;
use ncc\ThirdParty\Symfony\Process\Exception\LogicException;
use ncc\ThirdParty\Symfony\Process\Exception\ProcessFailedException;
use ncc\ThirdParty\Symfony\Process\Exception\ProcessSignaledException;
use ncc\ThirdParty\Symfony\Process\Exception\ProcessStartFailedException;
use ncc\ThirdParty\Symfony\Process\Exception\ProcessTimedOutException;
use ncc\ThirdParty\Symfony\Process\Exception\RuntimeException;
use ncc\ThirdParty\Symfony\Process\Pipes\PipesInterface;
use ncc\ThirdParty\Symfony\Process\Pipes\UnixPipes;
use ncc\ThirdParty\Symfony\Process\Pipes\WindowsPipes;
@ -51,44 +51,47 @@ 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 = [];
/** @var resource|string|\Iterator|null */
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;
/** @var resource */
private $stdout;
/** @var resource */
private $stderr;
/** @var resource|null */
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 array $ignoredSignals = [];
private $useFileHandles = false;
/** @var PipesInterface */
private $processPipes;
private WindowsPipes|UnixPipes $processPipes;
private $latestSignal;
private ?int $latestSignal = null;
private ?int $cachedExitCode = null;
private static $sigchild;
private static ?bool $sigchild = null;
/**
* Exit codes translation table.
*
* 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',
@ -140,7 +143,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.');
@ -162,7 +165,6 @@ class Process implements \IteratorAggregate
$this->setInput($input);
$this->setTimeout($timeout);
$this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR;
$this->pty = false;
}
@ -187,7 +189,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;
@ -200,7 +202,7 @@ class Process implements \IteratorAggregate
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
public function __wakeup(): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
@ -234,15 +236,15 @@ class Process implements \IteratorAggregate
*
* @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
*/
public function run(callable $callback = null, array $env = []): int
public function run(?callable $callback = null, array $env = []): int
{
$this->start($callback, $env);
@ -261,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);
@ -285,13 +287,11 @@ class Process implements \IteratorAggregate
* @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
* @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 = [])
public function start(?callable $callback = null, array $env = []): void
{
if ($this->isRunning()) {
throw new RuntimeException('Process is already running.');
@ -300,8 +300,7 @@ class Process implements \IteratorAggregate
$this->resetProcessData();
$this->starttime = $this->lastOutputTime = microtime(true);
$this->callback = $this->buildCallback($callback);
$this->hasCallback = null !== $callback;
$descriptors = $this->getDescriptors();
$descriptors = $this->getDescriptors(null !== $callback);
if ($this->env) {
$env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env;
@ -310,29 +309,25 @@ class Process implements \IteratorAggregate
$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);
}
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'];
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';
// Workaround for the bug, when PTS functionality is enabled.
// @see : https://bugs.php.net/69442
$ptsWorkaround = fopen(__FILE__, 'r');
}
$envPairs = [];
@ -346,11 +341,41 @@ class Process implements \IteratorAggregate
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);
$lastError = null;
set_error_handler(function ($type, $msg) use (&$lastError) {
$lastError = $msg;
if (!\is_resource($this->process)) {
throw new RuntimeException('Unable to launch a new process.');
return true;
});
$oldMask = [];
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);
}
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
pcntl_sigprocmask(\SIG_SETMASK, $oldMask);
}
restore_error_handler();
}
if (!$process) {
throw new ProcessStartFailedException($this, $lastError);
}
$this->process = $process;
$this->status = self::STATUS_STARTED;
if (isset($descriptors[3])) {
@ -373,14 +398,14 @@ class Process implements \IteratorAggregate
* @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()
*
* @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.');
@ -407,7 +432,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__);
@ -880,7 +905,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()) {
@ -950,7 +975,7 @@ class Process implements \IteratorAggregate
*/
public function getCommandLine(): string
{
return \is_array($this->commandline) ? implode(' ', array_map($this->escapeArgument(...), $this->commandline)) : $this->commandline;
return $this->buildShellCommandline($this->commandline);
}
/**
@ -1119,7 +1144,7 @@ class Process implements \IteratorAggregate
*
* 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
*
@ -1142,11 +1167,9 @@ class Process implements \IteratorAggregate
* 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;
@ -1184,10 +1207,8 @@ class Process implements \IteratorAggregate
*
* 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.');
@ -1205,6 +1226,20 @@ class Process implements \IteratorAggregate
}
}
/**
* 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.
*/
@ -1212,7 +1247,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'));
}
/**
@ -1236,15 +1271,15 @@ class Process implements \IteratorAggregate
/**
* 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->hasCallback);
$this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $hasCallback);
} 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 || $hasCallback);
}
return $this->processPipes->getDescriptors();
@ -1258,7 +1293,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);
@ -1281,10 +1316,8 @@ class Process implements \IteratorAggregate
* 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;
@ -1293,6 +1326,19 @@ class Process implements \IteratorAggregate
$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()) {
@ -1388,8 +1434,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;
@ -1421,7 +1468,7 @@ class Process implements \IteratorAggregate
$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;
@ -1443,6 +1490,11 @@ class Process implements \IteratorAggregate
*/
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.');
@ -1485,8 +1537,18 @@ class Process implements \IteratorAggregate
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(
'/"(?:(

View file

@ -43,9 +43,6 @@ class ProcessUtils
if (\is_resource($input)) {
return $input;
}
if (\is_string($input)) {
return $input;
}
if (\is_scalar($input)) {
return (string) $input;
}

View file

@ -1 +1 @@
6.3.4
7.1.3