Verzeichnisstruktur phpBB-3.3.15
- Veröffentlicht
- 28.08.2024
So funktioniert es
|
Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück |
Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis. |
|
(Beispiel Datei-Icons)
|
Auf das Icon klicken um den Quellcode anzuzeigen |
Process.php
0001 <?php
0002
0003 /*
0004 * This file is part of the Symfony package.
0005 *
0006 * (c) Fabien Potencier <fabien@symfony.com>
0007 *
0008 * For the full copyright and license information, please view the LICENSE
0009 * file that was distributed with this source code.
0010 */
0011
0012 namespace Symfony\Component\Process;
0013
0014 use Symfony\Component\Process\Exception\InvalidArgumentException;
0015 use Symfony\Component\Process\Exception\LogicException;
0016 use Symfony\Component\Process\Exception\ProcessFailedException;
0017 use Symfony\Component\Process\Exception\ProcessTimedOutException;
0018 use Symfony\Component\Process\Exception\RuntimeException;
0019 use Symfony\Component\Process\Pipes\PipesInterface;
0020 use Symfony\Component\Process\Pipes\UnixPipes;
0021 use Symfony\Component\Process\Pipes\WindowsPipes;
0022
0023 /**
0024 * Process is a thin wrapper around proc_* functions to easily
0025 * start independent PHP processes.
0026 *
0027 * @author Fabien Potencier <fabien@symfony.com>
0028 * @author Romain Neutron <imprec@gmail.com>
0029 */
0030 class Process implements \IteratorAggregate
0031 {
0032 const ERR = 'err';
0033 const OUT = 'out';
0034
0035 const STATUS_READY = 'ready';
0036 const STATUS_STARTED = 'started';
0037 const STATUS_TERMINATED = 'terminated';
0038
0039 const STDIN = 0;
0040 const STDOUT = 1;
0041 const STDERR = 2;
0042
0043 // Timeout Precision in seconds.
0044 const TIMEOUT_PRECISION = 0.2;
0045
0046 const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking
0047 const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory
0048 const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating
0049 const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating
0050
0051 private $callback;
0052 private $hasCallback = false;
0053 private $commandline;
0054 private $cwd;
0055 private $env;
0056 private $input;
0057 private $starttime;
0058 private $lastOutputTime;
0059 private $timeout;
0060 private $idleTimeout;
0061 private $options = ['suppress_errors' => true];
0062 private $exitcode;
0063 private $fallbackStatus = [];
0064 private $processInformation;
0065 private $outputDisabled = false;
0066 private $stdout;
0067 private $stderr;
0068 private $enhanceWindowsCompatibility = true;
0069 private $enhanceSigchildCompatibility;
0070 private $process;
0071 private $status = self::STATUS_READY;
0072 private $incrementalOutputOffset = 0;
0073 private $incrementalErrorOutputOffset = 0;
0074 private $tty = false;
0075 private $pty;
0076 private $inheritEnv = false;
0077
0078 private $useFileHandles = false;
0079 /** @var PipesInterface */
0080 private $processPipes;
0081
0082 private $latestSignal;
0083
0084 private static $sigchild;
0085
0086 /**
0087 * Exit codes translation table.
0088 *
0089 * User-defined errors must use exit codes in the 64-113 range.
0090 */
0091 public static $exitCodes = [
0092 0 => 'OK',
0093 1 => 'General error',
0094 2 => 'Misuse of shell builtins',
0095
0096 126 => 'Invoked command cannot execute',
0097 127 => 'Command not found',
0098 128 => 'Invalid exit argument',
0099
0100 // signals
0101 129 => 'Hangup',
0102 130 => 'Interrupt',
0103 131 => 'Quit and dump core',
0104 132 => 'Illegal instruction',
0105 133 => 'Trace/breakpoint trap',
0106 134 => 'Process aborted',
0107 135 => 'Bus error: "access to undefined portion of memory object"',
0108 136 => 'Floating point exception: "erroneous arithmetic operation"',
0109 137 => 'Kill (terminate immediately)',
0110 138 => 'User-defined 1',
0111 139 => 'Segmentation violation',
0112 140 => 'User-defined 2',
0113 141 => 'Write to pipe with no one reading',
0114 142 => 'Signal raised by alarm',
0115 143 => 'Termination (request to terminate)',
0116 // 144 - not defined
0117 145 => 'Child process terminated, stopped (or continued*)',
0118 146 => 'Continue if stopped',
0119 147 => 'Stop executing temporarily',
0120 148 => 'Terminal stop signal',
0121 149 => 'Background process attempting to read from tty ("in")',
0122 150 => 'Background process attempting to write to tty ("out")',
0123 151 => 'Urgent data available on socket',
0124 152 => 'CPU time limit exceeded',
0125 153 => 'File size limit exceeded',
0126 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
0127 155 => 'Profiling timer expired',
0128 // 156 - not defined
0129 157 => 'Pollable event',
0130 // 158 - not defined
0131 159 => 'Bad syscall',
0132 ];
0133
0134 /**
0135 * @param string|array $commandline The command line to run
0136 * @param string|null $cwd The working directory or null to use the working dir of the current PHP process
0137 * @param array|null $env The environment variables or null to use the same environment as the current PHP process
0138 * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input
0139 * @param int|float|null $timeout The timeout in seconds or null to disable
0140 * @param array $options An array of options for proc_open
0141 *
0142 * @throws RuntimeException When proc_open is not installed
0143 */
0144 public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = null)
0145 {
0146 if (!\function_exists('proc_open')) {
0147 throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.');
0148 }
0149
0150 $this->commandline = $commandline;
0151 $this->cwd = $cwd;
0152
0153 // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started
0154 // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected
0155 // @see : https://bugs.php.net/51800
0156 // @see : https://bugs.php.net/50524
0157 if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) {
0158 $this->cwd = getcwd();
0159 }
0160 if (null !== $env) {
0161 $this->setEnv($env);
0162 }
0163
0164 $this->setInput($input);
0165 $this->setTimeout($timeout);
0166 $this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR;
0167 $this->pty = false;
0168 $this->enhanceSigchildCompatibility = '\\' !== \DIRECTORY_SEPARATOR && $this->isSigchildEnabled();
0169 if (null !== $options) {
0170 @trigger_error(sprintf('The $options parameter of the %s constructor is deprecated since Symfony 3.3 and will be removed in 4.0.', __CLASS__), \E_USER_DEPRECATED);
0171 $this->options = array_replace($this->options, $options);
0172 }
0173 }
0174
0175 public function __destruct()
0176 {
0177 $this->stop(0);
0178 }
0179
0180 public function __clone()
0181 {
0182 $this->resetProcessData();
0183 }
0184
0185 /**
0186 * Runs the process.
0187 *
0188 * The callback receives the type of output (out or err) and
0189 * some bytes from the output in real-time. It allows to have feedback
0190 * from the independent process during execution.
0191 *
0192 * The STDOUT and STDERR are also available after the process is finished
0193 * via the getOutput() and getErrorOutput() methods.
0194 *
0195 * @param callable|null $callback A PHP callback to run whenever there is some
0196 * output available on STDOUT or STDERR
0197 *
0198 * @return int The exit status code
0199 *
0200 * @throws RuntimeException When process can't be launched
0201 * @throws RuntimeException When process stopped after receiving signal
0202 * @throws LogicException In case a callback is provided and output has been disabled
0203 *
0204 * @final since version 3.3
0205 */
0206 public function run($callback = null/*, array $env = []*/)
0207 {
0208 $env = 1 < \func_num_args() ? func_get_arg(1) : null;
0209 $this->start($callback, $env);
0210
0211 return $this->wait();
0212 }
0213
0214 /**
0215 * Runs the process.
0216 *
0217 * This is identical to run() except that an exception is thrown if the process
0218 * exits with a non-zero exit code.
0219 *
0220 * @return $this
0221 *
0222 * @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled
0223 * @throws ProcessFailedException if the process didn't terminate successfully
0224 *
0225 * @final since version 3.3
0226 */
0227 public function mustRun(callable $callback = null/*, array $env = []*/)
0228 {
0229 if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
0230 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
0231 }
0232 $env = 1 < \func_num_args() ? func_get_arg(1) : null;
0233
0234 if (0 !== $this->run($callback, $env)) {
0235 throw new ProcessFailedException($this);
0236 }
0237
0238 return $this;
0239 }
0240
0241 /**
0242 * Starts the process and returns after writing the input to STDIN.
0243 *
0244 * This method blocks until all STDIN data is sent to the process then it
0245 * returns while the process runs in the background.
0246 *
0247 * The termination of the process can be awaited with wait().
0248 *
0249 * The callback receives the type of output (out or err) and some bytes from
0250 * the output in real-time while writing the standard input to the process.
0251 * It allows to have feedback from the independent process during execution.
0252 *
0253 * @param callable|null $callback A PHP callback to run whenever there is some
0254 * output available on STDOUT or STDERR
0255 *
0256 * @throws RuntimeException When process can't be launched
0257 * @throws RuntimeException When process is already running
0258 * @throws LogicException In case a callback is provided and output has been disabled
0259 */
0260 public function start(callable $callback = null/*, array $env = [*/)
0261 {
0262 if ($this->isRunning()) {
0263 throw new RuntimeException('Process is already running.');
0264 }
0265 if (2 <= \func_num_args()) {
0266 $env = func_get_arg(1);
0267 } else {
0268 if (__CLASS__ !== static::class) {
0269 $r = new \ReflectionMethod($this, __FUNCTION__);
0270 if (__CLASS__ !== $r->getDeclaringClass()->getName() && (2 > $r->getNumberOfParameters() || 'env' !== $r->getParameters()[1]->name)) {
0271 @trigger_error(sprintf('The %s::start() method expects a second "$env" argument since Symfony 3.3. It will be made mandatory in 4.0.', static::class), \E_USER_DEPRECATED);
0272 }
0273 }
0274 $env = null;
0275 }
0276
0277 $this->resetProcessData();
0278 $this->starttime = $this->lastOutputTime = microtime(true);
0279 $this->callback = $this->buildCallback($callback);
0280 $this->hasCallback = null !== $callback;
0281 $descriptors = $this->getDescriptors();
0282 $inheritEnv = $this->inheritEnv;
0283
0284 if (\is_array($commandline = $this->commandline)) {
0285 $commandline = implode(' ', array_map([$this, 'escapeArgument'], $commandline));
0286
0287 if ('\\' !== \DIRECTORY_SEPARATOR) {
0288 // exec is mandatory to deal with sending a signal to the process
0289 $commandline = 'exec '.$commandline;
0290 }
0291 }
0292
0293 if (null === $env) {
0294 $env = $this->env;
0295 } else {
0296 if ($this->env) {
0297 $env += $this->env;
0298 }
0299 $inheritEnv = true;
0300 }
0301
0302 if (null !== $env && $inheritEnv) {
0303 $env += $this->getDefaultEnv();
0304 } elseif (null !== $env) {
0305 @trigger_error('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', \E_USER_DEPRECATED);
0306 } else {
0307 $env = $this->getDefaultEnv();
0308 }
0309 if ('\\' === \DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) {
0310 $this->options['bypass_shell'] = true;
0311 $commandline = $this->prepareWindowsCommandLine($commandline, $env);
0312 } elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
0313 // last exit code is output on the fourth pipe and caught to work around --enable-sigchild
0314 $descriptors[3] = ['pipe', 'w'];
0315
0316 // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input
0317 $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;';
0318 $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code';
0319
0320 // Workaround for the bug, when PTS functionality is enabled.
0321 // @see : https://bugs.php.net/69442
0322 $ptsWorkaround = fopen(__FILE__, 'r');
0323 }
0324 if (\defined('HHVM_VERSION')) {
0325 $envPairs = $env;
0326 } else {
0327 $envPairs = [];
0328 foreach ($env as $k => $v) {
0329 if (false !== $v) {
0330 $envPairs[] = $k.'='.$v;
0331 }
0332 }
0333 }
0334
0335 if (!is_dir($this->cwd)) {
0336 @trigger_error('The provided cwd does not exist. Command is currently ran against getcwd(). This behavior is deprecated since Symfony 3.4 and will be removed in 4.0.', \E_USER_DEPRECATED);
0337 }
0338
0339 $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options);
0340
0341 if (!\is_resource($this->process)) {
0342 throw new RuntimeException('Unable to launch a new process.');
0343 }
0344 $this->status = self::STATUS_STARTED;
0345
0346 if (isset($descriptors[3])) {
0347 $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]);
0348 }
0349
0350 if ($this->tty) {
0351 return;
0352 }
0353
0354 $this->updateStatus(false);
0355 $this->checkTimeout();
0356 }
0357
0358 /**
0359 * Restarts the process.
0360 *
0361 * Be warned that the process is cloned before being started.
0362 *
0363 * @param callable|null $callback A PHP callback to run whenever there is some
0364 * output available on STDOUT or STDERR
0365 *
0366 * @return static
0367 *
0368 * @throws RuntimeException When process can't be launched
0369 * @throws RuntimeException When process is already running
0370 *
0371 * @see start()
0372 *
0373 * @final since version 3.3
0374 */
0375 public function restart(callable $callback = null/*, array $env = []*/)
0376 {
0377 if ($this->isRunning()) {
0378 throw new RuntimeException('Process is already running.');
0379 }
0380 $env = 1 < \func_num_args() ? func_get_arg(1) : null;
0381
0382 $process = clone $this;
0383 $process->start($callback, $env);
0384
0385 return $process;
0386 }
0387
0388 /**
0389 * Waits for the process to terminate.
0390 *
0391 * The callback receives the type of output (out or err) and some bytes
0392 * from the output in real-time while writing the standard input to the process.
0393 * It allows to have feedback from the independent process during execution.
0394 *
0395 * @param callable|null $callback A valid PHP callback
0396 *
0397 * @return int The exitcode of the process
0398 *
0399 * @throws RuntimeException When process timed out
0400 * @throws RuntimeException When process stopped after receiving signal
0401 * @throws LogicException When process is not yet started
0402 */
0403 public function wait(callable $callback = null)
0404 {
0405 $this->requireProcessIsStarted(__FUNCTION__);
0406
0407 $this->updateStatus(false);
0408
0409 if (null !== $callback) {
0410 if (!$this->processPipes->haveReadSupport()) {
0411 $this->stop(0);
0412 throw new \LogicException('Pass the callback to the Process::start method or enableOutput to use a callback with Process::wait.');
0413 }
0414 $this->callback = $this->buildCallback($callback);
0415 }
0416
0417 do {
0418 $this->checkTimeout();
0419 $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
0420 $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
0421 } while ($running);
0422
0423 while ($this->isRunning()) {
0424 $this->checkTimeout();
0425 usleep(1000);
0426 }
0427
0428 if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
0429 throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig']));
0430 }
0431
0432 return $this->exitcode;
0433 }
0434
0435 /**
0436 * Returns the Pid (process identifier), if applicable.
0437 *
0438 * @return int|null The process id if running, null otherwise
0439 */
0440 public function getPid()
0441 {
0442 return $this->isRunning() ? $this->processInformation['pid'] : null;
0443 }
0444
0445 /**
0446 * Sends a POSIX signal to the process.
0447 *
0448 * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants)
0449 *
0450 * @return $this
0451 *
0452 * @throws LogicException In case the process is not running
0453 * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
0454 * @throws RuntimeException In case of failure
0455 */
0456 public function signal($signal)
0457 {
0458 $this->doSignal($signal, true);
0459
0460 return $this;
0461 }
0462
0463 /**
0464 * Disables fetching output and error output from the underlying process.
0465 *
0466 * @return $this
0467 *
0468 * @throws RuntimeException In case the process is already running
0469 * @throws LogicException if an idle timeout is set
0470 */
0471 public function disableOutput()
0472 {
0473 if ($this->isRunning()) {
0474 throw new RuntimeException('Disabling output while the process is running is not possible.');
0475 }
0476 if (null !== $this->idleTimeout) {
0477 throw new LogicException('Output can not be disabled while an idle timeout is set.');
0478 }
0479
0480 $this->outputDisabled = true;
0481
0482 return $this;
0483 }
0484
0485 /**
0486 * Enables fetching output and error output from the underlying process.
0487 *
0488 * @return $this
0489 *
0490 * @throws RuntimeException In case the process is already running
0491 */
0492 public function enableOutput()
0493 {
0494 if ($this->isRunning()) {
0495 throw new RuntimeException('Enabling output while the process is running is not possible.');
0496 }
0497
0498 $this->outputDisabled = false;
0499
0500 return $this;
0501 }
0502
0503 /**
0504 * Returns true in case the output is disabled, false otherwise.
0505 *
0506 * @return bool
0507 */
0508 public function isOutputDisabled()
0509 {
0510 return $this->outputDisabled;
0511 }
0512
0513 /**
0514 * Returns the current output of the process (STDOUT).
0515 *
0516 * @return string The process output
0517 *
0518 * @throws LogicException in case the output has been disabled
0519 * @throws LogicException In case the process is not started
0520 */
0521 public function getOutput()
0522 {
0523 $this->readPipesForOutput(__FUNCTION__);
0524
0525 if (false === $ret = stream_get_contents($this->stdout, -1, 0)) {
0526 return '';
0527 }
0528
0529 return $ret;
0530 }
0531
0532 /**
0533 * Returns the output incrementally.
0534 *
0535 * In comparison with the getOutput method which always return the whole
0536 * output, this one returns the new output since the last call.
0537 *
0538 * @return string The process output since the last call
0539 *
0540 * @throws LogicException in case the output has been disabled
0541 * @throws LogicException In case the process is not started
0542 */
0543 public function getIncrementalOutput()
0544 {
0545 $this->readPipesForOutput(__FUNCTION__);
0546
0547 $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
0548 $this->incrementalOutputOffset = ftell($this->stdout);
0549
0550 if (false === $latest) {
0551 return '';
0552 }
0553
0554 return $latest;
0555 }
0556
0557 /**
0558 * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR).
0559 *
0560 * @param int $flags A bit field of Process::ITER_* flags
0561 *
0562 * @throws LogicException in case the output has been disabled
0563 * @throws LogicException In case the process is not started
0564 *
0565 * @return \Generator
0566 */
0567 public function getIterator($flags = 0)
0568 {
0569 $this->readPipesForOutput(__FUNCTION__, false);
0570
0571 $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags);
0572 $blocking = !(self::ITER_NON_BLOCKING & $flags);
0573 $yieldOut = !(self::ITER_SKIP_OUT & $flags);
0574 $yieldErr = !(self::ITER_SKIP_ERR & $flags);
0575
0576 while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) {
0577 if ($yieldOut) {
0578 $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
0579
0580 if (isset($out[0])) {
0581 if ($clearOutput) {
0582 $this->clearOutput();
0583 } else {
0584 $this->incrementalOutputOffset = ftell($this->stdout);
0585 }
0586
0587 yield self::OUT => $out;
0588 }
0589 }
0590
0591 if ($yieldErr) {
0592 $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
0593
0594 if (isset($err[0])) {
0595 if ($clearOutput) {
0596 $this->clearErrorOutput();
0597 } else {
0598 $this->incrementalErrorOutputOffset = ftell($this->stderr);
0599 }
0600
0601 yield self::ERR => $err;
0602 }
0603 }
0604
0605 if (!$blocking && !isset($out[0]) && !isset($err[0])) {
0606 yield self::OUT => '';
0607 }
0608
0609 $this->checkTimeout();
0610 $this->readPipesForOutput(__FUNCTION__, $blocking);
0611 }
0612 }
0613
0614 /**
0615 * Clears the process output.
0616 *
0617 * @return $this
0618 */
0619 public function clearOutput()
0620 {
0621 ftruncate($this->stdout, 0);
0622 fseek($this->stdout, 0);
0623 $this->incrementalOutputOffset = 0;
0624
0625 return $this;
0626 }
0627
0628 /**
0629 * Returns the current error output of the process (STDERR).
0630 *
0631 * @return string The process error output
0632 *
0633 * @throws LogicException in case the output has been disabled
0634 * @throws LogicException In case the process is not started
0635 */
0636 public function getErrorOutput()
0637 {
0638 $this->readPipesForOutput(__FUNCTION__);
0639
0640 if (false === $ret = stream_get_contents($this->stderr, -1, 0)) {
0641 return '';
0642 }
0643
0644 return $ret;
0645 }
0646
0647 /**
0648 * Returns the errorOutput incrementally.
0649 *
0650 * In comparison with the getErrorOutput method which always return the
0651 * whole error output, this one returns the new error output since the last
0652 * call.
0653 *
0654 * @return string The process error output since the last call
0655 *
0656 * @throws LogicException in case the output has been disabled
0657 * @throws LogicException In case the process is not started
0658 */
0659 public function getIncrementalErrorOutput()
0660 {
0661 $this->readPipesForOutput(__FUNCTION__);
0662
0663 $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
0664 $this->incrementalErrorOutputOffset = ftell($this->stderr);
0665
0666 if (false === $latest) {
0667 return '';
0668 }
0669
0670 return $latest;
0671 }
0672
0673 /**
0674 * Clears the process output.
0675 *
0676 * @return $this
0677 */
0678 public function clearErrorOutput()
0679 {
0680 ftruncate($this->stderr, 0);
0681 fseek($this->stderr, 0);
0682 $this->incrementalErrorOutputOffset = 0;
0683
0684 return $this;
0685 }
0686
0687 /**
0688 * Returns the exit code returned by the process.
0689 *
0690 * @return int|null The exit status code, null if the Process is not terminated
0691 *
0692 * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled
0693 */
0694 public function getExitCode()
0695 {
0696 if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
0697 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
0698 }
0699
0700 $this->updateStatus(false);
0701
0702 return $this->exitcode;
0703 }
0704
0705 /**
0706 * Returns a string representation for the exit code returned by the process.
0707 *
0708 * This method relies on the Unix exit code status standardization
0709 * and might not be relevant for other operating systems.
0710 *
0711 * @return string|null A string representation for the exit status code, null if the Process is not terminated
0712 *
0713 * @see http://tldp.org/LDP/abs/html/exitcodes.html
0714 * @see http://en.wikipedia.org/wiki/Unix_signal
0715 */
0716 public function getExitCodeText()
0717 {
0718 if (null === $exitcode = $this->getExitCode()) {
0719 return null;
0720 }
0721
0722 return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error';
0723 }
0724
0725 /**
0726 * Checks if the process ended successfully.
0727 *
0728 * @return bool true if the process ended successfully, false otherwise
0729 */
0730 public function isSuccessful()
0731 {
0732 return 0 === $this->getExitCode();
0733 }
0734
0735 /**
0736 * Returns true if the child process has been terminated by an uncaught signal.
0737 *
0738 * It always returns false on Windows.
0739 *
0740 * @return bool
0741 *
0742 * @throws RuntimeException In case --enable-sigchild is activated
0743 * @throws LogicException In case the process is not terminated
0744 */
0745 public function hasBeenSignaled()
0746 {
0747 $this->requireProcessIsTerminated(__FUNCTION__);
0748
0749 if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
0750 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
0751 }
0752
0753 return $this->processInformation['signaled'];
0754 }
0755
0756 /**
0757 * Returns the number of the signal that caused the child process to terminate its execution.
0758 *
0759 * It is only meaningful if hasBeenSignaled() returns true.
0760 *
0761 * @return int
0762 *
0763 * @throws RuntimeException In case --enable-sigchild is activated
0764 * @throws LogicException In case the process is not terminated
0765 */
0766 public function getTermSignal()
0767 {
0768 $this->requireProcessIsTerminated(__FUNCTION__);
0769
0770 if ($this->isSigchildEnabled() && (!$this->enhanceSigchildCompatibility || -1 === $this->processInformation['termsig'])) {
0771 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
0772 }
0773
0774 return $this->processInformation['termsig'];
0775 }
0776
0777 /**
0778 * Returns true if the child process has been stopped by a signal.
0779 *
0780 * It always returns false on Windows.
0781 *
0782 * @return bool
0783 *
0784 * @throws LogicException In case the process is not terminated
0785 */
0786 public function hasBeenStopped()
0787 {
0788 $this->requireProcessIsTerminated(__FUNCTION__);
0789
0790 return $this->processInformation['stopped'];
0791 }
0792
0793 /**
0794 * Returns the number of the signal that caused the child process to stop its execution.
0795 *
0796 * It is only meaningful if hasBeenStopped() returns true.
0797 *
0798 * @return int
0799 *
0800 * @throws LogicException In case the process is not terminated
0801 */
0802 public function getStopSignal()
0803 {
0804 $this->requireProcessIsTerminated(__FUNCTION__);
0805
0806 return $this->processInformation['stopsig'];
0807 }
0808
0809 /**
0810 * Checks if the process is currently running.
0811 *
0812 * @return bool true if the process is currently running, false otherwise
0813 */
0814 public function isRunning()
0815 {
0816 if (self::STATUS_STARTED !== $this->status) {
0817 return false;
0818 }
0819
0820 $this->updateStatus(false);
0821
0822 return $this->processInformation['running'];
0823 }
0824
0825 /**
0826 * Checks if the process has been started with no regard to the current state.
0827 *
0828 * @return bool true if status is ready, false otherwise
0829 */
0830 public function isStarted()
0831 {
0832 return self::STATUS_READY != $this->status;
0833 }
0834
0835 /**
0836 * Checks if the process is terminated.
0837 *
0838 * @return bool true if process is terminated, false otherwise
0839 */
0840 public function isTerminated()
0841 {
0842 $this->updateStatus(false);
0843
0844 return self::STATUS_TERMINATED == $this->status;
0845 }
0846
0847 /**
0848 * Gets the process status.
0849 *
0850 * The status is one of: ready, started, terminated.
0851 *
0852 * @return string The current process status
0853 */
0854 public function getStatus()
0855 {
0856 $this->updateStatus(false);
0857
0858 return $this->status;
0859 }
0860
0861 /**
0862 * Stops the process.
0863 *
0864 * @param int|float $timeout The timeout in seconds
0865 * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
0866 *
0867 * @return int|null The exit-code of the process or null if it's not running
0868 */
0869 public function stop($timeout = 10, $signal = null)
0870 {
0871 $timeoutMicro = microtime(true) + $timeout;
0872 if ($this->isRunning()) {
0873 // given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here
0874 $this->doSignal(15, false);
0875 do {
0876 usleep(1000);
0877 } while ($this->isRunning() && microtime(true) < $timeoutMicro);
0878
0879 if ($this->isRunning()) {
0880 // Avoid exception here: process is supposed to be running, but it might have stopped just
0881 // after this line. In any case, let's silently discard the error, we cannot do anything.
0882 $this->doSignal($signal ?: 9, false);
0883 }
0884 }
0885
0886 if ($this->isRunning()) {
0887 if (isset($this->fallbackStatus['pid'])) {
0888 unset($this->fallbackStatus['pid']);
0889
0890 return $this->stop(0, $signal);
0891 }
0892 $this->close();
0893 }
0894
0895 return $this->exitcode;
0896 }
0897
0898 /**
0899 * Adds a line to the STDOUT stream.
0900 *
0901 * @internal
0902 *
0903 * @param string $line The line to append
0904 */
0905 public function addOutput($line)
0906 {
0907 $this->lastOutputTime = microtime(true);
0908
0909 fseek($this->stdout, 0, \SEEK_END);
0910 fwrite($this->stdout, $line);
0911 fseek($this->stdout, $this->incrementalOutputOffset);
0912 }
0913
0914 /**
0915 * Adds a line to the STDERR stream.
0916 *
0917 * @internal
0918 *
0919 * @param string $line The line to append
0920 */
0921 public function addErrorOutput($line)
0922 {
0923 $this->lastOutputTime = microtime(true);
0924
0925 fseek($this->stderr, 0, \SEEK_END);
0926 fwrite($this->stderr, $line);
0927 fseek($this->stderr, $this->incrementalErrorOutputOffset);
0928 }
0929
0930 /**
0931 * Gets the command line to be executed.
0932 *
0933 * @return string The command to execute
0934 */
0935 public function getCommandLine()
0936 {
0937 return \is_array($this->commandline) ? implode(' ', array_map([$this, 'escapeArgument'], $this->commandline)) : $this->commandline;
0938 }
0939
0940 /**
0941 * Sets the command line to be executed.
0942 *
0943 * @param string|array $commandline The command to execute
0944 *
0945 * @return $this
0946 */
0947 public function setCommandLine($commandline)
0948 {
0949 $this->commandline = $commandline;
0950
0951 return $this;
0952 }
0953
0954 /**
0955 * Gets the process timeout (max. runtime).
0956 *
0957 * @return float|null The timeout in seconds or null if it's disabled
0958 */
0959 public function getTimeout()
0960 {
0961 return $this->timeout;
0962 }
0963
0964 /**
0965 * Gets the process idle timeout (max. time since last output).
0966 *
0967 * @return float|null The timeout in seconds or null if it's disabled
0968 */
0969 public function getIdleTimeout()
0970 {
0971 return $this->idleTimeout;
0972 }
0973
0974 /**
0975 * Sets the process timeout (max. runtime) in seconds.
0976 *
0977 * To disable the timeout, set this value to null.
0978 *
0979 * @param int|float|null $timeout The timeout in seconds
0980 *
0981 * @return $this
0982 *
0983 * @throws InvalidArgumentException if the timeout is negative
0984 */
0985 public function setTimeout($timeout)
0986 {
0987 $this->timeout = $this->validateTimeout($timeout);
0988
0989 return $this;
0990 }
0991
0992 /**
0993 * Sets the process idle timeout (max. time since last output).
0994 *
0995 * To disable the timeout, set this value to null.
0996 *
0997 * @param int|float|null $timeout The timeout in seconds
0998 *
0999 * @return $this
1000 *
1001 * @throws LogicException if the output is disabled
1002 * @throws InvalidArgumentException if the timeout is negative
1003 */
1004 public function setIdleTimeout($timeout)
1005 {
1006 if (null !== $timeout && $this->outputDisabled) {
1007 throw new LogicException('Idle timeout can not be set while the output is disabled.');
1008 }
1009
1010 $this->idleTimeout = $this->validateTimeout($timeout);
1011
1012 return $this;
1013 }
1014
1015 /**
1016 * Enables or disables the TTY mode.
1017 *
1018 * @param bool $tty True to enabled and false to disable
1019 *
1020 * @return $this
1021 *
1022 * @throws RuntimeException In case the TTY mode is not supported
1023 */
1024 public function setTty($tty)
1025 {
1026 if ('\\' === \DIRECTORY_SEPARATOR && $tty) {
1027 throw new RuntimeException('TTY mode is not supported on Windows platform.');
1028 }
1029 if ($tty) {
1030 static $isTtySupported;
1031
1032 if (null === $isTtySupported) {
1033 $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes);
1034 }
1035
1036 if (!$isTtySupported) {
1037 throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.');
1038 }
1039 }
1040
1041 $this->tty = (bool) $tty;
1042
1043 return $this;
1044 }
1045
1046 /**
1047 * Checks if the TTY mode is enabled.
1048 *
1049 * @return bool true if the TTY mode is enabled, false otherwise
1050 */
1051 public function isTty()
1052 {
1053 return $this->tty;
1054 }
1055
1056 /**
1057 * Sets PTY mode.
1058 *
1059 * @param bool $bool
1060 *
1061 * @return $this
1062 */
1063 public function setPty($bool)
1064 {
1065 $this->pty = (bool) $bool;
1066
1067 return $this;
1068 }
1069
1070 /**
1071 * Returns PTY state.
1072 *
1073 * @return bool
1074 */
1075 public function isPty()
1076 {
1077 return $this->pty;
1078 }
1079
1080 /**
1081 * Gets the working directory.
1082 *
1083 * @return string|null The current working directory or null on failure
1084 */
1085 public function getWorkingDirectory()
1086 {
1087 if (null === $this->cwd) {
1088 // getcwd() will return false if any one of the parent directories does not have
1089 // the readable or search mode set, even if the current directory does
1090 return getcwd() ?: null;
1091 }
1092
1093 return $this->cwd;
1094 }
1095
1096 /**
1097 * Sets the current working directory.
1098 *
1099 * @param string $cwd The new working directory
1100 *
1101 * @return $this
1102 */
1103 public function setWorkingDirectory($cwd)
1104 {
1105 $this->cwd = $cwd;
1106
1107 return $this;
1108 }
1109
1110 /**
1111 * Gets the environment variables.
1112 *
1113 * @return array The current environment variables
1114 */
1115 public function getEnv()
1116 {
1117 return $this->env;
1118 }
1119
1120 /**
1121 * Sets the environment variables.
1122 *
1123 * Each environment variable value should be a string.
1124 * If it is an array, the variable is ignored.
1125 * If it is false or null, it will be removed when
1126 * env vars are otherwise inherited.
1127 *
1128 * That happens in PHP when 'argv' is registered into
1129 * the $_ENV array for instance.
1130 *
1131 * @param array $env The new environment variables
1132 *
1133 * @return $this
1134 */
1135 public function setEnv(array $env)
1136 {
1137 // Process can not handle env values that are arrays
1138 $env = array_filter($env, function ($value) {
1139 return !\is_array($value);
1140 });
1141
1142 $this->env = $env;
1143
1144 return $this;
1145 }
1146
1147 /**
1148 * Gets the Process input.
1149 *
1150 * @return resource|string|\Iterator|null The Process input
1151 */
1152 public function getInput()
1153 {
1154 return $this->input;
1155 }
1156
1157 /**
1158 * Sets the input.
1159 *
1160 * This content will be passed to the underlying process standard input.
1161 *
1162 * @param string|int|float|bool|resource|\Traversable|null $input The content
1163 *
1164 * @return $this
1165 *
1166 * @throws LogicException In case the process is running
1167 */
1168 public function setInput($input)
1169 {
1170 if ($this->isRunning()) {
1171 throw new LogicException('Input can not be set while the process is running.');
1172 }
1173
1174 $this->input = ProcessUtils::validateInput(__METHOD__, $input);
1175
1176 return $this;
1177 }
1178
1179 /**
1180 * Gets the options for proc_open.
1181 *
1182 * @return array The current options
1183 *
1184 * @deprecated since version 3.3, to be removed in 4.0.
1185 */
1186 public function getOptions()
1187 {
1188 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__), \E_USER_DEPRECATED);
1189
1190 return $this->options;
1191 }
1192
1193 /**
1194 * Sets the options for proc_open.
1195 *
1196 * @param array $options The new options
1197 *
1198 * @return $this
1199 *
1200 * @deprecated since version 3.3, to be removed in 4.0.
1201 */
1202 public function setOptions(array $options)
1203 {
1204 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__), \E_USER_DEPRECATED);
1205
1206 $this->options = $options;
1207
1208 return $this;
1209 }
1210
1211 /**
1212 * Gets whether or not Windows compatibility is enabled.
1213 *
1214 * This is true by default.
1215 *
1216 * @return bool
1217 *
1218 * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled.
1219 */
1220 public function getEnhanceWindowsCompatibility()
1221 {
1222 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__), \E_USER_DEPRECATED);
1223
1224 return $this->enhanceWindowsCompatibility;
1225 }
1226
1227 /**
1228 * Sets whether or not Windows compatibility is enabled.
1229 *
1230 * @param bool $enhance
1231 *
1232 * @return $this
1233 *
1234 * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled.
1235 */
1236 public function setEnhanceWindowsCompatibility($enhance)
1237 {
1238 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__), \E_USER_DEPRECATED);
1239
1240 $this->enhanceWindowsCompatibility = (bool) $enhance;
1241
1242 return $this;
1243 }
1244
1245 /**
1246 * Returns whether sigchild compatibility mode is activated or not.
1247 *
1248 * @return bool
1249 *
1250 * @deprecated since version 3.3, to be removed in 4.0. Sigchild compatibility will always be enabled.
1251 */
1252 public function getEnhanceSigchildCompatibility()
1253 {
1254 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__), \E_USER_DEPRECATED);
1255
1256 return $this->enhanceSigchildCompatibility;
1257 }
1258
1259 /**
1260 * Activates sigchild compatibility mode.
1261 *
1262 * Sigchild compatibility mode is required to get the exit code and
1263 * determine the success of a process when PHP has been compiled with
1264 * the --enable-sigchild option
1265 *
1266 * @param bool $enhance
1267 *
1268 * @return $this
1269 *
1270 * @deprecated since version 3.3, to be removed in 4.0.
1271 */
1272 public function setEnhanceSigchildCompatibility($enhance)
1273 {
1274 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__), \E_USER_DEPRECATED);
1275
1276 $this->enhanceSigchildCompatibility = (bool) $enhance;
1277
1278 return $this;
1279 }
1280
1281 /**
1282 * Sets whether environment variables will be inherited or not.
1283 *
1284 * @param bool $inheritEnv
1285 *
1286 * @return $this
1287 */
1288 public function inheritEnvironmentVariables($inheritEnv = true)
1289 {
1290 if (!$inheritEnv) {
1291 @trigger_error('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', \E_USER_DEPRECATED);
1292 }
1293
1294 $this->inheritEnv = (bool) $inheritEnv;
1295
1296 return $this;
1297 }
1298
1299 /**
1300 * Returns whether environment variables will be inherited or not.
1301 *
1302 * @return bool
1303 *
1304 * @deprecated since version 3.3, to be removed in 4.0. Environment variables will always be inherited.
1305 */
1306 public function areEnvironmentVariablesInherited()
1307 {
1308 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Environment variables will always be inherited.', __METHOD__), \E_USER_DEPRECATED);
1309
1310 return $this->inheritEnv;
1311 }
1312
1313 /**
1314 * Performs a check between the timeout definition and the time the process started.
1315 *
1316 * In case you run a background process (with the start method), you should
1317 * trigger this method regularly to ensure the process timeout
1318 *
1319 * @throws ProcessTimedOutException In case the timeout was reached
1320 */
1321 public function checkTimeout()
1322 {
1323 if (self::STATUS_STARTED !== $this->status) {
1324 return;
1325 }
1326
1327 if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
1328 $this->stop(0);
1329
1330 throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL);
1331 }
1332
1333 if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
1334 $this->stop(0);
1335
1336 throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE);
1337 }
1338 }
1339
1340 /**
1341 * Returns whether PTY is supported on the current operating system.
1342 *
1343 * @return bool
1344 */
1345 public static function isPtySupported()
1346 {
1347 static $result;
1348
1349 if (null !== $result) {
1350 return $result;
1351 }
1352
1353 if ('\\' === \DIRECTORY_SEPARATOR) {
1354 return $result = false;
1355 }
1356
1357 return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes);
1358 }
1359
1360 /**
1361 * Creates the descriptors needed by the proc_open.
1362 *
1363 * @return array
1364 */
1365 private function getDescriptors()
1366 {
1367 if ($this->input instanceof \Iterator) {
1368 $this->input->rewind();
1369 }
1370 if ('\\' === \DIRECTORY_SEPARATOR) {
1371 $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback);
1372 } else {
1373 $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback);
1374 }
1375
1376 return $this->processPipes->getDescriptors();
1377 }
1378
1379 /**
1380 * Builds up the callback used by wait().
1381 *
1382 * The callbacks adds all occurred output to the specific buffer and calls
1383 * the user callback (if present) with the received output.
1384 *
1385 * @param callable|null $callback The user defined PHP callback
1386 *
1387 * @return \Closure A PHP closure
1388 */
1389 protected function buildCallback(callable $callback = null)
1390 {
1391 if ($this->outputDisabled) {
1392 return function ($type, $data) use ($callback) {
1393 if (null !== $callback) {
1394 \call_user_func($callback, $type, $data);
1395 }
1396 };
1397 }
1398
1399 $out = self::OUT;
1400
1401 return function ($type, $data) use ($callback, $out) {
1402 if ($out == $type) {
1403 $this->addOutput($data);
1404 } else {
1405 $this->addErrorOutput($data);
1406 }
1407
1408 if (null !== $callback) {
1409 \call_user_func($callback, $type, $data);
1410 }
1411 };
1412 }
1413
1414 /**
1415 * Updates the status of the process, reads pipes.
1416 *
1417 * @param bool $blocking Whether to use a blocking read call
1418 */
1419 protected function updateStatus($blocking)
1420 {
1421 if (self::STATUS_STARTED !== $this->status) {
1422 return;
1423 }
1424
1425 $this->processInformation = proc_get_status($this->process);
1426 $running = $this->processInformation['running'];
1427
1428 $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running);
1429
1430 if ($this->fallbackStatus && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
1431 $this->processInformation = $this->fallbackStatus + $this->processInformation;
1432 }
1433
1434 if (!$running) {
1435 $this->close();
1436 }
1437 }
1438
1439 /**
1440 * Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
1441 *
1442 * @return bool
1443 */
1444 protected function isSigchildEnabled()
1445 {
1446 if (null !== self::$sigchild) {
1447 return self::$sigchild;
1448 }
1449
1450 if (!\function_exists('phpinfo') || \defined('HHVM_VERSION')) {
1451 return self::$sigchild = false;
1452 }
1453
1454 ob_start();
1455 phpinfo(\INFO_GENERAL);
1456
1457 return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
1458 }
1459
1460 /**
1461 * Reads pipes for the freshest output.
1462 *
1463 * @param string $caller The name of the method that needs fresh outputs
1464 * @param bool $blocking Whether to use blocking calls or not
1465 *
1466 * @throws LogicException in case output has been disabled or process is not started
1467 */
1468 private function readPipesForOutput($caller, $blocking = false)
1469 {
1470 if ($this->outputDisabled) {
1471 throw new LogicException('Output has been disabled.');
1472 }
1473
1474 $this->requireProcessIsStarted($caller);
1475
1476 $this->updateStatus($blocking);
1477 }
1478
1479 /**
1480 * Validates and returns the filtered timeout.
1481 *
1482 * @param int|float|null $timeout
1483 *
1484 * @return float|null
1485 *
1486 * @throws InvalidArgumentException if the given timeout is a negative number
1487 */
1488 private function validateTimeout($timeout)
1489 {
1490 $timeout = (float) $timeout;
1491
1492 if (0.0 === $timeout) {
1493 $timeout = null;
1494 } elseif ($timeout < 0) {
1495 throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
1496 }
1497
1498 return $timeout;
1499 }
1500
1501 /**
1502 * Reads pipes, executes callback.
1503 *
1504 * @param bool $blocking Whether to use blocking calls or not
1505 * @param bool $close Whether to close file handles or not
1506 */
1507 private function readPipes($blocking, $close)
1508 {
1509 $result = $this->processPipes->readAndWrite($blocking, $close);
1510
1511 $callback = $this->callback;
1512 foreach ($result as $type => $data) {
1513 if (3 !== $type) {
1514 $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data);
1515 } elseif (!isset($this->fallbackStatus['signaled'])) {
1516 $this->fallbackStatus['exitcode'] = (int) $data;
1517 }
1518 }
1519 }
1520
1521 /**
1522 * Closes process resource, closes file handles, sets the exitcode.
1523 *
1524 * @return int The exitcode
1525 */
1526 private function close()
1527 {
1528 $this->processPipes->close();
1529 if (\is_resource($this->process)) {
1530 proc_close($this->process);
1531 }
1532 $this->exitcode = $this->processInformation['exitcode'];
1533 $this->status = self::STATUS_TERMINATED;
1534
1535 if (-1 === $this->exitcode) {
1536 if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) {
1537 // if process has been signaled, no exitcode but a valid termsig, apply Unix convention
1538 $this->exitcode = 128 + $this->processInformation['termsig'];
1539 } elseif ($this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
1540 $this->processInformation['signaled'] = true;
1541 $this->processInformation['termsig'] = -1;
1542 }
1543 }
1544
1545 // Free memory from self-reference callback created by buildCallback
1546 // Doing so in other contexts like __destruct or by garbage collector is ineffective
1547 // Now pipes are closed, so the callback is no longer necessary
1548 $this->callback = null;
1549
1550 return $this->exitcode;
1551 }
1552
1553 /**
1554 * Resets data related to the latest run of the process.
1555 */
1556 private function resetProcessData()
1557 {
1558 $this->starttime = null;
1559 $this->callback = null;
1560 $this->exitcode = null;
1561 $this->fallbackStatus = [];
1562 $this->processInformation = null;
1563 $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b');
1564 $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b');
1565 $this->process = null;
1566 $this->latestSignal = null;
1567 $this->status = self::STATUS_READY;
1568 $this->incrementalOutputOffset = 0;
1569 $this->incrementalErrorOutputOffset = 0;
1570 }
1571
1572 /**
1573 * Sends a POSIX signal to the process.
1574 *
1575 * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants)
1576 * @param bool $throwException Whether to throw exception in case signal failed
1577 *
1578 * @return bool True if the signal was sent successfully, false otherwise
1579 *
1580 * @throws LogicException In case the process is not running
1581 * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
1582 * @throws RuntimeException In case of failure
1583 */
1584 private function doSignal($signal, $throwException)
1585 {
1586 if (null === $pid = $this->getPid()) {
1587 if ($throwException) {
1588 throw new LogicException('Can not send signal on a non running process.');
1589 }
1590
1591 return false;
1592 }
1593
1594 if ('\\' === \DIRECTORY_SEPARATOR) {
1595 exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode);
1596 if ($exitCode && $this->isRunning()) {
1597 if ($throwException) {
1598 throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output)));
1599 }
1600
1601 return false;
1602 }
1603 } else {
1604 if (!$this->enhanceSigchildCompatibility || !$this->isSigchildEnabled()) {
1605 $ok = @proc_terminate($this->process, $signal);
1606 } elseif (\function_exists('posix_kill')) {
1607 $ok = @posix_kill($pid, $signal);
1608 } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) {
1609 $ok = false === fgets($pipes[2]);
1610 }
1611 if (!$ok) {
1612 if ($throwException) {
1613 throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal));
1614 }
1615
1616 return false;
1617 }
1618 }
1619
1620 $this->latestSignal = (int) $signal;
1621 $this->fallbackStatus['signaled'] = true;
1622 $this->fallbackStatus['exitcode'] = -1;
1623 $this->fallbackStatus['termsig'] = $this->latestSignal;
1624
1625 return true;
1626 }
1627
1628 private function prepareWindowsCommandLine($cmd, array &$env)
1629 {
1630 $uid = uniqid('', true);
1631 $varCount = 0;
1632 $varCache = [];
1633 $cmd = preg_replace_callback(
1634 '/"(?:(
1635 [^"%!^]*+
1636 (?:
1637 (?: !LF! | "(?:\^[%!^])?+" )
1638 [^"%!^]*+
1639 )++
1640 ) | [^"]*+ )"/x',
1641 function ($m) use (&$env, &$varCache, &$varCount, $uid) {
1642 if (!isset($m[1])) {
1643 return $m[0];
1644 }
1645 if (isset($varCache[$m[0]])) {
1646 return $varCache[$m[0]];
1647 }
1648 if (false !== strpos($value = $m[1], "\0")) {
1649 $value = str_replace("\0", '?', $value);
1650 }
1651 if (false === strpbrk($value, "\"%!\n")) {
1652 return '"'.$value.'"';
1653 }
1654
1655 $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value);
1656 $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"';
1657 $var = $uid.++$varCount;
1658
1659 $env[$var] = $value;
1660
1661 return $varCache[$m[0]] = '!'.$var.'!';
1662 },
1663 $cmd
1664 );
1665
1666 $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')';
1667 foreach ($this->processPipes->getFiles() as $offset => $filename) {
1668 $cmd .= ' '.$offset.'>"'.$filename.'"';
1669 }
1670
1671 return $cmd;
1672 }
1673
1674 /**
1675 * Ensures the process is running or terminated, throws a LogicException if the process has a not started.
1676 *
1677 * @param string $functionName The function name that was called
1678 *
1679 * @throws LogicException if the process has not run
1680 */
1681 private function requireProcessIsStarted($functionName)
1682 {
1683 if (!$this->isStarted()) {
1684 throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName));
1685 }
1686 }
1687
1688 /**
1689 * Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`.
1690 *
1691 * @param string $functionName The function name that was called
1692 *
1693 * @throws LogicException if the process is not yet terminated
1694 */
1695 private function requireProcessIsTerminated($functionName)
1696 {
1697 if (!$this->isTerminated()) {
1698 throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName));
1699 }
1700 }
1701
1702 /**
1703 * Escapes a string to be used as a shell argument.
1704 *
1705 * @param string $argument The argument that will be escaped
1706 *
1707 * @return string The escaped argument
1708 */
1709 private function escapeArgument($argument)
1710 {
1711 if ('\\' !== \DIRECTORY_SEPARATOR) {
1712 return "'".str_replace("'", "'\\''", $argument)."'";
1713 }
1714 if ('' === $argument = (string) $argument) {
1715 return '""';
1716 }
1717 if (false !== strpos($argument, "\0")) {
1718 $argument = str_replace("\0", '?', $argument);
1719 }
1720 if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) {
1721 return $argument;
1722 }
1723 $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument);
1724
1725 return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"';
1726 }
1727
1728 private function getDefaultEnv()
1729 {
1730 $env = [];
1731
1732 foreach ($_SERVER as $k => $v) {
1733 if (\is_string($v) && false !== $v = getenv($k)) {
1734 $env[$k] = $v;
1735 }
1736 }
1737
1738 foreach ($_ENV as $k => $v) {
1739 if (\is_string($v)) {
1740 $env[$k] = $v;
1741 }
1742 }
1743
1744 return $env;
1745 }
1746 }
1747