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