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