vendor/symfony/process/Process.php line 147

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