annotate vendor/symfony/process/Process.php @ 0:4c8ae668cc8c

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