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