[ Index ]

PHP Cross Reference of phpBB-3.3.14-deutsch

title

Body

[close]

/vendor/symfony/process/ -> Process.php (source)

   1  <?php
   2  
   3  /*
   4   * This file is part of the Symfony package.
   5   *
   6   * (c) Fabien Potencier <fabien@symfony.com>
   7   *
   8   * For the full copyright and license information, please view the LICENSE
   9   * file that was distributed with this source code.
  10   */
  11  
  12  namespace Symfony\Component\Process;
  13  
  14  use Symfony\Component\Process\Exception\InvalidArgumentException;
  15  use Symfony\Component\Process\Exception\LogicException;
  16  use Symfony\Component\Process\Exception\ProcessFailedException;
  17  use Symfony\Component\Process\Exception\ProcessTimedOutException;
  18  use Symfony\Component\Process\Exception\RuntimeException;
  19  use Symfony\Component\Process\Pipes\PipesInterface;
  20  use Symfony\Component\Process\Pipes\UnixPipes;
  21  use Symfony\Component\Process\Pipes\WindowsPipes;
  22  
  23  /**
  24   * Process is a thin wrapper around proc_* functions to easily
  25   * start independent PHP processes.
  26   *
  27   * @author Fabien Potencier <fabien@symfony.com>
  28   * @author Romain Neutron <imprec@gmail.com>
  29   */
  30  class Process implements \IteratorAggregate
  31  {
  32      const ERR = 'err';
  33      const OUT = 'out';
  34  
  35      const STATUS_READY = 'ready';
  36      const STATUS_STARTED = 'started';
  37      const STATUS_TERMINATED = 'terminated';
  38  
  39      const STDIN = 0;
  40      const STDOUT = 1;
  41      const STDERR = 2;
  42  
  43      // Timeout Precision in seconds.
  44      const TIMEOUT_PRECISION = 0.2;
  45  
  46      const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking
  47      const ITER_KEEP_OUTPUT = 2;  // By default, outputs are cleared while iterating, use this flag to keep them in memory
  48      const ITER_SKIP_OUT = 4;     // Use this flag to skip STDOUT while iterating
  49      const ITER_SKIP_ERR = 8;     // Use this flag to skip STDERR while iterating
  50  
  51      private $callback;
  52      private $hasCallback = false;
  53      private $commandline;
  54      private $cwd;
  55      private $env;
  56      private $input;
  57      private $starttime;
  58      private $lastOutputTime;
  59      private $timeout;
  60      private $idleTimeout;
  61      private $options = ['suppress_errors' => true];
  62      private $exitcode;
  63      private $fallbackStatus = [];
  64      private $processInformation;
  65      private $outputDisabled = false;
  66      private $stdout;
  67      private $stderr;
  68      private $enhanceWindowsCompatibility = true;
  69      private $enhanceSigchildCompatibility;
  70      private $process;
  71      private $status = self::STATUS_READY;
  72      private $incrementalOutputOffset = 0;
  73      private $incrementalErrorOutputOffset = 0;
  74      private $tty = false;
  75      private $pty;
  76      private $inheritEnv = false;
  77  
  78      private $useFileHandles = false;
  79      /** @var PipesInterface */
  80      private $processPipes;
  81  
  82      private $latestSignal;
  83  
  84      private static $sigchild;
  85  
  86      /**
  87       * Exit codes translation table.
  88       *
  89       * User-defined errors must use exit codes in the 64-113 range.
  90       */
  91      public static $exitCodes = [
  92          0 => 'OK',
  93          1 => 'General error',
  94          2 => 'Misuse of shell builtins',
  95  
  96          126 => 'Invoked command cannot execute',
  97          127 => 'Command not found',
  98          128 => 'Invalid exit argument',
  99  
 100          // signals
 101          129 => 'Hangup',
 102          130 => 'Interrupt',
 103          131 => 'Quit and dump core',
 104          132 => 'Illegal instruction',
 105          133 => 'Trace/breakpoint trap',
 106          134 => 'Process aborted',
 107          135 => 'Bus error: "access to undefined portion of memory object"',
 108          136 => 'Floating point exception: "erroneous arithmetic operation"',
 109          137 => 'Kill (terminate immediately)',
 110          138 => 'User-defined 1',
 111          139 => 'Segmentation violation',
 112          140 => 'User-defined 2',
 113          141 => 'Write to pipe with no one reading',
 114          142 => 'Signal raised by alarm',
 115          143 => 'Termination (request to terminate)',
 116          // 144 - not defined
 117          145 => 'Child process terminated, stopped (or continued*)',
 118          146 => 'Continue if stopped',
 119          147 => 'Stop executing temporarily',
 120          148 => 'Terminal stop signal',
 121          149 => 'Background process attempting to read from tty ("in")',
 122          150 => 'Background process attempting to write to tty ("out")',
 123          151 => 'Urgent data available on socket',
 124          152 => 'CPU time limit exceeded',
 125          153 => 'File size limit exceeded',
 126          154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
 127          155 => 'Profiling timer expired',
 128          // 156 - not defined
 129          157 => 'Pollable event',
 130          // 158 - not defined
 131          159 => 'Bad syscall',
 132      ];
 133  
 134      /**
 135       * @param string|array   $commandline The command line to run
 136       * @param string|null    $cwd         The working directory or null to use the working dir of the current PHP process
 137       * @param array|null     $env         The environment variables or null to use the same environment as the current PHP process
 138       * @param mixed|null     $input       The input as stream resource, scalar or \Traversable, or null for no input
 139       * @param int|float|null $timeout     The timeout in seconds or null to disable
 140       * @param array          $options     An array of options for proc_open
 141       *
 142       * @throws RuntimeException When proc_open is not installed
 143       */
 144      public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = null)
 145      {
 146          if (!\function_exists('proc_open')) {
 147              throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.');
 148          }
 149  
 150          $this->commandline = $commandline;
 151          $this->cwd = $cwd;
 152  
 153          // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started
 154          // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected
 155          // @see : https://bugs.php.net/51800
 156          // @see : https://bugs.php.net/50524
 157          if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) {
 158              $this->cwd = getcwd();
 159          }
 160          if (null !== $env) {
 161              $this->setEnv($env);
 162          }
 163  
 164          $this->setInput($input);
 165          $this->setTimeout($timeout);
 166          $this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR;
 167          $this->pty = false;
 168          $this->enhanceSigchildCompatibility = '\\' !== \DIRECTORY_SEPARATOR && $this->isSigchildEnabled();
 169          if (null !== $options) {
 170              @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);
 171              $this->options = array_replace($this->options, $options);
 172          }
 173      }
 174  
 175      public function __destruct()
 176      {
 177          $this->stop(0);
 178      }
 179  
 180      public function __clone()
 181      {
 182          $this->resetProcessData();
 183      }
 184  
 185      /**
 186       * Runs the process.
 187       *
 188       * The callback receives the type of output (out or err) and
 189       * some bytes from the output in real-time. It allows to have feedback
 190       * from the independent process during execution.
 191       *
 192       * The STDOUT and STDERR are also available after the process is finished
 193       * via the getOutput() and getErrorOutput() methods.
 194       *
 195       * @param callable|null $callback A PHP callback to run whenever there is some
 196       *                                output available on STDOUT or STDERR
 197       *
 198       * @return int The exit status code
 199       *
 200       * @throws RuntimeException When process can't be launched
 201       * @throws RuntimeException When process stopped after receiving signal
 202       * @throws LogicException   In case a callback is provided and output has been disabled
 203       *
 204       * @final since version 3.3
 205       */
 206      public function run($callback = null/*, array $env = []*/)
 207      {
 208          $env = 1 < \func_num_args() ? func_get_arg(1) : null;
 209          $this->start($callback, $env);
 210  
 211          return $this->wait();
 212      }
 213  
 214      /**
 215       * Runs the process.
 216       *
 217       * This is identical to run() except that an exception is thrown if the process
 218       * exits with a non-zero exit code.
 219       *
 220       * @return $this
 221       *
 222       * @throws RuntimeException       if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled
 223       * @throws ProcessFailedException if the process didn't terminate successfully
 224       *
 225       * @final since version 3.3
 226       */
 227      public function mustRun(callable $callback = null/*, array $env = []*/)
 228      {
 229          if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
 230              throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
 231          }
 232          $env = 1 < \func_num_args() ? func_get_arg(1) : null;
 233  
 234          if (0 !== $this->run($callback, $env)) {
 235              throw new ProcessFailedException($this);
 236          }
 237  
 238          return $this;
 239      }
 240  
 241      /**
 242       * Starts the process and returns after writing the input to STDIN.
 243       *
 244       * This method blocks until all STDIN data is sent to the process then it
 245       * returns while the process runs in the background.
 246       *
 247       * The termination of the process can be awaited with wait().
 248       *
 249       * The callback receives the type of output (out or err) and some bytes from
 250       * the output in real-time while writing the standard input to the process.
 251       * It allows to have feedback from the independent process during execution.
 252       *
 253       * @param callable|null $callback A PHP callback to run whenever there is some
 254       *                                output available on STDOUT or STDERR
 255       *
 256       * @throws RuntimeException When process can't be launched
 257       * @throws RuntimeException When process is already running
 258       * @throws LogicException   In case a callback is provided and output has been disabled
 259       */
 260      public function start(callable $callback = null/*, array $env = [*/)
 261      {
 262          if ($this->isRunning()) {
 263              throw new RuntimeException('Process is already running.');
 264          }
 265          if (2 <= \func_num_args()) {
 266              $env = func_get_arg(1);
 267          } else {
 268              if (__CLASS__ !== static::class) {
 269                  $r = new \ReflectionMethod($this, __FUNCTION__);
 270                  if (__CLASS__ !== $r->getDeclaringClass()->getName() && (2 > $r->getNumberOfParameters() || 'env' !== $r->getParameters()[1]->name)) {
 271                      @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);
 272                  }
 273              }
 274              $env = null;
 275          }
 276  
 277          $this->resetProcessData();
 278          $this->starttime = $this->lastOutputTime = microtime(true);
 279          $this->callback = $this->buildCallback($callback);
 280          $this->hasCallback = null !== $callback;
 281          $descriptors = $this->getDescriptors();
 282          $inheritEnv = $this->inheritEnv;
 283  
 284          if (\is_array($commandline = $this->commandline)) {
 285              $commandline = implode(' ', array_map([$this, 'escapeArgument'], $commandline));
 286  
 287              if ('\\' !== \DIRECTORY_SEPARATOR) {
 288                  // exec is mandatory to deal with sending a signal to the process
 289                  $commandline = 'exec '.$commandline;
 290              }
 291          }
 292  
 293          if (null === $env) {
 294              $env = $this->env;
 295          } else {
 296              if ($this->env) {
 297                  $env += $this->env;
 298              }
 299              $inheritEnv = true;
 300          }
 301  
 302          if (null !== $env && $inheritEnv) {
 303              $env += $this->getDefaultEnv();
 304          } elseif (null !== $env) {
 305              @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);
 306          } else {
 307              $env = $this->getDefaultEnv();
 308          }
 309          if ('\\' === \DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) {
 310              $this->options['bypass_shell'] = true;
 311              $commandline = $this->prepareWindowsCommandLine($commandline, $env);
 312          } elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
 313              // last exit code is output on the fourth pipe and caught to work around --enable-sigchild
 314              $descriptors[3] = ['pipe', 'w'];
 315  
 316              // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input
 317              $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;';
 318              $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code';
 319  
 320              // Workaround for the bug, when PTS functionality is enabled.
 321              // @see : https://bugs.php.net/69442
 322              $ptsWorkaround = fopen(__FILE__, 'r');
 323          }
 324          if (\defined('HHVM_VERSION')) {
 325              $envPairs = $env;
 326          } else {
 327              $envPairs = [];
 328              foreach ($env as $k => $v) {
 329                  if (false !== $v) {
 330                      $envPairs[] = $k.'='.$v;
 331                  }
 332              }
 333          }
 334  
 335          if (!is_dir($this->cwd)) {
 336              @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);
 337          }
 338  
 339          $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options);
 340  
 341          if (!\is_resource($this->process)) {
 342              throw new RuntimeException('Unable to launch a new process.');
 343          }
 344          $this->status = self::STATUS_STARTED;
 345  
 346          if (isset($descriptors[3])) {
 347              $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]);
 348          }
 349  
 350          if ($this->tty) {
 351              return;
 352          }
 353  
 354          $this->updateStatus(false);
 355          $this->checkTimeout();
 356      }
 357  
 358      /**
 359       * Restarts the process.
 360       *
 361       * Be warned that the process is cloned before being started.
 362       *
 363       * @param callable|null $callback A PHP callback to run whenever there is some
 364       *                                output available on STDOUT or STDERR
 365       *
 366       * @return static
 367       *
 368       * @throws RuntimeException When process can't be launched
 369       * @throws RuntimeException When process is already running
 370       *
 371       * @see start()
 372       *
 373       * @final since version 3.3
 374       */
 375      public function restart(callable $callback = null/*, array $env = []*/)
 376      {
 377          if ($this->isRunning()) {
 378              throw new RuntimeException('Process is already running.');
 379          }
 380          $env = 1 < \func_num_args() ? func_get_arg(1) : null;
 381  
 382          $process = clone $this;
 383          $process->start($callback, $env);
 384  
 385          return $process;
 386      }
 387  
 388      /**
 389       * Waits for the process to terminate.
 390       *
 391       * The callback receives the type of output (out or err) and some bytes
 392       * from the output in real-time while writing the standard input to the process.
 393       * It allows to have feedback from the independent process during execution.
 394       *
 395       * @param callable|null $callback A valid PHP callback
 396       *
 397       * @return int The exitcode of the process
 398       *
 399       * @throws RuntimeException When process timed out
 400       * @throws RuntimeException When process stopped after receiving signal
 401       * @throws LogicException   When process is not yet started
 402       */
 403      public function wait(callable $callback = null)
 404      {
 405          $this->requireProcessIsStarted(__FUNCTION__);
 406  
 407          $this->updateStatus(false);
 408  
 409          if (null !== $callback) {
 410              if (!$this->processPipes->haveReadSupport()) {
 411                  $this->stop(0);
 412                  throw new \LogicException('Pass the callback to the Process::start method or enableOutput to use a callback with Process::wait.');
 413              }
 414              $this->callback = $this->buildCallback($callback);
 415          }
 416  
 417          do {
 418              $this->checkTimeout();
 419              $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
 420              $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
 421          } while ($running);
 422  
 423          while ($this->isRunning()) {
 424              $this->checkTimeout();
 425              usleep(1000);
 426          }
 427  
 428          if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
 429              throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig']));
 430          }
 431  
 432          return $this->exitcode;
 433      }
 434  
 435      /**
 436       * Returns the Pid (process identifier), if applicable.
 437       *
 438       * @return int|null The process id if running, null otherwise
 439       */
 440      public function getPid()
 441      {
 442          return $this->isRunning() ? $this->processInformation['pid'] : null;
 443      }
 444  
 445      /**
 446       * Sends a POSIX signal to the process.
 447       *
 448       * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants)
 449       *
 450       * @return $this
 451       *
 452       * @throws LogicException   In case the process is not running
 453       * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
 454       * @throws RuntimeException In case of failure
 455       */
 456      public function signal($signal)
 457      {
 458          $this->doSignal($signal, true);
 459  
 460          return $this;
 461      }
 462  
 463      /**
 464       * Disables fetching output and error output from the underlying process.
 465       *
 466       * @return $this
 467       *
 468       * @throws RuntimeException In case the process is already running
 469       * @throws LogicException   if an idle timeout is set
 470       */
 471      public function disableOutput()
 472      {
 473          if ($this->isRunning()) {
 474              throw new RuntimeException('Disabling output while the process is running is not possible.');
 475          }
 476          if (null !== $this->idleTimeout) {
 477              throw new LogicException('Output can not be disabled while an idle timeout is set.');
 478          }
 479  
 480          $this->outputDisabled = true;
 481  
 482          return $this;
 483      }
 484  
 485      /**
 486       * Enables fetching output and error output from the underlying process.
 487       *
 488       * @return $this
 489       *
 490       * @throws RuntimeException In case the process is already running
 491       */
 492      public function enableOutput()
 493      {
 494          if ($this->isRunning()) {
 495              throw new RuntimeException('Enabling output while the process is running is not possible.');
 496          }
 497  
 498          $this->outputDisabled = false;
 499  
 500          return $this;
 501      }
 502  
 503      /**
 504       * Returns true in case the output is disabled, false otherwise.
 505       *
 506       * @return bool
 507       */
 508      public function isOutputDisabled()
 509      {
 510          return $this->outputDisabled;
 511      }
 512  
 513      /**
 514       * Returns the current output of the process (STDOUT).
 515       *
 516       * @return string The process output
 517       *
 518       * @throws LogicException in case the output has been disabled
 519       * @throws LogicException In case the process is not started
 520       */
 521      public function getOutput()
 522      {
 523          $this->readPipesForOutput(__FUNCTION__);
 524  
 525          if (false === $ret = stream_get_contents($this->stdout, -1, 0)) {
 526              return '';
 527          }
 528  
 529          return $ret;
 530      }
 531  
 532      /**
 533       * Returns the output incrementally.
 534       *
 535       * In comparison with the getOutput method which always return the whole
 536       * output, this one returns the new output since the last call.
 537       *
 538       * @return string The process output since the last call
 539       *
 540       * @throws LogicException in case the output has been disabled
 541       * @throws LogicException In case the process is not started
 542       */
 543      public function getIncrementalOutput()
 544      {
 545          $this->readPipesForOutput(__FUNCTION__);
 546  
 547          $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
 548          $this->incrementalOutputOffset = ftell($this->stdout);
 549  
 550          if (false === $latest) {
 551              return '';
 552          }
 553  
 554          return $latest;
 555      }
 556  
 557      /**
 558       * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR).
 559       *
 560       * @param int $flags A bit field of Process::ITER_* flags
 561       *
 562       * @throws LogicException in case the output has been disabled
 563       * @throws LogicException In case the process is not started
 564       *
 565       * @return \Generator
 566       */
 567      public function getIterator($flags = 0)
 568      {
 569          $this->readPipesForOutput(__FUNCTION__, false);
 570  
 571          $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags);
 572          $blocking = !(self::ITER_NON_BLOCKING & $flags);
 573          $yieldOut = !(self::ITER_SKIP_OUT & $flags);
 574          $yieldErr = !(self::ITER_SKIP_ERR & $flags);
 575  
 576          while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) {
 577              if ($yieldOut) {
 578                  $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
 579  
 580                  if (isset($out[0])) {
 581                      if ($clearOutput) {
 582                          $this->clearOutput();
 583                      } else {
 584                          $this->incrementalOutputOffset = ftell($this->stdout);
 585                      }
 586  
 587                      yield self::OUT => $out;
 588                  }
 589              }
 590  
 591              if ($yieldErr) {
 592                  $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
 593  
 594                  if (isset($err[0])) {
 595                      if ($clearOutput) {
 596                          $this->clearErrorOutput();
 597                      } else {
 598                          $this->incrementalErrorOutputOffset = ftell($this->stderr);
 599                      }
 600  
 601                      yield self::ERR => $err;
 602                  }
 603              }
 604  
 605              if (!$blocking && !isset($out[0]) && !isset($err[0])) {
 606                  yield self::OUT => '';
 607              }
 608  
 609              $this->checkTimeout();
 610              $this->readPipesForOutput(__FUNCTION__, $blocking);
 611          }
 612      }
 613  
 614      /**
 615       * Clears the process output.
 616       *
 617       * @return $this
 618       */
 619      public function clearOutput()
 620      {
 621          ftruncate($this->stdout, 0);
 622          fseek($this->stdout, 0);
 623          $this->incrementalOutputOffset = 0;
 624  
 625          return $this;
 626      }
 627  
 628      /**
 629       * Returns the current error output of the process (STDERR).
 630       *
 631       * @return string The process error output
 632       *
 633       * @throws LogicException in case the output has been disabled
 634       * @throws LogicException In case the process is not started
 635       */
 636      public function getErrorOutput()
 637      {
 638          $this->readPipesForOutput(__FUNCTION__);
 639  
 640          if (false === $ret = stream_get_contents($this->stderr, -1, 0)) {
 641              return '';
 642          }
 643  
 644          return $ret;
 645      }
 646  
 647      /**
 648       * Returns the errorOutput incrementally.
 649       *
 650       * In comparison with the getErrorOutput method which always return the
 651       * whole error output, this one returns the new error output since the last
 652       * call.
 653       *
 654       * @return string The process error output since the last call
 655       *
 656       * @throws LogicException in case the output has been disabled
 657       * @throws LogicException In case the process is not started
 658       */
 659      public function getIncrementalErrorOutput()
 660      {
 661          $this->readPipesForOutput(__FUNCTION__);
 662  
 663          $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
 664          $this->incrementalErrorOutputOffset = ftell($this->stderr);
 665  
 666          if (false === $latest) {
 667              return '';
 668          }
 669  
 670          return $latest;
 671      }
 672  
 673      /**
 674       * Clears the process output.
 675       *
 676       * @return $this
 677       */
 678      public function clearErrorOutput()
 679      {
 680          ftruncate($this->stderr, 0);
 681          fseek($this->stderr, 0);
 682          $this->incrementalErrorOutputOffset = 0;
 683  
 684          return $this;
 685      }
 686  
 687      /**
 688       * Returns the exit code returned by the process.
 689       *
 690       * @return int|null The exit status code, null if the Process is not terminated
 691       *
 692       * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled
 693       */
 694      public function getExitCode()
 695      {
 696          if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
 697              throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
 698          }
 699  
 700          $this->updateStatus(false);
 701  
 702          return $this->exitcode;
 703      }
 704  
 705      /**
 706       * Returns a string representation for the exit code returned by the process.
 707       *
 708       * This method relies on the Unix exit code status standardization
 709       * and might not be relevant for other operating systems.
 710       *
 711       * @return string|null A string representation for the exit status code, null if the Process is not terminated
 712       *
 713       * @see http://tldp.org/LDP/abs/html/exitcodes.html
 714       * @see http://en.wikipedia.org/wiki/Unix_signal
 715       */
 716      public function getExitCodeText()
 717      {
 718          if (null === $exitcode = $this->getExitCode()) {
 719              return null;
 720          }
 721  
 722          return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error';
 723      }
 724  
 725      /**
 726       * Checks if the process ended successfully.
 727       *
 728       * @return bool true if the process ended successfully, false otherwise
 729       */
 730      public function isSuccessful()
 731      {
 732          return 0 === $this->getExitCode();
 733      }
 734  
 735      /**
 736       * Returns true if the child process has been terminated by an uncaught signal.
 737       *
 738       * It always returns false on Windows.
 739       *
 740       * @return bool
 741       *
 742       * @throws RuntimeException In case --enable-sigchild is activated
 743       * @throws LogicException   In case the process is not terminated
 744       */
 745      public function hasBeenSignaled()
 746      {
 747          $this->requireProcessIsTerminated(__FUNCTION__);
 748  
 749          if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
 750              throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
 751          }
 752  
 753          return $this->processInformation['signaled'];
 754      }
 755  
 756      /**
 757       * Returns the number of the signal that caused the child process to terminate its execution.
 758       *
 759       * It is only meaningful if hasBeenSignaled() returns true.
 760       *
 761       * @return int
 762       *
 763       * @throws RuntimeException In case --enable-sigchild is activated
 764       * @throws LogicException   In case the process is not terminated
 765       */
 766      public function getTermSignal()
 767      {
 768          $this->requireProcessIsTerminated(__FUNCTION__);
 769  
 770          if ($this->isSigchildEnabled() && (!$this->enhanceSigchildCompatibility || -1 === $this->processInformation['termsig'])) {
 771              throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
 772          }
 773  
 774          return $this->processInformation['termsig'];
 775      }
 776  
 777      /**
 778       * Returns true if the child process has been stopped by a signal.
 779       *
 780       * It always returns false on Windows.
 781       *
 782       * @return bool
 783       *
 784       * @throws LogicException In case the process is not terminated
 785       */
 786      public function hasBeenStopped()
 787      {
 788          $this->requireProcessIsTerminated(__FUNCTION__);
 789  
 790          return $this->processInformation['stopped'];
 791      }
 792  
 793      /**
 794       * Returns the number of the signal that caused the child process to stop its execution.
 795       *
 796       * It is only meaningful if hasBeenStopped() returns true.
 797       *
 798       * @return int
 799       *
 800       * @throws LogicException In case the process is not terminated
 801       */
 802      public function getStopSignal()
 803      {
 804          $this->requireProcessIsTerminated(__FUNCTION__);
 805  
 806          return $this->processInformation['stopsig'];
 807      }
 808  
 809      /**
 810       * Checks if the process is currently running.
 811       *
 812       * @return bool true if the process is currently running, false otherwise
 813       */
 814      public function isRunning()
 815      {
 816          if (self::STATUS_STARTED !== $this->status) {
 817              return false;
 818          }
 819  
 820          $this->updateStatus(false);
 821  
 822          return $this->processInformation['running'];
 823      }
 824  
 825      /**
 826       * Checks if the process has been started with no regard to the current state.
 827       *
 828       * @return bool true if status is ready, false otherwise
 829       */
 830      public function isStarted()
 831      {
 832          return self::STATUS_READY != $this->status;
 833      }
 834  
 835      /**
 836       * Checks if the process is terminated.
 837       *
 838       * @return bool true if process is terminated, false otherwise
 839       */
 840      public function isTerminated()
 841      {
 842          $this->updateStatus(false);
 843  
 844          return self::STATUS_TERMINATED == $this->status;
 845      }
 846  
 847      /**
 848       * Gets the process status.
 849       *
 850       * The status is one of: ready, started, terminated.
 851       *
 852       * @return string The current process status
 853       */
 854      public function getStatus()
 855      {
 856          $this->updateStatus(false);
 857  
 858          return $this->status;
 859      }
 860  
 861      /**
 862       * Stops the process.
 863       *
 864       * @param int|float $timeout The timeout in seconds
 865       * @param int       $signal  A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
 866       *
 867       * @return int|null The exit-code of the process or null if it's not running
 868       */
 869      public function stop($timeout = 10, $signal = null)
 870      {
 871          $timeoutMicro = microtime(true) + $timeout;
 872          if ($this->isRunning()) {
 873              // given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here
 874              $this->doSignal(15, false);
 875              do {
 876                  usleep(1000);
 877              } while ($this->isRunning() && microtime(true) < $timeoutMicro);
 878  
 879              if ($this->isRunning()) {
 880                  // Avoid exception here: process is supposed to be running, but it might have stopped just
 881                  // after this line. In any case, let's silently discard the error, we cannot do anything.
 882                  $this->doSignal($signal ?: 9, false);
 883              }
 884          }
 885  
 886          if ($this->isRunning()) {
 887              if (isset($this->fallbackStatus['pid'])) {
 888                  unset($this->fallbackStatus['pid']);
 889  
 890                  return $this->stop(0, $signal);
 891              }
 892              $this->close();
 893          }
 894  
 895          return $this->exitcode;
 896      }
 897  
 898      /**
 899       * Adds a line to the STDOUT stream.
 900       *
 901       * @internal
 902       *
 903       * @param string $line The line to append
 904       */
 905      public function addOutput($line)
 906      {
 907          $this->lastOutputTime = microtime(true);
 908  
 909          fseek($this->stdout, 0, \SEEK_END);
 910          fwrite($this->stdout, $line);
 911          fseek($this->stdout, $this->incrementalOutputOffset);
 912      }
 913  
 914      /**
 915       * Adds a line to the STDERR stream.
 916       *
 917       * @internal
 918       *
 919       * @param string $line The line to append
 920       */
 921      public function addErrorOutput($line)
 922      {
 923          $this->lastOutputTime = microtime(true);
 924  
 925          fseek($this->stderr, 0, \SEEK_END);
 926          fwrite($this->stderr, $line);
 927          fseek($this->stderr, $this->incrementalErrorOutputOffset);
 928      }
 929  
 930      /**
 931       * Gets the command line to be executed.
 932       *
 933       * @return string The command to execute
 934       */
 935      public function getCommandLine()
 936      {
 937          return \is_array($this->commandline) ? implode(' ', array_map([$this, 'escapeArgument'], $this->commandline)) : $this->commandline;
 938      }
 939  
 940      /**
 941       * Sets the command line to be executed.
 942       *
 943       * @param string|array $commandline The command to execute
 944       *
 945       * @return $this
 946       */
 947      public function setCommandLine($commandline)
 948      {
 949          $this->commandline = $commandline;
 950  
 951          return $this;
 952      }
 953  
 954      /**
 955       * Gets the process timeout (max. runtime).
 956       *
 957       * @return float|null The timeout in seconds or null if it's disabled
 958       */
 959      public function getTimeout()
 960      {
 961          return $this->timeout;
 962      }
 963  
 964      /**
 965       * Gets the process idle timeout (max. time since last output).
 966       *
 967       * @return float|null The timeout in seconds or null if it's disabled
 968       */
 969      public function getIdleTimeout()
 970      {
 971          return $this->idleTimeout;
 972      }
 973  
 974      /**
 975       * Sets the process timeout (max. runtime) in seconds.
 976       *
 977       * To disable the timeout, set this value to null.
 978       *
 979       * @param int|float|null $timeout The timeout in seconds
 980       *
 981       * @return $this
 982       *
 983       * @throws InvalidArgumentException if the timeout is negative
 984       */
 985      public function setTimeout($timeout)
 986      {
 987          $this->timeout = $this->validateTimeout($timeout);
 988  
 989          return $this;
 990      }
 991  
 992      /**
 993       * Sets the process idle timeout (max. time since last output).
 994       *
 995       * To disable the timeout, set this value to null.
 996       *
 997       * @param int|float|null $timeout The timeout in seconds
 998       *
 999       * @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  }


Generated: Mon Nov 25 19:05:08 2024 Cross-referenced by PHPXref 0.7.1