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