annotate vendor/symfony/process/Process.php @ 19:fa3358dc1485 tip

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