���� JFIF �� � ( %"1"%)+...383,7(-.-
![]() Server : Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/7.4.20 System : Linux st2.domain.com 3.10.0-1127.10.1.el7.x86_64 #1 SMP Wed Jun 3 14:28:03 UTC 2020 x86_64 User : apache ( 48) PHP Version : 7.4.20 Disable Function : NONE Directory : /proc/self/root/usr/share/php/Composer/Util/ |
<?php /* * This file is part of Composer. * * (c) Nils Adermann <[email protected]> * Jordi Boggiano <[email protected]> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\IO\IOInterface; use Symfony\Component\Process\Process; use Symfony\Component\Process\ProcessUtils; use Symfony\Component\Process\Exception\RuntimeException; use React\Promise\Promise; use React\Promise\PromiseInterface; /** * @author Robert Schönthal <[email protected]> * @author Jordi Boggiano <[email protected]> */ class ProcessExecutor { const STATUS_QUEUED = 1; const STATUS_STARTED = 2; const STATUS_COMPLETED = 3; const STATUS_FAILED = 4; const STATUS_ABORTED = 5; protected static $timeout = 300; protected $captureOutput; protected $errorOutput; protected $io; /** * @phpstan-var array<int, array<string, mixed>> */ private $jobs = array(); private $runningJobs = 0; private $maxJobs = 10; private $idGen = 0; private $allowAsync = false; public function __construct(IOInterface $io = null) { $this->io = $io; } /** * runs a process on the commandline * * @param string $command the command to execute * @param mixed $output the output will be written into this var if passed by ref * if a callable is passed it will be used as output handler * @param string $cwd the working directory * @return int statuscode */ public function execute($command, &$output = null, $cwd = null) { if (func_num_args() > 1) { return $this->doExecute($command, $cwd, false, $output); } return $this->doExecute($command, $cwd, false); } /** * runs a process on the commandline in TTY mode * * @param string $command the command to execute * @param string $cwd the working directory * @return int statuscode */ public function executeTty($command, $cwd = null) { if (Platform::isTty()) { return $this->doExecute($command, $cwd, true); } return $this->doExecute($command, $cwd, false); } private function doExecute($command, $cwd, $tty, &$output = null) { if ($this->io && $this->io->isDebug()) { $safeCommand = preg_replace_callback('{://(?P<user>[^:/\s]+):(?P<password>[^@\s/]+)@}i', function ($m) { // if the username looks like a long (12char+) hex string, or a modern github token (e.g. ghp_xxx) we obfuscate that if (preg_match('{^([a-f0-9]{12,}|gh[a-z]_[a-zA-Z0-9_]+)$}', $m['user'])) { return '://***:***@'; } return '://'.$m['user'].':***@'; }, $command); $safeCommand = preg_replace("{--password (.*[^\\\\]\') }", '--password \'***\' ', $safeCommand); $this->io->writeError('Executing command ('.($cwd ?: 'CWD').'): '.$safeCommand); } // TODO in 2.2, these two checks can be dropped as Symfony 4+ supports them out of the box // make sure that null translate to the proper directory in case the dir is a symlink // and we call a git command, because msysgit does not handle symlinks properly if (null === $cwd && Platform::isWindows() && false !== strpos($command, 'git') && getcwd()) { $cwd = realpath(getcwd()); } if (null !== $cwd && !is_dir($cwd)) { throw new \RuntimeException('The given CWD for the process does not exist: '.$cwd); } $this->captureOutput = func_num_args() > 3; $this->errorOutput = null; // TODO in v3, commands should be passed in as arrays of cmd + args if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { $process = Process::fromShellCommandline($command, $cwd, null, null, static::getTimeout()); } else { $process = new Process($command, $cwd, null, null, static::getTimeout()); } if (!Platform::isWindows() && $tty) { try { $process->setTty(true); } catch (RuntimeException $e) { // ignore TTY enabling errors } } $callback = is_callable($output) ? $output : array($this, 'outputHandler'); $process->run($callback); if ($this->captureOutput && !is_callable($output)) { $output = $process->getOutput(); } $this->errorOutput = $process->getErrorOutput(); return $process->getExitCode(); } /** * starts a process on the commandline in async mode * * @param string $command the command to execute * @param string $cwd the working directory * @return PromiseInterface */ public function executeAsync($command, $cwd = null) { if (!$this->allowAsync) { throw new \LogicException('You must use the ProcessExecutor instance which is part of a Composer\Loop instance to be able to run async processes'); } $job = array( 'id' => $this->idGen++, 'status' => self::STATUS_QUEUED, 'command' => $command, 'cwd' => $cwd, ); $resolver = function ($resolve, $reject) use (&$job) { $job['status'] = ProcessExecutor::STATUS_QUEUED; $job['resolve'] = $resolve; $job['reject'] = $reject; }; $self = $this; $canceler = function () use (&$job) { if ($job['status'] === ProcessExecutor::STATUS_QUEUED) { $job['status'] = ProcessExecutor::STATUS_ABORTED; } if ($job['status'] !== ProcessExecutor::STATUS_STARTED) { return; } $job['status'] = ProcessExecutor::STATUS_ABORTED; try { if (defined('SIGINT')) { $job['process']->signal(SIGINT); } } catch (\Exception $e) { // signal can throw in various conditions, but we don't care if it fails } $job['process']->stop(1); throw new \RuntimeException('Aborted process'); }; $promise = new Promise($resolver, $canceler); $promise = $promise->then(function () use (&$job, $self) { if ($job['process']->isSuccessful()) { $job['status'] = ProcessExecutor::STATUS_COMPLETED; } else { $job['status'] = ProcessExecutor::STATUS_FAILED; } // TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped $self->markJobDone(); return $job['process']; }, function ($e) use (&$job, $self) { $job['status'] = ProcessExecutor::STATUS_FAILED; $self->markJobDone(); throw $e; }); $this->jobs[$job['id']] = &$job; if ($this->runningJobs < $this->maxJobs) { $this->startJob($job['id']); } return $promise; } private function startJob($id) { $job = &$this->jobs[$id]; if ($job['status'] !== self::STATUS_QUEUED) { return; } // start job $job['status'] = self::STATUS_STARTED; $this->runningJobs++; $command = $job['command']; $cwd = $job['cwd']; if ($this->io && $this->io->isDebug()) { $safeCommand = preg_replace_callback('{://(?P<user>[^:/\s]+):(?P<password>[^@\s/]+)@}i', function ($m) { if (preg_match('{^[a-f0-9]{12,}$}', $m['user'])) { return '://***:***@'; } return '://'.$m['user'].':***@'; }, $command); $safeCommand = preg_replace("{--password (.*[^\\\\]\') }", '--password \'***\' ', $safeCommand); $this->io->writeError('Executing async command ('.($cwd ?: 'CWD').'): '.$safeCommand); } // TODO in 2.2, these two checks can be dropped as Symfony 4+ supports them out of the box // make sure that null translate to the proper directory in case the dir is a symlink // and we call a git command, because msysgit does not handle symlinks properly if (null === $cwd && Platform::isWindows() && false !== strpos($command, 'git') && getcwd()) { $cwd = realpath(getcwd()); } if (null !== $cwd && !is_dir($cwd)) { throw new \RuntimeException('The given CWD for the process does not exist: '.$cwd); } try { // TODO in v3, commands should be passed in as arrays of cmd + args if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { $process = Process::fromShellCommandline($command, $cwd, null, null, static::getTimeout()); } else { $process = new Process($command, $cwd, null, null, static::getTimeout()); } } catch (\Exception $e) { call_user_func($job['reject'], $e); return; } catch (\Throwable $e) { call_user_func($job['reject'], $e); return; } $job['process'] = $process; try { $process->start(); } catch (\Exception $e) { call_user_func($job['reject'], $e); return; } catch (\Throwable $e) { call_user_func($job['reject'], $e); return; } } public function wait($index = null) { while (true) { if (!$this->countActiveJobs($index)) { return; } usleep(1000); } } /** * @internal */ public function enableAsync() { $this->allowAsync = true; } /** * @internal * * @return int number of active (queued or started) jobs */ public function countActiveJobs($index = null) { // tick foreach ($this->jobs as $job) { if ($job['status'] === self::STATUS_STARTED) { if (!$job['process']->isRunning()) { call_user_func($job['resolve'], $job['process']); } } if ($this->runningJobs < $this->maxJobs) { if ($job['status'] === self::STATUS_QUEUED) { $this->startJob($job['id']); } } } if (null !== $index) { return $this->jobs[$index]['status'] < self::STATUS_COMPLETED ? 1 : 0; } $active = 0; foreach ($this->jobs as $job) { if ($job['status'] < self::STATUS_COMPLETED) { $active++; } else { unset($this->jobs[$job['id']]); } } return $active; } /** * @private */ public function markJobDone() { $this->runningJobs--; } /** * @return string[] */ public function splitLines($output) { $output = trim($output); return ((string) $output === '') ? array() : preg_split('{\r?\n}', $output); } /** * Get any error output from the last command * * @return string */ public function getErrorOutput() { return $this->errorOutput; } public function outputHandler($type, $buffer) { if ($this->captureOutput) { return; } if (null === $this->io) { echo $buffer; return; } if (Process::ERR === $type) { $this->io->writeErrorRaw($buffer, false); } else { $this->io->writeRaw($buffer, false); } } /** * @return int the timeout in seconds */ public static function getTimeout() { return static::$timeout; } /** * @param int $timeout the timeout in seconds */ public static function setTimeout($timeout) { static::$timeout = $timeout; } /** * Escapes a string to be used as a shell argument. * * @param string $argument The argument that will be escaped * * @return string The escaped argument */ public static function escape($argument) { return self::escapeArgument($argument); } /** * Copy of ProcessUtils::escapeArgument() that is deprecated in Symfony 3.3 and removed in Symfony 4. * * @param string $argument * * @return string */ private static function escapeArgument($argument) { //Fix for PHP bug #43784 escapeshellarg removes % from given string //Fix for PHP bug #49446 escapeshellarg doesn't work on Windows //@see https://bugs.php.net/bug.php?id=43784 //@see https://bugs.php.net/bug.php?id=49446 if ('\\' === DIRECTORY_SEPARATOR) { if ((string) $argument === '') { return escapeshellarg($argument); } $escapedArgument = ''; $quote = false; foreach (preg_split('/(")/', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { if ('"' === $part) { $escapedArgument .= '\\"'; } elseif (self::isSurroundedBy($part, '%')) { // Avoid environment variable expansion $escapedArgument .= '^%"'.substr($part, 1, -1).'"^%'; } else { // escape trailing backslash if ('\\' === substr($part, -1)) { $part .= '\\'; } $quote = true; $escapedArgument .= $part; } } if ($quote) { $escapedArgument = '"'.$escapedArgument.'"'; } return $escapedArgument; } return "'".str_replace("'", "'\\''", $argument)."'"; } private static function isSurroundedBy($arg, $char) { return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1]; } }