"Fossies" - the Fresh Open Source Software Archive

Member "grav/vendor/symfony/process/Process.php" (1 Sep 2020, 52393 Bytes) of package /linux/www/grav-v1.6.27.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "Process.php" see the Fossies "Dox" file reference documentation.

    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\ProcessSignaledException;
   18 use Symfony\Component\Process\Exception\ProcessTimedOutException;
   19 use Symfony\Component\Process\Exception\RuntimeException;
   20 use Symfony\Component\Process\Pipes\PipesInterface;
   21 use Symfony\Component\Process\Pipes\UnixPipes;
   22 use Symfony\Component\Process\Pipes\WindowsPipes;
   23 
   24 /**
   25  * Process is a thin wrapper around proc_* functions to easily
   26  * start independent PHP processes.
   27  *
   28  * @author Fabien Potencier <fabien@symfony.com>
   29  * @author Romain Neutron <imprec@gmail.com>
   30  */
   31 class Process implements \IteratorAggregate
   32 {
   33     const ERR = 'err';
   34     const OUT = 'out';
   35 
   36     const STATUS_READY = 'ready';
   37     const STATUS_STARTED = 'started';
   38     const STATUS_TERMINATED = 'terminated';
   39 
   40     const STDIN = 0;
   41     const STDOUT = 1;
   42     const STDERR = 2;
   43 
   44     // Timeout Precision in seconds.
   45     const TIMEOUT_PRECISION = 0.2;
   46 
   47     const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking
   48     const ITER_KEEP_OUTPUT = 2;  // By default, outputs are cleared while iterating, use this flag to keep them in memory
   49     const ITER_SKIP_OUT = 4;     // Use this flag to skip STDOUT while iterating
   50     const ITER_SKIP_ERR = 8;     // Use this flag to skip STDERR while iterating
   51 
   52     private $callback;
   53     private $hasCallback = false;
   54     private $commandline;
   55     private $cwd;
   56     private $env;
   57     private $input;
   58     private $starttime;
   59     private $lastOutputTime;
   60     private $timeout;
   61     private $idleTimeout;
   62     private $exitcode;
   63     private $fallbackStatus = [];
   64     private $processInformation;
   65     private $outputDisabled = false;
   66     private $stdout;
   67     private $stderr;
   68     private $process;
   69     private $status = self::STATUS_READY;
   70     private $incrementalOutputOffset = 0;
   71     private $incrementalErrorOutputOffset = 0;
   72     private $tty;
   73     private $pty;
   74 
   75     private $useFileHandles = false;
   76     /** @var PipesInterface */
   77     private $processPipes;
   78 
   79     private $latestSignal;
   80 
   81     private static $sigchild;
   82 
   83     /**
   84      * Exit codes translation table.
   85      *
   86      * User-defined errors must use exit codes in the 64-113 range.
   87      */
   88     public static $exitCodes = [
   89         0 => 'OK',
   90         1 => 'General error',
   91         2 => 'Misuse of shell builtins',
   92 
   93         126 => 'Invoked command cannot execute',
   94         127 => 'Command not found',
   95         128 => 'Invalid exit argument',
   96 
   97         // signals
   98         129 => 'Hangup',
   99         130 => 'Interrupt',
  100         131 => 'Quit and dump core',
  101         132 => 'Illegal instruction',
  102         133 => 'Trace/breakpoint trap',
  103         134 => 'Process aborted',
  104         135 => 'Bus error: "access to undefined portion of memory object"',
  105         136 => 'Floating point exception: "erroneous arithmetic operation"',
  106         137 => 'Kill (terminate immediately)',
  107         138 => 'User-defined 1',
  108         139 => 'Segmentation violation',
  109         140 => 'User-defined 2',
  110         141 => 'Write to pipe with no one reading',
  111         142 => 'Signal raised by alarm',
  112         143 => 'Termination (request to terminate)',
  113         // 144 - not defined
  114         145 => 'Child process terminated, stopped (or continued*)',
  115         146 => 'Continue if stopped',
  116         147 => 'Stop executing temporarily',
  117         148 => 'Terminal stop signal',
  118         149 => 'Background process attempting to read from tty ("in")',
  119         150 => 'Background process attempting to write to tty ("out")',
  120         151 => 'Urgent data available on socket',
  121         152 => 'CPU time limit exceeded',
  122         153 => 'File size limit exceeded',
  123         154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
  124         155 => 'Profiling timer expired',
  125         // 156 - not defined
  126         157 => 'Pollable event',
  127         // 158 - not defined
  128         159 => 'Bad syscall',
  129     ];
  130 
  131     /**
  132      * @param array          $command The command to run and its arguments listed as separate entries
  133      * @param string|null    $cwd     The working directory or null to use the working dir of the current PHP process
  134      * @param array|null     $env     The environment variables or null to use the same environment as the current PHP process
  135      * @param mixed|null     $input   The input as stream resource, scalar or \Traversable, or null for no input
  136      * @param int|float|null $timeout The timeout in seconds or null to disable
  137      *
  138      * @throws RuntimeException When proc_open is not installed
  139      */
  140     public function __construct($command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60)
  141     {
  142         if (!\function_exists('proc_open')) {
  143             throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.');
  144         }
  145 
  146         if (!\is_array($command)) {
  147             @trigger_error(sprintf('Passing a command as string when creating a "%s" instance is deprecated since Symfony 4.2, pass it as an array of its arguments instead, or use the "Process::fromShellCommandline()" constructor if you need features provided by the shell.', __CLASS__), E_USER_DEPRECATED);
  148         }
  149 
  150         $this->commandline = $command;
  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/bug.php?id=51800
  156         // @see : https://bugs.php.net/bug.php?id=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     }
  169 
  170     /**
  171      * Creates a Process instance as a command-line to be run in a shell wrapper.
  172      *
  173      * Command-lines are parsed by the shell of your OS (/bin/sh on Unix-like, cmd.exe on Windows.)
  174      * This allows using e.g. pipes or conditional execution. In this mode, signals are sent to the
  175      * shell wrapper and not to your commands.
  176      *
  177      * In order to inject dynamic values into command-lines, we strongly recommend using placeholders.
  178      * This will save escaping values, which is not portable nor secure anyway:
  179      *
  180      *   $process = Process::fromShellCommandline('my_command "$MY_VAR"');
  181      *   $process->run(null, ['MY_VAR' => $theValue]);
  182      *
  183      * @param string         $command The command line to pass to the shell of the OS
  184      * @param string|null    $cwd     The working directory or null to use the working dir of the current PHP process
  185      * @param array|null     $env     The environment variables or null to use the same environment as the current PHP process
  186      * @param mixed|null     $input   The input as stream resource, scalar or \Traversable, or null for no input
  187      * @param int|float|null $timeout The timeout in seconds or null to disable
  188      *
  189      * @throws RuntimeException When proc_open is not installed
  190      */
  191     public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60)
  192     {
  193         $process = new static([], $cwd, $env, $input, $timeout);
  194         $process->commandline = $command;
  195 
  196         return $process;
  197     }
  198 
  199     public function __destruct()
  200     {
  201         $this->stop(0);
  202     }
  203 
  204     public function __clone()
  205     {
  206         $this->resetProcessData();
  207     }
  208 
  209     /**
  210      * Runs the process.
  211      *
  212      * The callback receives the type of output (out or err) and
  213      * some bytes from the output in real-time. It allows to have feedback
  214      * from the independent process during execution.
  215      *
  216      * The STDOUT and STDERR are also available after the process is finished
  217      * via the getOutput() and getErrorOutput() methods.
  218      *
  219      * @param callable|null $callback A PHP callback to run whenever there is some
  220      *                                output available on STDOUT or STDERR
  221      * @param array         $env      An array of additional env vars to set when running the process
  222      *
  223      * @return int The exit status code
  224      *
  225      * @throws RuntimeException When process can't be launched
  226      * @throws RuntimeException When process stopped after receiving signal
  227      * @throws LogicException   In case a callback is provided and output has been disabled
  228      *
  229      * @final
  230      */
  231     public function run(callable $callback = null, array $env = []): int
  232     {
  233         $this->start($callback, $env);
  234 
  235         return $this->wait();
  236     }
  237 
  238     /**
  239      * Runs the process.
  240      *
  241      * This is identical to run() except that an exception is thrown if the process
  242      * exits with a non-zero exit code.
  243      *
  244      * @param callable|null $callback
  245      * @param array         $env      An array of additional env vars to set when running the process
  246      *
  247      * @return self
  248      *
  249      * @throws ProcessFailedException if the process didn't terminate successfully
  250      *
  251      * @final
  252      */
  253     public function mustRun(callable $callback = null, array $env = [])
  254     {
  255         if (0 !== $this->run($callback, $env)) {
  256             throw new ProcessFailedException($this);
  257         }
  258 
  259         return $this;
  260     }
  261 
  262     /**
  263      * Starts the process and returns after writing the input to STDIN.
  264      *
  265      * This method blocks until all STDIN data is sent to the process then it
  266      * returns while the process runs in the background.
  267      *
  268      * The termination of the process can be awaited with wait().
  269      *
  270      * The callback receives the type of output (out or err) and some bytes from
  271      * the output in real-time while writing the standard input to the process.
  272      * It allows to have feedback from the independent process during execution.
  273      *
  274      * @param callable|null $callback A PHP callback to run whenever there is some
  275      *                                output available on STDOUT or STDERR
  276      * @param array         $env      An array of additional env vars to set when running the process
  277      *
  278      * @throws RuntimeException When process can't be launched
  279      * @throws RuntimeException When process is already running
  280      * @throws LogicException   In case a callback is provided and output has been disabled
  281      */
  282     public function start(callable $callback = null, array $env = [])
  283     {
  284         if ($this->isRunning()) {
  285             throw new RuntimeException('Process is already running');
  286         }
  287 
  288         $this->resetProcessData();
  289         $this->starttime = $this->lastOutputTime = microtime(true);
  290         $this->callback = $this->buildCallback($callback);
  291         $this->hasCallback = null !== $callback;
  292         $descriptors = $this->getDescriptors();
  293 
  294         if (\is_array($commandline = $this->commandline)) {
  295             $commandline = implode(' ', array_map([$this, 'escapeArgument'], $commandline));
  296 
  297             if ('\\' !== \DIRECTORY_SEPARATOR) {
  298                 // exec is mandatory to deal with sending a signal to the process
  299                 $commandline = 'exec '.$commandline;
  300             }
  301         }
  302 
  303         if ($this->env) {
  304             $env += $this->env;
  305         }
  306         $env += $this->getDefaultEnv();
  307 
  308         $options = ['suppress_errors' => true];
  309 
  310         if ('\\' === \DIRECTORY_SEPARATOR) {
  311             $options['bypass_shell'] = true;
  312             $commandline = $this->prepareWindowsCommandLine($commandline, $env);
  313         } elseif (!$this->useFileHandles && $this->isSigchildEnabled()) {
  314             // last exit code is output on the fourth pipe and caught to work around --enable-sigchild
  315             $descriptors[3] = ['pipe', 'w'];
  316 
  317             // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input
  318             $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;';
  319             $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code';
  320 
  321             // Workaround for the bug, when PTS functionality is enabled.
  322             // @see : https://bugs.php.net/69442
  323             $ptsWorkaround = fopen(__FILE__, 'r');
  324         }
  325 
  326         $envPairs = [];
  327         foreach ($env as $k => $v) {
  328             if (false !== $v) {
  329                 $envPairs[] = $k.'='.$v;
  330             }
  331         }
  332 
  333         if (!is_dir($this->cwd)) {
  334             throw new RuntimeException('The provided cwd does not exist.');
  335         }
  336 
  337         $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $options);
  338 
  339         if (!\is_resource($this->process)) {
  340             throw new RuntimeException('Unable to launch a new process.');
  341         }
  342         $this->status = self::STATUS_STARTED;
  343 
  344         if (isset($descriptors[3])) {
  345             $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]);
  346         }
  347 
  348         if ($this->tty) {
  349             return;
  350         }
  351 
  352         $this->updateStatus(false);
  353         $this->checkTimeout();
  354     }
  355 
  356     /**
  357      * Restarts the process.
  358      *
  359      * Be warned that the process is cloned before being started.
  360      *
  361      * @param callable|null $callback A PHP callback to run whenever there is some
  362      *                                output available on STDOUT or STDERR
  363      * @param array         $env      An array of additional env vars to set when running the process
  364      *
  365      * @return $this
  366      *
  367      * @throws RuntimeException When process can't be launched
  368      * @throws RuntimeException When process is already running
  369      *
  370      * @see start()
  371      *
  372      * @final
  373      */
  374     public function restart(callable $callback = null, array $env = [])
  375     {
  376         if ($this->isRunning()) {
  377             throw new RuntimeException('Process is already running');
  378         }
  379 
  380         $process = clone $this;
  381         $process->start($callback, $env);
  382 
  383         return $process;
  384     }
  385 
  386     /**
  387      * Waits for the process to terminate.
  388      *
  389      * The callback receives the type of output (out or err) and some bytes
  390      * from the output in real-time while writing the standard input to the process.
  391      * It allows to have feedback from the independent process during execution.
  392      *
  393      * @param callable|null $callback A valid PHP callback
  394      *
  395      * @return int The exitcode of the process
  396      *
  397      * @throws RuntimeException When process timed out
  398      * @throws RuntimeException When process stopped after receiving signal
  399      * @throws LogicException   When process is not yet started
  400      */
  401     public function wait(callable $callback = null)
  402     {
  403         $this->requireProcessIsStarted(__FUNCTION__);
  404 
  405         $this->updateStatus(false);
  406 
  407         if (null !== $callback) {
  408             if (!$this->processPipes->haveReadSupport()) {
  409                 $this->stop(0);
  410                 throw new \LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait"');
  411             }
  412             $this->callback = $this->buildCallback($callback);
  413         }
  414 
  415         do {
  416             $this->checkTimeout();
  417             $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
  418             $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
  419         } while ($running);
  420 
  421         while ($this->isRunning()) {
  422             $this->checkTimeout();
  423             usleep(1000);
  424         }
  425 
  426         if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
  427             throw new ProcessSignaledException($this);
  428         }
  429 
  430         return $this->exitcode;
  431     }
  432 
  433     /**
  434      * Waits until the callback returns true.
  435      *
  436      * The callback receives the type of output (out or err) and some bytes
  437      * from the output in real-time while writing the standard input to the process.
  438      * It allows to have feedback from the independent process during execution.
  439      *
  440      * @throws RuntimeException When process timed out
  441      * @throws LogicException   When process is not yet started
  442      */
  443     public function waitUntil(callable $callback): bool
  444     {
  445         $this->requireProcessIsStarted(__FUNCTION__);
  446         $this->updateStatus(false);
  447 
  448         if (!$this->processPipes->haveReadSupport()) {
  449             $this->stop(0);
  450             throw new \LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".');
  451         }
  452         $callback = $this->buildCallback($callback);
  453 
  454         $ready = false;
  455         while (true) {
  456             $this->checkTimeout();
  457             $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
  458             $output = $this->processPipes->readAndWrite($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
  459 
  460             foreach ($output as $type => $data) {
  461                 if (3 !== $type) {
  462                     $ready = $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data) || $ready;
  463                 } elseif (!isset($this->fallbackStatus['signaled'])) {
  464                     $this->fallbackStatus['exitcode'] = (int) $data;
  465                 }
  466             }
  467             if ($ready) {
  468                 return true;
  469             }
  470             if (!$running) {
  471                 return false;
  472             }
  473 
  474             usleep(1000);
  475         }
  476     }
  477 
  478     /**
  479      * Returns the Pid (process identifier), if applicable.
  480      *
  481      * @return int|null The process id if running, null otherwise
  482      */
  483     public function getPid()
  484     {
  485         return $this->isRunning() ? $this->processInformation['pid'] : null;
  486     }
  487 
  488     /**
  489      * Sends a POSIX signal to the process.
  490      *
  491      * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php)
  492      *
  493      * @return $this
  494      *
  495      * @throws LogicException   In case the process is not running
  496      * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
  497      * @throws RuntimeException In case of failure
  498      */
  499     public function signal($signal)
  500     {
  501         $this->doSignal($signal, true);
  502 
  503         return $this;
  504     }
  505 
  506     /**
  507      * Disables fetching output and error output from the underlying process.
  508      *
  509      * @return $this
  510      *
  511      * @throws RuntimeException In case the process is already running
  512      * @throws LogicException   if an idle timeout is set
  513      */
  514     public function disableOutput()
  515     {
  516         if ($this->isRunning()) {
  517             throw new RuntimeException('Disabling output while the process is running is not possible.');
  518         }
  519         if (null !== $this->idleTimeout) {
  520             throw new LogicException('Output can not be disabled while an idle timeout is set.');
  521         }
  522 
  523         $this->outputDisabled = true;
  524 
  525         return $this;
  526     }
  527 
  528     /**
  529      * Enables fetching output and error output from the underlying process.
  530      *
  531      * @return $this
  532      *
  533      * @throws RuntimeException In case the process is already running
  534      */
  535     public function enableOutput()
  536     {
  537         if ($this->isRunning()) {
  538             throw new RuntimeException('Enabling output while the process is running is not possible.');
  539         }
  540 
  541         $this->outputDisabled = false;
  542 
  543         return $this;
  544     }
  545 
  546     /**
  547      * Returns true in case the output is disabled, false otherwise.
  548      *
  549      * @return bool
  550      */
  551     public function isOutputDisabled()
  552     {
  553         return $this->outputDisabled;
  554     }
  555 
  556     /**
  557      * Returns the current output of the process (STDOUT).
  558      *
  559      * @return string The process output
  560      *
  561      * @throws LogicException in case the output has been disabled
  562      * @throws LogicException In case the process is not started
  563      */
  564     public function getOutput()
  565     {
  566         $this->readPipesForOutput(__FUNCTION__);
  567 
  568         if (false === $ret = stream_get_contents($this->stdout, -1, 0)) {
  569             return '';
  570         }
  571 
  572         return $ret;
  573     }
  574 
  575     /**
  576      * Returns the output incrementally.
  577      *
  578      * In comparison with the getOutput method which always return the whole
  579      * output, this one returns the new output since the last call.
  580      *
  581      * @return string The process output since the last call
  582      *
  583      * @throws LogicException in case the output has been disabled
  584      * @throws LogicException In case the process is not started
  585      */
  586     public function getIncrementalOutput()
  587     {
  588         $this->readPipesForOutput(__FUNCTION__);
  589 
  590         $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
  591         $this->incrementalOutputOffset = ftell($this->stdout);
  592 
  593         if (false === $latest) {
  594             return '';
  595         }
  596 
  597         return $latest;
  598     }
  599 
  600     /**
  601      * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR).
  602      *
  603      * @param int $flags A bit field of Process::ITER_* flags
  604      *
  605      * @throws LogicException in case the output has been disabled
  606      * @throws LogicException In case the process is not started
  607      *
  608      * @return \Generator
  609      */
  610     public function getIterator($flags = 0)
  611     {
  612         $this->readPipesForOutput(__FUNCTION__, false);
  613 
  614         $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags);
  615         $blocking = !(self::ITER_NON_BLOCKING & $flags);
  616         $yieldOut = !(self::ITER_SKIP_OUT & $flags);
  617         $yieldErr = !(self::ITER_SKIP_ERR & $flags);
  618 
  619         while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) {
  620             if ($yieldOut) {
  621                 $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
  622 
  623                 if (isset($out[0])) {
  624                     if ($clearOutput) {
  625                         $this->clearOutput();
  626                     } else {
  627                         $this->incrementalOutputOffset = ftell($this->stdout);
  628                     }
  629 
  630                     yield self::OUT => $out;
  631                 }
  632             }
  633 
  634             if ($yieldErr) {
  635                 $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
  636 
  637                 if (isset($err[0])) {
  638                     if ($clearOutput) {
  639                         $this->clearErrorOutput();
  640                     } else {
  641                         $this->incrementalErrorOutputOffset = ftell($this->stderr);
  642                     }
  643 
  644                     yield self::ERR => $err;
  645                 }
  646             }
  647 
  648             if (!$blocking && !isset($out[0]) && !isset($err[0])) {
  649                 yield self::OUT => '';
  650             }
  651 
  652             $this->checkTimeout();
  653             $this->readPipesForOutput(__FUNCTION__, $blocking);
  654         }
  655     }
  656 
  657     /**
  658      * Clears the process output.
  659      *
  660      * @return $this
  661      */
  662     public function clearOutput()
  663     {
  664         ftruncate($this->stdout, 0);
  665         fseek($this->stdout, 0);
  666         $this->incrementalOutputOffset = 0;
  667 
  668         return $this;
  669     }
  670 
  671     /**
  672      * Returns the current error output of the process (STDERR).
  673      *
  674      * @return string The process error output
  675      *
  676      * @throws LogicException in case the output has been disabled
  677      * @throws LogicException In case the process is not started
  678      */
  679     public function getErrorOutput()
  680     {
  681         $this->readPipesForOutput(__FUNCTION__);
  682 
  683         if (false === $ret = stream_get_contents($this->stderr, -1, 0)) {
  684             return '';
  685         }
  686 
  687         return $ret;
  688     }
  689 
  690     /**
  691      * Returns the errorOutput incrementally.
  692      *
  693      * In comparison with the getErrorOutput method which always return the
  694      * whole error output, this one returns the new error output since the last
  695      * call.
  696      *
  697      * @return string The process error output since the last call
  698      *
  699      * @throws LogicException in case the output has been disabled
  700      * @throws LogicException In case the process is not started
  701      */
  702     public function getIncrementalErrorOutput()
  703     {
  704         $this->readPipesForOutput(__FUNCTION__);
  705 
  706         $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
  707         $this->incrementalErrorOutputOffset = ftell($this->stderr);
  708 
  709         if (false === $latest) {
  710             return '';
  711         }
  712 
  713         return $latest;
  714     }
  715 
  716     /**
  717      * Clears the process output.
  718      *
  719      * @return $this
  720      */
  721     public function clearErrorOutput()
  722     {
  723         ftruncate($this->stderr, 0);
  724         fseek($this->stderr, 0);
  725         $this->incrementalErrorOutputOffset = 0;
  726 
  727         return $this;
  728     }
  729 
  730     /**
  731      * Returns the exit code returned by the process.
  732      *
  733      * @return int|null The exit status code, null if the Process is not terminated
  734      */
  735     public function getExitCode()
  736     {
  737         $this->updateStatus(false);
  738 
  739         return $this->exitcode;
  740     }
  741 
  742     /**
  743      * Returns a string representation for the exit code returned by the process.
  744      *
  745      * This method relies on the Unix exit code status standardization
  746      * and might not be relevant for other operating systems.
  747      *
  748      * @return string|null A string representation for the exit status code, null if the Process is not terminated
  749      *
  750      * @see http://tldp.org/LDP/abs/html/exitcodes.html
  751      * @see http://en.wikipedia.org/wiki/Unix_signal
  752      */
  753     public function getExitCodeText()
  754     {
  755         if (null === $exitcode = $this->getExitCode()) {
  756             return;
  757         }
  758 
  759         return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error';
  760     }
  761 
  762     /**
  763      * Checks if the process ended successfully.
  764      *
  765      * @return bool true if the process ended successfully, false otherwise
  766      */
  767     public function isSuccessful()
  768     {
  769         return 0 === $this->getExitCode();
  770     }
  771 
  772     /**
  773      * Returns true if the child process has been terminated by an uncaught signal.
  774      *
  775      * It always returns false on Windows.
  776      *
  777      * @return bool
  778      *
  779      * @throws LogicException In case the process is not terminated
  780      */
  781     public function hasBeenSignaled()
  782     {
  783         $this->requireProcessIsTerminated(__FUNCTION__);
  784 
  785         return $this->processInformation['signaled'];
  786     }
  787 
  788     /**
  789      * Returns the number of the signal that caused the child process to terminate its execution.
  790      *
  791      * It is only meaningful if hasBeenSignaled() returns true.
  792      *
  793      * @return int
  794      *
  795      * @throws RuntimeException In case --enable-sigchild is activated
  796      * @throws LogicException   In case the process is not terminated
  797      */
  798     public function getTermSignal()
  799     {
  800         $this->requireProcessIsTerminated(__FUNCTION__);
  801 
  802         if ($this->isSigchildEnabled() && -1 === $this->processInformation['termsig']) {
  803             throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
  804         }
  805 
  806         return $this->processInformation['termsig'];
  807     }
  808 
  809     /**
  810      * Returns true if the child process has been stopped by a signal.
  811      *
  812      * It always returns false on Windows.
  813      *
  814      * @return bool
  815      *
  816      * @throws LogicException In case the process is not terminated
  817      */
  818     public function hasBeenStopped()
  819     {
  820         $this->requireProcessIsTerminated(__FUNCTION__);
  821 
  822         return $this->processInformation['stopped'];
  823     }
  824 
  825     /**
  826      * Returns the number of the signal that caused the child process to stop its execution.
  827      *
  828      * It is only meaningful if hasBeenStopped() returns true.
  829      *
  830      * @return int
  831      *
  832      * @throws LogicException In case the process is not terminated
  833      */
  834     public function getStopSignal()
  835     {
  836         $this->requireProcessIsTerminated(__FUNCTION__);
  837 
  838         return $this->processInformation['stopsig'];
  839     }
  840 
  841     /**
  842      * Checks if the process is currently running.
  843      *
  844      * @return bool true if the process is currently running, false otherwise
  845      */
  846     public function isRunning()
  847     {
  848         if (self::STATUS_STARTED !== $this->status) {
  849             return false;
  850         }
  851 
  852         $this->updateStatus(false);
  853 
  854         return $this->processInformation['running'];
  855     }
  856 
  857     /**
  858      * Checks if the process has been started with no regard to the current state.
  859      *
  860      * @return bool true if status is ready, false otherwise
  861      */
  862     public function isStarted()
  863     {
  864         return self::STATUS_READY != $this->status;
  865     }
  866 
  867     /**
  868      * Checks if the process is terminated.
  869      *
  870      * @return bool true if process is terminated, false otherwise
  871      */
  872     public function isTerminated()
  873     {
  874         $this->updateStatus(false);
  875 
  876         return self::STATUS_TERMINATED == $this->status;
  877     }
  878 
  879     /**
  880      * Gets the process status.
  881      *
  882      * The status is one of: ready, started, terminated.
  883      *
  884      * @return string The current process status
  885      */
  886     public function getStatus()
  887     {
  888         $this->updateStatus(false);
  889 
  890         return $this->status;
  891     }
  892 
  893     /**
  894      * Stops the process.
  895      *
  896      * @param int|float $timeout The timeout in seconds
  897      * @param int       $signal  A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
  898      *
  899      * @return int The exit-code of the process
  900      */
  901     public function stop($timeout = 10, $signal = null)
  902     {
  903         $timeoutMicro = microtime(true) + $timeout;
  904         if ($this->isRunning()) {
  905             // given SIGTERM may not be defined and that "proc_terminate" uses the constant value and not the constant itself, we use the same here
  906             $this->doSignal(15, false);
  907             do {
  908                 usleep(1000);
  909             } while ($this->isRunning() && microtime(true) < $timeoutMicro);
  910 
  911             if ($this->isRunning()) {
  912                 // Avoid exception here: process is supposed to be running, but it might have stopped just
  913                 // after this line. In any case, let's silently discard the error, we cannot do anything.
  914                 $this->doSignal($signal ?: 9, false);
  915             }
  916         }
  917 
  918         if ($this->isRunning()) {
  919             if (isset($this->fallbackStatus['pid'])) {
  920                 unset($this->fallbackStatus['pid']);
  921 
  922                 return $this->stop(0, $signal);
  923             }
  924             $this->close();
  925         }
  926 
  927         return $this->exitcode;
  928     }
  929 
  930     /**
  931      * Adds a line to the STDOUT stream.
  932      *
  933      * @internal
  934      */
  935     public function addOutput(string $line)
  936     {
  937         $this->lastOutputTime = microtime(true);
  938 
  939         fseek($this->stdout, 0, SEEK_END);
  940         fwrite($this->stdout, $line);
  941         fseek($this->stdout, $this->incrementalOutputOffset);
  942     }
  943 
  944     /**
  945      * Adds a line to the STDERR stream.
  946      *
  947      * @internal
  948      */
  949     public function addErrorOutput(string $line)
  950     {
  951         $this->lastOutputTime = microtime(true);
  952 
  953         fseek($this->stderr, 0, SEEK_END);
  954         fwrite($this->stderr, $line);
  955         fseek($this->stderr, $this->incrementalErrorOutputOffset);
  956     }
  957 
  958     /**
  959      * Gets the command line to be executed.
  960      *
  961      * @return string The command to execute
  962      */
  963     public function getCommandLine()
  964     {
  965         return \is_array($this->commandline) ? implode(' ', array_map([$this, 'escapeArgument'], $this->commandline)) : $this->commandline;
  966     }
  967 
  968     /**
  969      * Sets the command line to be executed.
  970      *
  971      * @param string|array $commandline The command to execute
  972      *
  973      * @return self The current Process instance
  974      *
  975      * @deprecated since Symfony 4.2.
  976      */
  977     public function setCommandLine($commandline)
  978     {
  979         @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED);
  980 
  981         $this->commandline = $commandline;
  982 
  983         return $this;
  984     }
  985 
  986     /**
  987      * Gets the process timeout (max. runtime).
  988      *
  989      * @return float|null The timeout in seconds or null if it's disabled
  990      */
  991     public function getTimeout()
  992     {
  993         return $this->timeout;
  994     }
  995 
  996     /**
  997      * Gets the process idle timeout (max. time since last output).
  998      *
  999      * @return float|null The timeout in seconds or null if it's disabled
 1000      */
 1001     public function getIdleTimeout()
 1002     {
 1003         return $this->idleTimeout;
 1004     }
 1005 
 1006     /**
 1007      * Sets the process timeout (max. runtime).
 1008      *
 1009      * To disable the timeout, set this value to null.
 1010      *
 1011      * @param int|float|null $timeout The timeout in seconds
 1012      *
 1013      * @return self The current Process instance
 1014      *
 1015      * @throws InvalidArgumentException if the timeout is negative
 1016      */
 1017     public function setTimeout($timeout)
 1018     {
 1019         $this->timeout = $this->validateTimeout($timeout);
 1020 
 1021         return $this;
 1022     }
 1023 
 1024     /**
 1025      * Sets the process idle timeout (max. time since last output).
 1026      *
 1027      * To disable the timeout, set this value to null.
 1028      *
 1029      * @param int|float|null $timeout The timeout in seconds
 1030      *
 1031      * @return self The current Process instance
 1032      *
 1033      * @throws LogicException           if the output is disabled
 1034      * @throws InvalidArgumentException if the timeout is negative
 1035      */
 1036     public function setIdleTimeout($timeout)
 1037     {
 1038         if (null !== $timeout && $this->outputDisabled) {
 1039             throw new LogicException('Idle timeout can not be set while the output is disabled.');
 1040         }
 1041 
 1042         $this->idleTimeout = $this->validateTimeout($timeout);
 1043 
 1044         return $this;
 1045     }
 1046 
 1047     /**
 1048      * Enables or disables the TTY mode.
 1049      *
 1050      * @param bool $tty True to enabled and false to disable
 1051      *
 1052      * @return self The current Process instance
 1053      *
 1054      * @throws RuntimeException In case the TTY mode is not supported
 1055      */
 1056     public function setTty($tty)
 1057     {
 1058         if ('\\' === \DIRECTORY_SEPARATOR && $tty) {
 1059             throw new RuntimeException('TTY mode is not supported on Windows platform.');
 1060         }
 1061 
 1062         if ($tty && !self::isTtySupported()) {
 1063             throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.');
 1064         }
 1065 
 1066         $this->tty = (bool) $tty;
 1067 
 1068         return $this;
 1069     }
 1070 
 1071     /**
 1072      * Checks if the TTY mode is enabled.
 1073      *
 1074      * @return bool true if the TTY mode is enabled, false otherwise
 1075      */
 1076     public function isTty()
 1077     {
 1078         return $this->tty;
 1079     }
 1080 
 1081     /**
 1082      * Sets PTY mode.
 1083      *
 1084      * @param bool $bool
 1085      *
 1086      * @return self
 1087      */
 1088     public function setPty($bool)
 1089     {
 1090         $this->pty = (bool) $bool;
 1091 
 1092         return $this;
 1093     }
 1094 
 1095     /**
 1096      * Returns PTY state.
 1097      *
 1098      * @return bool
 1099      */
 1100     public function isPty()
 1101     {
 1102         return $this->pty;
 1103     }
 1104 
 1105     /**
 1106      * Gets the working directory.
 1107      *
 1108      * @return string|null The current working directory or null on failure
 1109      */
 1110     public function getWorkingDirectory()
 1111     {
 1112         if (null === $this->cwd) {
 1113             // getcwd() will return false if any one of the parent directories does not have
 1114             // the readable or search mode set, even if the current directory does
 1115             return getcwd() ?: null;
 1116         }
 1117 
 1118         return $this->cwd;
 1119     }
 1120 
 1121     /**
 1122      * Sets the current working directory.
 1123      *
 1124      * @param string $cwd The new working directory
 1125      *
 1126      * @return self The current Process instance
 1127      */
 1128     public function setWorkingDirectory($cwd)
 1129     {
 1130         $this->cwd = $cwd;
 1131 
 1132         return $this;
 1133     }
 1134 
 1135     /**
 1136      * Gets the environment variables.
 1137      *
 1138      * @return array The current environment variables
 1139      */
 1140     public function getEnv()
 1141     {
 1142         return $this->env;
 1143     }
 1144 
 1145     /**
 1146      * Sets the environment variables.
 1147      *
 1148      * Each environment variable value should be a string.
 1149      * If it is an array, the variable is ignored.
 1150      * If it is false or null, it will be removed when
 1151      * env vars are otherwise inherited.
 1152      *
 1153      * That happens in PHP when 'argv' is registered into
 1154      * the $_ENV array for instance.
 1155      *
 1156      * @param array $env The new environment variables
 1157      *
 1158      * @return self The current Process instance
 1159      */
 1160     public function setEnv(array $env)
 1161     {
 1162         // Process can not handle env values that are arrays
 1163         $env = array_filter($env, function ($value) {
 1164             return !\is_array($value);
 1165         });
 1166 
 1167         $this->env = $env;
 1168 
 1169         return $this;
 1170     }
 1171 
 1172     /**
 1173      * Gets the Process input.
 1174      *
 1175      * @return resource|string|\Iterator|null The Process input
 1176      */
 1177     public function getInput()
 1178     {
 1179         return $this->input;
 1180     }
 1181 
 1182     /**
 1183      * Sets the input.
 1184      *
 1185      * This content will be passed to the underlying process standard input.
 1186      *
 1187      * @param string|int|float|bool|resource|\Traversable|null $input The content
 1188      *
 1189      * @return self The current Process instance
 1190      *
 1191      * @throws LogicException In case the process is running
 1192      */
 1193     public function setInput($input)
 1194     {
 1195         if ($this->isRunning()) {
 1196             throw new LogicException('Input can not be set while the process is running.');
 1197         }
 1198 
 1199         $this->input = ProcessUtils::validateInput(__METHOD__, $input);
 1200 
 1201         return $this;
 1202     }
 1203 
 1204     /**
 1205      * Sets whether environment variables will be inherited or not.
 1206      *
 1207      * @param bool $inheritEnv
 1208      *
 1209      * @return self The current Process instance
 1210      */
 1211     public function inheritEnvironmentVariables($inheritEnv = true)
 1212     {
 1213         if (!$inheritEnv) {
 1214             throw new InvalidArgumentException('Not inheriting environment variables is not supported.');
 1215         }
 1216 
 1217         return $this;
 1218     }
 1219 
 1220     /**
 1221      * Performs a check between the timeout definition and the time the process started.
 1222      *
 1223      * In case you run a background process (with the start method), you should
 1224      * trigger this method regularly to ensure the process timeout
 1225      *
 1226      * @throws ProcessTimedOutException In case the timeout was reached
 1227      */
 1228     public function checkTimeout()
 1229     {
 1230         if (self::STATUS_STARTED !== $this->status) {
 1231             return;
 1232         }
 1233 
 1234         if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
 1235             $this->stop(0);
 1236 
 1237             throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL);
 1238         }
 1239 
 1240         if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
 1241             $this->stop(0);
 1242 
 1243             throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE);
 1244         }
 1245     }
 1246 
 1247     /**
 1248      * Returns whether TTY is supported on the current operating system.
 1249      */
 1250     public static function isTtySupported(): bool
 1251     {
 1252         static $isTtySupported;
 1253 
 1254         if (null === $isTtySupported) {
 1255             $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes);
 1256         }
 1257 
 1258         return $isTtySupported;
 1259     }
 1260 
 1261     /**
 1262      * Returns whether PTY is supported on the current operating system.
 1263      *
 1264      * @return bool
 1265      */
 1266     public static function isPtySupported()
 1267     {
 1268         static $result;
 1269 
 1270         if (null !== $result) {
 1271             return $result;
 1272         }
 1273 
 1274         if ('\\' === \DIRECTORY_SEPARATOR) {
 1275             return $result = false;
 1276         }
 1277 
 1278         return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes);
 1279     }
 1280 
 1281     /**
 1282      * Creates the descriptors needed by the proc_open.
 1283      */
 1284     private function getDescriptors(): array
 1285     {
 1286         if ($this->input instanceof \Iterator) {
 1287             $this->input->rewind();
 1288         }
 1289         if ('\\' === \DIRECTORY_SEPARATOR) {
 1290             $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback);
 1291         } else {
 1292             $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback);
 1293         }
 1294 
 1295         return $this->processPipes->getDescriptors();
 1296     }
 1297 
 1298     /**
 1299      * Builds up the callback used by wait().
 1300      *
 1301      * The callbacks adds all occurred output to the specific buffer and calls
 1302      * the user callback (if present) with the received output.
 1303      *
 1304      * @param callable|null $callback The user defined PHP callback
 1305      *
 1306      * @return \Closure A PHP closure
 1307      */
 1308     protected function buildCallback(callable $callback = null)
 1309     {
 1310         if ($this->outputDisabled) {
 1311             return function ($type, $data) use ($callback) {
 1312                 if (null !== $callback) {
 1313                     return $callback($type, $data);
 1314                 }
 1315             };
 1316         }
 1317 
 1318         $out = self::OUT;
 1319 
 1320         return function ($type, $data) use ($callback, $out) {
 1321             if ($out == $type) {
 1322                 $this->addOutput($data);
 1323             } else {
 1324                 $this->addErrorOutput($data);
 1325             }
 1326 
 1327             if (null !== $callback) {
 1328                 return $callback($type, $data);
 1329             }
 1330         };
 1331     }
 1332 
 1333     /**
 1334      * Updates the status of the process, reads pipes.
 1335      *
 1336      * @param bool $blocking Whether to use a blocking read call
 1337      */
 1338     protected function updateStatus($blocking)
 1339     {
 1340         if (self::STATUS_STARTED !== $this->status) {
 1341             return;
 1342         }
 1343 
 1344         $this->processInformation = proc_get_status($this->process);
 1345         $running = $this->processInformation['running'];
 1346 
 1347         $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running);
 1348 
 1349         if ($this->fallbackStatus && $this->isSigchildEnabled()) {
 1350             $this->processInformation = $this->fallbackStatus + $this->processInformation;
 1351         }
 1352 
 1353         if (!$running) {
 1354             $this->close();
 1355         }
 1356     }
 1357 
 1358     /**
 1359      * Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
 1360      *
 1361      * @return bool
 1362      */
 1363     protected function isSigchildEnabled()
 1364     {
 1365         if (null !== self::$sigchild) {
 1366             return self::$sigchild;
 1367         }
 1368 
 1369         if (!\function_exists('phpinfo')) {
 1370             return self::$sigchild = false;
 1371         }
 1372 
 1373         ob_start();
 1374         phpinfo(INFO_GENERAL);
 1375 
 1376         return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
 1377     }
 1378 
 1379     /**
 1380      * Reads pipes for the freshest output.
 1381      *
 1382      * @param string $caller   The name of the method that needs fresh outputs
 1383      * @param bool   $blocking Whether to use blocking calls or not
 1384      *
 1385      * @throws LogicException in case output has been disabled or process is not started
 1386      */
 1387     private function readPipesForOutput(string $caller, bool $blocking = false)
 1388     {
 1389         if ($this->outputDisabled) {
 1390             throw new LogicException('Output has been disabled.');
 1391         }
 1392 
 1393         $this->requireProcessIsStarted($caller);
 1394 
 1395         $this->updateStatus($blocking);
 1396     }
 1397 
 1398     /**
 1399      * Validates and returns the filtered timeout.
 1400      *
 1401      * @throws InvalidArgumentException if the given timeout is a negative number
 1402      */
 1403     private function validateTimeout(?float $timeout): ?float
 1404     {
 1405         $timeout = (float) $timeout;
 1406 
 1407         if (0.0 === $timeout) {
 1408             $timeout = null;
 1409         } elseif ($timeout < 0) {
 1410             throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
 1411         }
 1412 
 1413         return $timeout;
 1414     }
 1415 
 1416     /**
 1417      * Reads pipes, executes callback.
 1418      *
 1419      * @param bool $blocking Whether to use blocking calls or not
 1420      * @param bool $close    Whether to close file handles or not
 1421      */
 1422     private function readPipes(bool $blocking, bool $close)
 1423     {
 1424         $result = $this->processPipes->readAndWrite($blocking, $close);
 1425 
 1426         $callback = $this->callback;
 1427         foreach ($result as $type => $data) {
 1428             if (3 !== $type) {
 1429                 $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data);
 1430             } elseif (!isset($this->fallbackStatus['signaled'])) {
 1431                 $this->fallbackStatus['exitcode'] = (int) $data;
 1432             }
 1433         }
 1434     }
 1435 
 1436     /**
 1437      * Closes process resource, closes file handles, sets the exitcode.
 1438      *
 1439      * @return int The exitcode
 1440      */
 1441     private function close(): int
 1442     {
 1443         $this->processPipes->close();
 1444         if (\is_resource($this->process)) {
 1445             proc_close($this->process);
 1446         }
 1447         $this->exitcode = $this->processInformation['exitcode'];
 1448         $this->status = self::STATUS_TERMINATED;
 1449 
 1450         if (-1 === $this->exitcode) {
 1451             if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) {
 1452                 // if process has been signaled, no exitcode but a valid termsig, apply Unix convention
 1453                 $this->exitcode = 128 + $this->processInformation['termsig'];
 1454             } elseif ($this->isSigchildEnabled()) {
 1455                 $this->processInformation['signaled'] = true;
 1456                 $this->processInformation['termsig'] = -1;
 1457             }
 1458         }
 1459 
 1460         // Free memory from self-reference callback created by buildCallback
 1461         // Doing so in other contexts like __destruct or by garbage collector is ineffective
 1462         // Now pipes are closed, so the callback is no longer necessary
 1463         $this->callback = null;
 1464 
 1465         return $this->exitcode;
 1466     }
 1467 
 1468     /**
 1469      * Resets data related to the latest run of the process.
 1470      */
 1471     private function resetProcessData()
 1472     {
 1473         $this->starttime = null;
 1474         $this->callback = null;
 1475         $this->exitcode = null;
 1476         $this->fallbackStatus = [];
 1477         $this->processInformation = null;
 1478         $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b');
 1479         $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b');
 1480         $this->process = null;
 1481         $this->latestSignal = null;
 1482         $this->status = self::STATUS_READY;
 1483         $this->incrementalOutputOffset = 0;
 1484         $this->incrementalErrorOutputOffset = 0;
 1485     }
 1486 
 1487     /**
 1488      * Sends a POSIX signal to the process.
 1489      *
 1490      * @param int  $signal         A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php)
 1491      * @param bool $throwException Whether to throw exception in case signal failed
 1492      *
 1493      * @return bool True if the signal was sent successfully, false otherwise
 1494      *
 1495      * @throws LogicException   In case the process is not running
 1496      * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
 1497      * @throws RuntimeException In case of failure
 1498      */
 1499     private function doSignal(int $signal, bool $throwException): bool
 1500     {
 1501         if (null === $pid = $this->getPid()) {
 1502             if ($throwException) {
 1503                 throw new LogicException('Can not send signal on a non running process.');
 1504             }
 1505 
 1506             return false;
 1507         }
 1508 
 1509         if ('\\' === \DIRECTORY_SEPARATOR) {
 1510             exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode);
 1511             if ($exitCode && $this->isRunning()) {
 1512                 if ($throwException) {
 1513                     throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output)));
 1514                 }
 1515 
 1516                 return false;
 1517             }
 1518         } else {
 1519             if (!$this->isSigchildEnabled()) {
 1520                 $ok = @proc_terminate($this->process, $signal);
 1521             } elseif (\function_exists('posix_kill')) {
 1522                 $ok = @posix_kill($pid, $signal);
 1523             } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) {
 1524                 $ok = false === fgets($pipes[2]);
 1525             }
 1526             if (!$ok) {
 1527                 if ($throwException) {
 1528                     throw new RuntimeException(sprintf('Error while sending signal "%s".', $signal));
 1529                 }
 1530 
 1531                 return false;
 1532             }
 1533         }
 1534 
 1535         $this->latestSignal = $signal;
 1536         $this->fallbackStatus['signaled'] = true;
 1537         $this->fallbackStatus['exitcode'] = -1;
 1538         $this->fallbackStatus['termsig'] = $this->latestSignal;
 1539 
 1540         return true;
 1541     }
 1542 
 1543     private function prepareWindowsCommandLine(string $cmd, array &$env)
 1544     {
 1545         $uid = uniqid('', true);
 1546         $varCount = 0;
 1547         $varCache = [];
 1548         $cmd = preg_replace_callback(
 1549             '/"(?:(
 1550                 [^"%!^]*+
 1551                 (?:
 1552                     (?: !LF! | "(?:\^[%!^])?+" )
 1553                     [^"%!^]*+
 1554                 )++
 1555             ) | [^"]*+ )"/x',
 1556             function ($m) use (&$env, &$varCache, &$varCount, $uid) {
 1557                 if (!isset($m[1])) {
 1558                     return $m[0];
 1559                 }
 1560                 if (isset($varCache[$m[0]])) {
 1561                     return $varCache[$m[0]];
 1562                 }
 1563                 if (false !== strpos($value = $m[1], "\0")) {
 1564                     $value = str_replace("\0", '?', $value);
 1565                 }
 1566                 if (false === strpbrk($value, "\"%!\n")) {
 1567                     return '"'.$value.'"';
 1568                 }
 1569 
 1570                 $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value);
 1571                 $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"';
 1572                 $var = $uid.++$varCount;
 1573 
 1574                 $env[$var] = $value;
 1575 
 1576                 return $varCache[$m[0]] = '!'.$var.'!';
 1577             },
 1578             $cmd
 1579         );
 1580 
 1581         $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')';
 1582         foreach ($this->processPipes->getFiles() as $offset => $filename) {
 1583             $cmd .= ' '.$offset.'>"'.$filename.'"';
 1584         }
 1585 
 1586         return $cmd;
 1587     }
 1588 
 1589     /**
 1590      * Ensures the process is running or terminated, throws a LogicException if the process has a not started.
 1591      *
 1592      * @throws LogicException if the process has not run
 1593      */
 1594     private function requireProcessIsStarted(string $functionName)
 1595     {
 1596         if (!$this->isStarted()) {
 1597             throw new LogicException(sprintf('Process must be started before calling %s.', $functionName));
 1598         }
 1599     }
 1600 
 1601     /**
 1602      * Ensures the process is terminated, throws a LogicException if the process has a status different than "terminated".
 1603      *
 1604      * @throws LogicException if the process is not yet terminated
 1605      */
 1606     private function requireProcessIsTerminated(string $functionName)
 1607     {
 1608         if (!$this->isTerminated()) {
 1609             throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName));
 1610         }
 1611     }
 1612 
 1613     /**
 1614      * Escapes a string to be used as a shell argument.
 1615      */
 1616     private function escapeArgument(?string $argument): string
 1617     {
 1618         if ('' === $argument || null === $argument) {
 1619             return '""';
 1620         }
 1621         if ('\\' !== \DIRECTORY_SEPARATOR) {
 1622             return "'".str_replace("'", "'\\''", $argument)."'";
 1623         }
 1624         if (false !== strpos($argument, "\0")) {
 1625             $argument = str_replace("\0", '?', $argument);
 1626         }
 1627         if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) {
 1628             return $argument;
 1629         }
 1630         $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument);
 1631 
 1632         return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"';
 1633     }
 1634 
 1635     private function getDefaultEnv()
 1636     {
 1637         $env = [];
 1638 
 1639         foreach ($_SERVER as $k => $v) {
 1640             if (\is_string($v) && false !== $v = getenv($k)) {
 1641                 $env[$k] = $v;
 1642             }
 1643         }
 1644 
 1645         foreach ($_ENV as $k => $v) {
 1646             if (\is_string($v)) {
 1647                 $env[$k] = $v;
 1648             }
 1649         }
 1650 
 1651         return $env;
 1652     }
 1653 }