composer package updates
[openemr.git] / vendor / symfony / process / Process.php
blob830c623e0dc3b79ff069c752f65313ecbc6bc03c
1 <?php
3 /*
4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Process;
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;
23 /**
24 * Process is a thin wrapper around proc_* functions to easily
25 * start independent PHP processes.
27 * @author Fabien Potencier <fabien@symfony.com>
28 * @author Romain Neutron <imprec@gmail.com>
30 class Process implements \IteratorAggregate
32 const ERR = 'err';
33 const OUT = 'out';
35 const STATUS_READY = 'ready';
36 const STATUS_STARTED = 'started';
37 const STATUS_TERMINATED = 'terminated';
39 const STDIN = 0;
40 const STDOUT = 1;
41 const STDERR = 2;
43 // Timeout Precision in seconds.
44 const TIMEOUT_PRECISION = 0.2;
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
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 = array('suppress_errors' => true);
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;
78 private $useFileHandles = false;
79 /** @var PipesInterface */
80 private $processPipes;
82 private $latestSignal;
84 private static $sigchild;
86 /**
87 * Exit codes translation table.
89 * User-defined errors must use exit codes in the 64-113 range.
91 public static $exitCodes = array(
92 0 => 'OK',
93 1 => 'General error',
94 2 => 'Misuse of shell builtins',
96 126 => 'Invoked command cannot execute',
97 127 => 'Command not found',
98 128 => 'Invalid exit argument',
100 // signals
101 129 => 'Hangup',
102 130 => 'Interrupt',
103 131 => 'Quit and dump core',
104 132 => 'Illegal instruction',
105 133 => 'Trace/breakpoint trap',
106 134 => 'Process aborted',
107 135 => 'Bus error: "access to undefined portion of memory object"',
108 136 => 'Floating point exception: "erroneous arithmetic operation"',
109 137 => 'Kill (terminate immediately)',
110 138 => 'User-defined 1',
111 139 => 'Segmentation violation',
112 140 => 'User-defined 2',
113 141 => 'Write to pipe with no one reading',
114 142 => 'Signal raised by alarm',
115 143 => 'Termination (request to terminate)',
116 // 144 - not defined
117 145 => 'Child process terminated, stopped (or continued*)',
118 146 => 'Continue if stopped',
119 147 => 'Stop executing temporarily',
120 148 => 'Terminal stop signal',
121 149 => 'Background process attempting to read from tty ("in")',
122 150 => 'Background process attempting to write to tty ("out")',
123 151 => 'Urgent data available on socket',
124 152 => 'CPU time limit exceeded',
125 153 => 'File size limit exceeded',
126 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
127 155 => 'Profiling timer expired',
128 // 156 - not defined
129 157 => 'Pollable event',
130 // 158 - not defined
131 159 => 'Bad syscall',
135 * @param string|array $commandline The command line to run
136 * @param string|null $cwd The working directory or null to use the working dir of the current PHP process
137 * @param array|null $env The environment variables or null to use the same environment as the current PHP process
138 * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input
139 * @param int|float|null $timeout The timeout in seconds or null to disable
140 * @param array $options An array of options for proc_open
142 * @throws RuntimeException When proc_open is not installed
144 public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = null)
146 if (!function_exists('proc_open')) {
147 throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.');
150 $this->commandline = $commandline;
151 $this->cwd = $cwd;
153 // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started
154 // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected
155 // @see : https://bugs.php.net/bug.php?id=51800
156 // @see : https://bugs.php.net/bug.php?id=50524
157 if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DIRECTORY_SEPARATOR)) {
158 $this->cwd = getcwd();
160 if (null !== $env) {
161 $this->setEnv($env);
164 $this->setInput($input);
165 $this->setTimeout($timeout);
166 $this->useFileHandles = '\\' === DIRECTORY_SEPARATOR;
167 $this->pty = false;
168 $this->enhanceSigchildCompatibility = '\\' !== DIRECTORY_SEPARATOR && $this->isSigchildEnabled();
169 if (null !== $options) {
170 @trigger_error(sprintf('The $options parameter of the %s constructor is deprecated since Symfony 3.3 and will be removed in 4.0.', __CLASS__), E_USER_DEPRECATED);
171 $this->options = array_replace($this->options, $options);
175 public function __destruct()
177 $this->stop(0);
180 public function __clone()
182 $this->resetProcessData();
186 * Runs the process.
188 * The callback receives the type of output (out or err) and
189 * some bytes from the output in real-time. It allows to have feedback
190 * from the independent process during execution.
192 * The STDOUT and STDERR are also available after the process is finished
193 * via the getOutput() and getErrorOutput() methods.
195 * @param callable|null $callback A PHP callback to run whenever there is some
196 * output available on STDOUT or STDERR
197 * @param array $env An array of additional env vars to set when running the process
199 * @return int The exit status code
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
205 * @final since version 3.3
207 public function run($callback = null/*, array $env = array()*/)
209 $env = 1 < func_num_args() ? func_get_arg(1) : null;
210 $this->start($callback, $env);
212 return $this->wait();
216 * Runs the process.
218 * This is identical to run() except that an exception is thrown if the process
219 * exits with a non-zero exit code.
221 * @param callable|null $callback
222 * @param array $env An array of additional env vars to set when running the process
224 * @return self
226 * @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled
227 * @throws ProcessFailedException if the process didn't terminate successfully
229 * @final since version 3.3
231 public function mustRun(callable $callback = null/*, array $env = array()*/)
233 if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
234 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
236 $env = 1 < func_num_args() ? func_get_arg(1) : null;
238 if (0 !== $this->run($callback, $env)) {
239 throw new ProcessFailedException($this);
242 return $this;
246 * Starts the process and returns after writing the input to STDIN.
248 * This method blocks until all STDIN data is sent to the process then it
249 * returns while the process runs in the background.
251 * The termination of the process can be awaited with wait().
253 * The callback receives the type of output (out or err) and some bytes from
254 * the output in real-time while writing the standard input to the process.
255 * It allows to have feedback from the independent process during execution.
257 * @param callable|null $callback A PHP callback to run whenever there is some
258 * output available on STDOUT or STDERR
259 * @param array $env An array of additional env vars to set when running the process
261 * @throws RuntimeException When process can't be launched
262 * @throws RuntimeException When process is already running
263 * @throws LogicException In case a callback is provided and output has been disabled
265 public function start(callable $callback = null/*, array $env = array()*/)
267 if ($this->isRunning()) {
268 throw new RuntimeException('Process is already running');
270 if (2 <= func_num_args()) {
271 $env = func_get_arg(1);
272 } else {
273 if (__CLASS__ !== static::class) {
274 $r = new \ReflectionMethod($this, __FUNCTION__);
275 if (__CLASS__ !== $r->getDeclaringClass()->getName() && (2 > $r->getNumberOfParameters() || 'env' !== $r->getParameters()[0]->name)) {
276 @trigger_error(sprintf('The %s::start() method expects a second "$env" argument since Symfony 3.3. It will be made mandatory in 4.0.', static::class), E_USER_DEPRECATED);
279 $env = null;
282 $this->resetProcessData();
283 $this->starttime = $this->lastOutputTime = microtime(true);
284 $this->callback = $this->buildCallback($callback);
285 $this->hasCallback = null !== $callback;
286 $descriptors = $this->getDescriptors();
287 $inheritEnv = $this->inheritEnv;
289 if (is_array($commandline = $this->commandline)) {
290 $commandline = implode(' ', array_map(array($this, 'escapeArgument'), $commandline));
292 if ('\\' !== DIRECTORY_SEPARATOR) {
293 // exec is mandatory to deal with sending a signal to the process
294 $commandline = 'exec '.$commandline;
298 if (null === $env) {
299 $env = $this->env;
300 } else {
301 if ($this->env) {
302 $env += $this->env;
304 $inheritEnv = true;
307 if (null !== $env && $inheritEnv) {
308 $env += $this->getDefaultEnv();
309 } elseif (null !== $env) {
310 @trigger_error('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', E_USER_DEPRECATED);
311 } else {
312 $env = $this->getDefaultEnv();
314 if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) {
315 $this->options['bypass_shell'] = true;
316 $commandline = $this->prepareWindowsCommandLine($commandline, $env);
317 } elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
318 // last exit code is output on the fourth pipe and caught to work around --enable-sigchild
319 $descriptors[3] = array('pipe', 'w');
321 // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input
322 $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;';
323 $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code';
325 // Workaround for the bug, when PTS functionality is enabled.
326 // @see : https://bugs.php.net/69442
327 $ptsWorkaround = fopen(__FILE__, 'r');
329 if (defined('HHVM_VERSION')) {
330 $envPairs = $env;
331 } else {
332 $envPairs = array();
333 foreach ($env as $k => $v) {
334 if (false !== $v) {
335 $envPairs[] = $k.'='.$v;
340 if (!is_dir($this->cwd)) {
341 @trigger_error('The provided cwd does not exist. Command is currently ran against getcwd(). This behavior is deprecated since Symfony 3.4 and will be removed in 4.0.', E_USER_DEPRECATED);
344 $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options);
346 if (!is_resource($this->process)) {
347 throw new RuntimeException('Unable to launch a new process.');
349 $this->status = self::STATUS_STARTED;
351 if (isset($descriptors[3])) {
352 $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]);
355 if ($this->tty) {
356 return;
359 $this->updateStatus(false);
360 $this->checkTimeout();
364 * Restarts the process.
366 * Be warned that the process is cloned before being started.
368 * @param callable|null $callback A PHP callback to run whenever there is some
369 * output available on STDOUT or STDERR
370 * @param array $env An array of additional env vars to set when running the process
372 * @return $this
374 * @throws RuntimeException When process can't be launched
375 * @throws RuntimeException When process is already running
377 * @see start()
379 * @final since version 3.3
381 public function restart(callable $callback = null/*, array $env = array()*/)
383 if ($this->isRunning()) {
384 throw new RuntimeException('Process is already running');
386 $env = 1 < func_num_args() ? func_get_arg(1) : null;
388 $process = clone $this;
389 $process->start($callback, $env);
391 return $process;
395 * Waits for the process to terminate.
397 * The callback receives the type of output (out or err) and some bytes
398 * from the output in real-time while writing the standard input to the process.
399 * It allows to have feedback from the independent process during execution.
401 * @param callable|null $callback A valid PHP callback
403 * @return int The exitcode of the process
405 * @throws RuntimeException When process timed out
406 * @throws RuntimeException When process stopped after receiving signal
407 * @throws LogicException When process is not yet started
409 public function wait(callable $callback = null)
411 $this->requireProcessIsStarted(__FUNCTION__);
413 $this->updateStatus(false);
415 if (null !== $callback) {
416 if (!$this->processPipes->haveReadSupport()) {
417 $this->stop(0);
418 throw new \LogicException('Pass the callback to the Process::start method or enableOutput to use a callback with Process::wait');
420 $this->callback = $this->buildCallback($callback);
423 do {
424 $this->checkTimeout();
425 $running = '\\' === DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
426 $this->readPipes($running, '\\' !== DIRECTORY_SEPARATOR || !$running);
427 } while ($running);
429 while ($this->isRunning()) {
430 usleep(1000);
433 if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
434 throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig']));
437 return $this->exitcode;
441 * Returns the Pid (process identifier), if applicable.
443 * @return int|null The process id if running, null otherwise
445 public function getPid()
447 return $this->isRunning() ? $this->processInformation['pid'] : null;
451 * Sends a POSIX signal to the process.
453 * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php)
455 * @return $this
457 * @throws LogicException In case the process is not running
458 * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
459 * @throws RuntimeException In case of failure
461 public function signal($signal)
463 $this->doSignal($signal, true);
465 return $this;
469 * Disables fetching output and error output from the underlying process.
471 * @return $this
473 * @throws RuntimeException In case the process is already running
474 * @throws LogicException if an idle timeout is set
476 public function disableOutput()
478 if ($this->isRunning()) {
479 throw new RuntimeException('Disabling output while the process is running is not possible.');
481 if (null !== $this->idleTimeout) {
482 throw new LogicException('Output can not be disabled while an idle timeout is set.');
485 $this->outputDisabled = true;
487 return $this;
491 * Enables fetching output and error output from the underlying process.
493 * @return $this
495 * @throws RuntimeException In case the process is already running
497 public function enableOutput()
499 if ($this->isRunning()) {
500 throw new RuntimeException('Enabling output while the process is running is not possible.');
503 $this->outputDisabled = false;
505 return $this;
509 * Returns true in case the output is disabled, false otherwise.
511 * @return bool
513 public function isOutputDisabled()
515 return $this->outputDisabled;
519 * Returns the current output of the process (STDOUT).
521 * @return string The process output
523 * @throws LogicException in case the output has been disabled
524 * @throws LogicException In case the process is not started
526 public function getOutput()
528 $this->readPipesForOutput(__FUNCTION__);
530 if (false === $ret = stream_get_contents($this->stdout, -1, 0)) {
531 return '';
534 return $ret;
538 * Returns the output incrementally.
540 * In comparison with the getOutput method which always return the whole
541 * output, this one returns the new output since the last call.
543 * @return string The process output since the last call
545 * @throws LogicException in case the output has been disabled
546 * @throws LogicException In case the process is not started
548 public function getIncrementalOutput()
550 $this->readPipesForOutput(__FUNCTION__);
552 $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
553 $this->incrementalOutputOffset = ftell($this->stdout);
555 if (false === $latest) {
556 return '';
559 return $latest;
563 * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR).
565 * @param int $flags A bit field of Process::ITER_* flags
567 * @throws LogicException in case the output has been disabled
568 * @throws LogicException In case the process is not started
570 * @return \Generator
572 public function getIterator($flags = 0)
574 $this->readPipesForOutput(__FUNCTION__, false);
576 $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags);
577 $blocking = !(self::ITER_NON_BLOCKING & $flags);
578 $yieldOut = !(self::ITER_SKIP_OUT & $flags);
579 $yieldErr = !(self::ITER_SKIP_ERR & $flags);
581 while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) {
582 if ($yieldOut) {
583 $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
585 if (isset($out[0])) {
586 if ($clearOutput) {
587 $this->clearOutput();
588 } else {
589 $this->incrementalOutputOffset = ftell($this->stdout);
592 yield self::OUT => $out;
596 if ($yieldErr) {
597 $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
599 if (isset($err[0])) {
600 if ($clearOutput) {
601 $this->clearErrorOutput();
602 } else {
603 $this->incrementalErrorOutputOffset = ftell($this->stderr);
606 yield self::ERR => $err;
610 if (!$blocking && !isset($out[0]) && !isset($err[0])) {
611 yield self::OUT => '';
614 $this->checkTimeout();
615 $this->readPipesForOutput(__FUNCTION__, $blocking);
620 * Clears the process output.
622 * @return $this
624 public function clearOutput()
626 ftruncate($this->stdout, 0);
627 fseek($this->stdout, 0);
628 $this->incrementalOutputOffset = 0;
630 return $this;
634 * Returns the current error output of the process (STDERR).
636 * @return string The process error output
638 * @throws LogicException in case the output has been disabled
639 * @throws LogicException In case the process is not started
641 public function getErrorOutput()
643 $this->readPipesForOutput(__FUNCTION__);
645 if (false === $ret = stream_get_contents($this->stderr, -1, 0)) {
646 return '';
649 return $ret;
653 * Returns the errorOutput incrementally.
655 * In comparison with the getErrorOutput method which always return the
656 * whole error output, this one returns the new error output since the last
657 * call.
659 * @return string The process error output since the last call
661 * @throws LogicException in case the output has been disabled
662 * @throws LogicException In case the process is not started
664 public function getIncrementalErrorOutput()
666 $this->readPipesForOutput(__FUNCTION__);
668 $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
669 $this->incrementalErrorOutputOffset = ftell($this->stderr);
671 if (false === $latest) {
672 return '';
675 return $latest;
679 * Clears the process output.
681 * @return $this
683 public function clearErrorOutput()
685 ftruncate($this->stderr, 0);
686 fseek($this->stderr, 0);
687 $this->incrementalErrorOutputOffset = 0;
689 return $this;
693 * Returns the exit code returned by the process.
695 * @return null|int The exit status code, null if the Process is not terminated
697 * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled
699 public function getExitCode()
701 if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
702 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
705 $this->updateStatus(false);
707 return $this->exitcode;
711 * Returns a string representation for the exit code returned by the process.
713 * This method relies on the Unix exit code status standardization
714 * and might not be relevant for other operating systems.
716 * @return null|string A string representation for the exit status code, null if the Process is not terminated
718 * @see http://tldp.org/LDP/abs/html/exitcodes.html
719 * @see http://en.wikipedia.org/wiki/Unix_signal
721 public function getExitCodeText()
723 if (null === $exitcode = $this->getExitCode()) {
724 return;
727 return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error';
731 * Checks if the process ended successfully.
733 * @return bool true if the process ended successfully, false otherwise
735 public function isSuccessful()
737 return 0 === $this->getExitCode();
741 * Returns true if the child process has been terminated by an uncaught signal.
743 * It always returns false on Windows.
745 * @return bool
747 * @throws RuntimeException In case --enable-sigchild is activated
748 * @throws LogicException In case the process is not terminated
750 public function hasBeenSignaled()
752 $this->requireProcessIsTerminated(__FUNCTION__);
754 if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
755 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
758 return $this->processInformation['signaled'];
762 * Returns the number of the signal that caused the child process to terminate its execution.
764 * It is only meaningful if hasBeenSignaled() returns true.
766 * @return int
768 * @throws RuntimeException In case --enable-sigchild is activated
769 * @throws LogicException In case the process is not terminated
771 public function getTermSignal()
773 $this->requireProcessIsTerminated(__FUNCTION__);
775 if ($this->isSigchildEnabled() && (!$this->enhanceSigchildCompatibility || -1 === $this->processInformation['termsig'])) {
776 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
779 return $this->processInformation['termsig'];
783 * Returns true if the child process has been stopped by a signal.
785 * It always returns false on Windows.
787 * @return bool
789 * @throws LogicException In case the process is not terminated
791 public function hasBeenStopped()
793 $this->requireProcessIsTerminated(__FUNCTION__);
795 return $this->processInformation['stopped'];
799 * Returns the number of the signal that caused the child process to stop its execution.
801 * It is only meaningful if hasBeenStopped() returns true.
803 * @return int
805 * @throws LogicException In case the process is not terminated
807 public function getStopSignal()
809 $this->requireProcessIsTerminated(__FUNCTION__);
811 return $this->processInformation['stopsig'];
815 * Checks if the process is currently running.
817 * @return bool true if the process is currently running, false otherwise
819 public function isRunning()
821 if (self::STATUS_STARTED !== $this->status) {
822 return false;
825 $this->updateStatus(false);
827 return $this->processInformation['running'];
831 * Checks if the process has been started with no regard to the current state.
833 * @return bool true if status is ready, false otherwise
835 public function isStarted()
837 return self::STATUS_READY != $this->status;
841 * Checks if the process is terminated.
843 * @return bool true if process is terminated, false otherwise
845 public function isTerminated()
847 $this->updateStatus(false);
849 return self::STATUS_TERMINATED == $this->status;
853 * Gets the process status.
855 * The status is one of: ready, started, terminated.
857 * @return string The current process status
859 public function getStatus()
861 $this->updateStatus(false);
863 return $this->status;
867 * Stops the process.
869 * @param int|float $timeout The timeout in seconds
870 * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
872 * @return int The exit-code of the process
874 public function stop($timeout = 10, $signal = null)
876 $timeoutMicro = microtime(true) + $timeout;
877 if ($this->isRunning()) {
878 // given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here
879 $this->doSignal(15, false);
880 do {
881 usleep(1000);
882 } while ($this->isRunning() && microtime(true) < $timeoutMicro);
884 if ($this->isRunning()) {
885 // Avoid exception here: process is supposed to be running, but it might have stopped just
886 // after this line. In any case, let's silently discard the error, we cannot do anything.
887 $this->doSignal($signal ?: 9, false);
891 if ($this->isRunning()) {
892 if (isset($this->fallbackStatus['pid'])) {
893 unset($this->fallbackStatus['pid']);
895 return $this->stop(0, $signal);
897 $this->close();
900 return $this->exitcode;
904 * Adds a line to the STDOUT stream.
906 * @internal
908 * @param string $line The line to append
910 public function addOutput($line)
912 $this->lastOutputTime = microtime(true);
914 fseek($this->stdout, 0, SEEK_END);
915 fwrite($this->stdout, $line);
916 fseek($this->stdout, $this->incrementalOutputOffset);
920 * Adds a line to the STDERR stream.
922 * @internal
924 * @param string $line The line to append
926 public function addErrorOutput($line)
928 $this->lastOutputTime = microtime(true);
930 fseek($this->stderr, 0, SEEK_END);
931 fwrite($this->stderr, $line);
932 fseek($this->stderr, $this->incrementalErrorOutputOffset);
936 * Gets the command line to be executed.
938 * @return string The command to execute
940 public function getCommandLine()
942 return is_array($this->commandline) ? implode(' ', array_map(array($this, 'escapeArgument'), $this->commandline)) : $this->commandline;
946 * Sets the command line to be executed.
948 * @param string|array $commandline The command to execute
950 * @return self The current Process instance
952 public function setCommandLine($commandline)
954 $this->commandline = $commandline;
956 return $this;
960 * Gets the process timeout (max. runtime).
962 * @return float|null The timeout in seconds or null if it's disabled
964 public function getTimeout()
966 return $this->timeout;
970 * Gets the process idle timeout (max. time since last output).
972 * @return float|null The timeout in seconds or null if it's disabled
974 public function getIdleTimeout()
976 return $this->idleTimeout;
980 * Sets the process timeout (max. runtime).
982 * To disable the timeout, set this value to null.
984 * @param int|float|null $timeout The timeout in seconds
986 * @return self The current Process instance
988 * @throws InvalidArgumentException if the timeout is negative
990 public function setTimeout($timeout)
992 $this->timeout = $this->validateTimeout($timeout);
994 return $this;
998 * Sets the process idle timeout (max. time since last output).
1000 * To disable the timeout, set this value to null.
1002 * @param int|float|null $timeout The timeout in seconds
1004 * @return self The current Process instance
1006 * @throws LogicException if the output is disabled
1007 * @throws InvalidArgumentException if the timeout is negative
1009 public function setIdleTimeout($timeout)
1011 if (null !== $timeout && $this->outputDisabled) {
1012 throw new LogicException('Idle timeout can not be set while the output is disabled.');
1015 $this->idleTimeout = $this->validateTimeout($timeout);
1017 return $this;
1021 * Enables or disables the TTY mode.
1023 * @param bool $tty True to enabled and false to disable
1025 * @return self The current Process instance
1027 * @throws RuntimeException In case the TTY mode is not supported
1029 public function setTty($tty)
1031 if ('\\' === DIRECTORY_SEPARATOR && $tty) {
1032 throw new RuntimeException('TTY mode is not supported on Windows platform.');
1034 if ($tty) {
1035 static $isTtySupported;
1037 if (null === $isTtySupported) {
1038 $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);
1041 if (!$isTtySupported) {
1042 throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.');
1046 $this->tty = (bool) $tty;
1048 return $this;
1052 * Checks if the TTY mode is enabled.
1054 * @return bool true if the TTY mode is enabled, false otherwise
1056 public function isTty()
1058 return $this->tty;
1062 * Sets PTY mode.
1064 * @param bool $bool
1066 * @return self
1068 public function setPty($bool)
1070 $this->pty = (bool) $bool;
1072 return $this;
1076 * Returns PTY state.
1078 * @return bool
1080 public function isPty()
1082 return $this->pty;
1086 * Gets the working directory.
1088 * @return string|null The current working directory or null on failure
1090 public function getWorkingDirectory()
1092 if (null === $this->cwd) {
1093 // getcwd() will return false if any one of the parent directories does not have
1094 // the readable or search mode set, even if the current directory does
1095 return getcwd() ?: null;
1098 return $this->cwd;
1102 * Sets the current working directory.
1104 * @param string $cwd The new working directory
1106 * @return self The current Process instance
1108 public function setWorkingDirectory($cwd)
1110 $this->cwd = $cwd;
1112 return $this;
1116 * Gets the environment variables.
1118 * @return array The current environment variables
1120 public function getEnv()
1122 return $this->env;
1126 * Sets the environment variables.
1128 * Each environment variable value should be a string.
1129 * If it is an array, the variable is ignored.
1130 * If it is false or null, it will be removed when
1131 * env vars are otherwise inherited.
1133 * That happens in PHP when 'argv' is registered into
1134 * the $_ENV array for instance.
1136 * @param array $env The new environment variables
1138 * @return self The current Process instance
1140 public function setEnv(array $env)
1142 // Process can not handle env values that are arrays
1143 $env = array_filter($env, function ($value) {
1144 return !is_array($value);
1147 $this->env = $env;
1149 return $this;
1153 * Gets the Process input.
1155 * @return resource|string|\Iterator|null The Process input
1157 public function getInput()
1159 return $this->input;
1163 * Sets the input.
1165 * This content will be passed to the underlying process standard input.
1167 * @param string|int|float|bool|resource|\Traversable|null $input The content
1169 * @return self The current Process instance
1171 * @throws LogicException In case the process is running
1173 public function setInput($input)
1175 if ($this->isRunning()) {
1176 throw new LogicException('Input can not be set while the process is running.');
1179 $this->input = ProcessUtils::validateInput(__METHOD__, $input);
1181 return $this;
1185 * Gets the options for proc_open.
1187 * @return array The current options
1189 * @deprecated since version 3.3, to be removed in 4.0.
1191 public function getOptions()
1193 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED);
1195 return $this->options;
1199 * Sets the options for proc_open.
1201 * @param array $options The new options
1203 * @return self The current Process instance
1205 * @deprecated since version 3.3, to be removed in 4.0.
1207 public function setOptions(array $options)
1209 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED);
1211 $this->options = $options;
1213 return $this;
1217 * Gets whether or not Windows compatibility is enabled.
1219 * This is true by default.
1221 * @return bool
1223 * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled.
1225 public function getEnhanceWindowsCompatibility()
1227 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED);
1229 return $this->enhanceWindowsCompatibility;
1233 * Sets whether or not Windows compatibility is enabled.
1235 * @param bool $enhance
1237 * @return self The current Process instance
1239 * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled.
1241 public function setEnhanceWindowsCompatibility($enhance)
1243 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED);
1245 $this->enhanceWindowsCompatibility = (bool) $enhance;
1247 return $this;
1251 * Returns whether sigchild compatibility mode is activated or not.
1253 * @return bool
1255 * @deprecated since version 3.3, to be removed in 4.0. Sigchild compatibility will always be enabled.
1257 public function getEnhanceSigchildCompatibility()
1259 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED);
1261 return $this->enhanceSigchildCompatibility;
1265 * Activates sigchild compatibility mode.
1267 * Sigchild compatibility mode is required to get the exit code and
1268 * determine the success of a process when PHP has been compiled with
1269 * the --enable-sigchild option
1271 * @param bool $enhance
1273 * @return self The current Process instance
1275 * @deprecated since version 3.3, to be removed in 4.0.
1277 public function setEnhanceSigchildCompatibility($enhance)
1279 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED);
1281 $this->enhanceSigchildCompatibility = (bool) $enhance;
1283 return $this;
1287 * Sets whether environment variables will be inherited or not.
1289 * @param bool $inheritEnv
1291 * @return self The current Process instance
1293 public function inheritEnvironmentVariables($inheritEnv = true)
1295 if (!$inheritEnv) {
1296 @trigger_error('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', E_USER_DEPRECATED);
1299 $this->inheritEnv = (bool) $inheritEnv;
1301 return $this;
1305 * Returns whether environment variables will be inherited or not.
1307 * @return bool
1309 * @deprecated since version 3.3, to be removed in 4.0. Environment variables will always be inherited.
1311 public function areEnvironmentVariablesInherited()
1313 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Environment variables will always be inherited.', __METHOD__), E_USER_DEPRECATED);
1315 return $this->inheritEnv;
1319 * Performs a check between the timeout definition and the time the process started.
1321 * In case you run a background process (with the start method), you should
1322 * trigger this method regularly to ensure the process timeout
1324 * @throws ProcessTimedOutException In case the timeout was reached
1326 public function checkTimeout()
1328 if (self::STATUS_STARTED !== $this->status) {
1329 return;
1332 if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
1333 $this->stop(0);
1335 throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL);
1338 if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
1339 $this->stop(0);
1341 throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE);
1346 * Returns whether PTY is supported on the current operating system.
1348 * @return bool
1350 public static function isPtySupported()
1352 static $result;
1354 if (null !== $result) {
1355 return $result;
1358 if ('\\' === DIRECTORY_SEPARATOR) {
1359 return $result = false;
1362 return $result = (bool) @proc_open('echo 1 >/dev/null', array(array('pty'), array('pty'), array('pty')), $pipes);
1366 * Creates the descriptors needed by the proc_open.
1368 * @return array
1370 private function getDescriptors()
1372 if ($this->input instanceof \Iterator) {
1373 $this->input->rewind();
1375 if ('\\' === DIRECTORY_SEPARATOR) {
1376 $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback);
1377 } else {
1378 $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback);
1381 return $this->processPipes->getDescriptors();
1385 * Builds up the callback used by wait().
1387 * The callbacks adds all occurred output to the specific buffer and calls
1388 * the user callback (if present) with the received output.
1390 * @param callable|null $callback The user defined PHP callback
1392 * @return \Closure A PHP closure
1394 protected function buildCallback(callable $callback = null)
1396 if ($this->outputDisabled) {
1397 return function ($type, $data) use ($callback) {
1398 if (null !== $callback) {
1399 call_user_func($callback, $type, $data);
1404 $out = self::OUT;
1406 return function ($type, $data) use ($callback, $out) {
1407 if ($out == $type) {
1408 $this->addOutput($data);
1409 } else {
1410 $this->addErrorOutput($data);
1413 if (null !== $callback) {
1414 call_user_func($callback, $type, $data);
1420 * Updates the status of the process, reads pipes.
1422 * @param bool $blocking Whether to use a blocking read call
1424 protected function updateStatus($blocking)
1426 if (self::STATUS_STARTED !== $this->status) {
1427 return;
1430 $this->processInformation = proc_get_status($this->process);
1431 $running = $this->processInformation['running'];
1433 $this->readPipes($running && $blocking, '\\' !== DIRECTORY_SEPARATOR || !$running);
1435 if ($this->fallbackStatus && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
1436 $this->processInformation = $this->fallbackStatus + $this->processInformation;
1439 if (!$running) {
1440 $this->close();
1445 * Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
1447 * @return bool
1449 protected function isSigchildEnabled()
1451 if (null !== self::$sigchild) {
1452 return self::$sigchild;
1455 if (!function_exists('phpinfo') || defined('HHVM_VERSION')) {
1456 return self::$sigchild = false;
1459 ob_start();
1460 phpinfo(INFO_GENERAL);
1462 return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
1466 * Reads pipes for the freshest output.
1468 * @param string $caller The name of the method that needs fresh outputs
1469 * @param bool $blocking Whether to use blocking calls or not
1471 * @throws LogicException in case output has been disabled or process is not started
1473 private function readPipesForOutput($caller, $blocking = false)
1475 if ($this->outputDisabled) {
1476 throw new LogicException('Output has been disabled.');
1479 $this->requireProcessIsStarted($caller);
1481 $this->updateStatus($blocking);
1485 * Validates and returns the filtered timeout.
1487 * @param int|float|null $timeout
1489 * @return float|null
1491 * @throws InvalidArgumentException if the given timeout is a negative number
1493 private function validateTimeout($timeout)
1495 $timeout = (float) $timeout;
1497 if (0.0 === $timeout) {
1498 $timeout = null;
1499 } elseif ($timeout < 0) {
1500 throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
1503 return $timeout;
1507 * Reads pipes, executes callback.
1509 * @param bool $blocking Whether to use blocking calls or not
1510 * @param bool $close Whether to close file handles or not
1512 private function readPipes($blocking, $close)
1514 $result = $this->processPipes->readAndWrite($blocking, $close);
1516 $callback = $this->callback;
1517 foreach ($result as $type => $data) {
1518 if (3 !== $type) {
1519 $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data);
1520 } elseif (!isset($this->fallbackStatus['signaled'])) {
1521 $this->fallbackStatus['exitcode'] = (int) $data;
1527 * Closes process resource, closes file handles, sets the exitcode.
1529 * @return int The exitcode
1531 private function close()
1533 $this->processPipes->close();
1534 if (is_resource($this->process)) {
1535 proc_close($this->process);
1537 $this->exitcode = $this->processInformation['exitcode'];
1538 $this->status = self::STATUS_TERMINATED;
1540 if (-1 === $this->exitcode) {
1541 if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) {
1542 // if process has been signaled, no exitcode but a valid termsig, apply Unix convention
1543 $this->exitcode = 128 + $this->processInformation['termsig'];
1544 } elseif ($this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
1545 $this->processInformation['signaled'] = true;
1546 $this->processInformation['termsig'] = -1;
1550 // Free memory from self-reference callback created by buildCallback
1551 // Doing so in other contexts like __destruct or by garbage collector is ineffective
1552 // Now pipes are closed, so the callback is no longer necessary
1553 $this->callback = null;
1555 return $this->exitcode;
1559 * Resets data related to the latest run of the process.
1561 private function resetProcessData()
1563 $this->starttime = null;
1564 $this->callback = null;
1565 $this->exitcode = null;
1566 $this->fallbackStatus = array();
1567 $this->processInformation = null;
1568 $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'wb+');
1569 $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'wb+');
1570 $this->process = null;
1571 $this->latestSignal = null;
1572 $this->status = self::STATUS_READY;
1573 $this->incrementalOutputOffset = 0;
1574 $this->incrementalErrorOutputOffset = 0;
1578 * Sends a POSIX signal to the process.
1580 * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php)
1581 * @param bool $throwException Whether to throw exception in case signal failed
1583 * @return bool True if the signal was sent successfully, false otherwise
1585 * @throws LogicException In case the process is not running
1586 * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
1587 * @throws RuntimeException In case of failure
1589 private function doSignal($signal, $throwException)
1591 if (null === $pid = $this->getPid()) {
1592 if ($throwException) {
1593 throw new LogicException('Can not send signal on a non running process.');
1596 return false;
1599 if ('\\' === DIRECTORY_SEPARATOR) {
1600 exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode);
1601 if ($exitCode && $this->isRunning()) {
1602 if ($throwException) {
1603 throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output)));
1606 return false;
1608 } else {
1609 if (!$this->enhanceSigchildCompatibility || !$this->isSigchildEnabled()) {
1610 $ok = @proc_terminate($this->process, $signal);
1611 } elseif (function_exists('posix_kill')) {
1612 $ok = @posix_kill($pid, $signal);
1613 } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), array(2 => array('pipe', 'w')), $pipes)) {
1614 $ok = false === fgets($pipes[2]);
1616 if (!$ok) {
1617 if ($throwException) {
1618 throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal));
1621 return false;
1625 $this->latestSignal = (int) $signal;
1626 $this->fallbackStatus['signaled'] = true;
1627 $this->fallbackStatus['exitcode'] = -1;
1628 $this->fallbackStatus['termsig'] = $this->latestSignal;
1630 return true;
1633 private function prepareWindowsCommandLine($cmd, array &$env)
1635 $uid = uniqid('', true);
1636 $varCount = 0;
1637 $varCache = array();
1638 $cmd = preg_replace_callback(
1639 '/"(?:(
1640 [^"%!^]*+
1642 (?: !LF! | "(?:\^[%!^])?+" )
1643 [^"%!^]*+
1645 ) | [^"]*+ )"/x',
1646 function ($m) use (&$env, &$varCache, &$varCount, $uid) {
1647 if (!isset($m[1])) {
1648 return $m[0];
1650 if (isset($varCache[$m[0]])) {
1651 return $varCache[$m[0]];
1653 if (false !== strpos($value = $m[1], "\0")) {
1654 $value = str_replace("\0", '?', $value);
1656 if (false === strpbrk($value, "\"%!\n")) {
1657 return '"'.$value.'"';
1660 $value = str_replace(array('!LF!', '"^!"', '"^%"', '"^^"', '""'), array("\n", '!', '%', '^', '"'), $value);
1661 $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"';
1662 $var = $uid.++$varCount;
1664 $env[$var] = $value;
1666 return $varCache[$m[0]] = '!'.$var.'!';
1668 $cmd
1671 $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')';
1672 foreach ($this->processPipes->getFiles() as $offset => $filename) {
1673 $cmd .= ' '.$offset.'>"'.$filename.'"';
1676 return $cmd;
1680 * Ensures the process is running or terminated, throws a LogicException if the process has a not started.
1682 * @param string $functionName The function name that was called
1684 * @throws LogicException if the process has not run
1686 private function requireProcessIsStarted($functionName)
1688 if (!$this->isStarted()) {
1689 throw new LogicException(sprintf('Process must be started before calling %s.', $functionName));
1694 * Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`.
1696 * @param string $functionName The function name that was called
1698 * @throws LogicException if the process is not yet terminated
1700 private function requireProcessIsTerminated($functionName)
1702 if (!$this->isTerminated()) {
1703 throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName));
1708 * Escapes a string to be used as a shell argument.
1710 * @param string $argument The argument that will be escaped
1712 * @return string The escaped argument
1714 private function escapeArgument($argument)
1716 if ('\\' !== DIRECTORY_SEPARATOR) {
1717 return "'".str_replace("'", "'\\''", $argument)."'";
1719 if ('' === $argument = (string) $argument) {
1720 return '""';
1722 if (false !== strpos($argument, "\0")) {
1723 $argument = str_replace("\0", '?', $argument);
1725 if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) {
1726 return $argument;
1728 $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument);
1730 return '"'.str_replace(array('"', '^', '%', '!', "\n"), array('""', '"^^"', '"^%"', '"^!"', '!LF!'), $argument).'"';
1733 private function getDefaultEnv()
1735 $env = array();
1737 foreach ($_SERVER as $k => $v) {
1738 if (is_string($v) && false !== $v = getenv($k)) {
1739 $env[$k] = $v;
1743 foreach ($_ENV as $k => $v) {
1744 if (is_string($v)) {
1745 $env[$k] = $v;
1749 return $env;