PageRenderTime 44ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/symfony/symfony/src/Symfony/Component/Process/Process.php

https://github.com/djae138/symfony
PHP | 1107 lines | 511 code | 143 blank | 453 comment | 69 complexity | cb48c7b8a94ac2d0127394ffcd9cd5f4 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Process;
  11. use Symfony\Component\Process\Exception\InvalidArgumentException;
  12. use Symfony\Component\Process\Exception\LogicException;
  13. use Symfony\Component\Process\Exception\RuntimeException;
  14. /**
  15. * Process is a thin wrapper around proc_* functions to ease
  16. * start independent PHP processes.
  17. *
  18. * @author Fabien Potencier <fabien@symfony.com>
  19. *
  20. * @api
  21. */
  22. class Process
  23. {
  24. const ERR = 'err';
  25. const OUT = 'out';
  26. const STATUS_READY = 'ready';
  27. const STATUS_STARTED = 'started';
  28. const STATUS_TERMINATED = 'terminated';
  29. const STDIN = 0;
  30. const STDOUT = 1;
  31. const STDERR = 2;
  32. // Timeout Precision in seconds.
  33. const TIMEOUT_PRECISION = 0.2;
  34. private $callback;
  35. private $commandline;
  36. private $cwd;
  37. private $env;
  38. private $stdin;
  39. private $starttime;
  40. private $timeout;
  41. private $options;
  42. private $exitcode;
  43. private $fallbackExitcode;
  44. private $processInformation;
  45. private $stdout;
  46. private $stderr;
  47. private $enhanceWindowsCompatibility;
  48. private $enhanceSigchildCompatibility;
  49. private $process;
  50. private $status = self::STATUS_READY;
  51. private $incrementalOutputOffset;
  52. private $incrementalErrorOutputOffset;
  53. private $tty;
  54. private $useFileHandles = false;
  55. /** @var ProcessPipes */
  56. private $processPipes;
  57. private static $sigchild;
  58. /**
  59. * Exit codes translation table.
  60. *
  61. * User-defined errors must use exit codes in the 64-113 range.
  62. *
  63. * @var array
  64. */
  65. public static $exitCodes = array(
  66. 0 => 'OK',
  67. 1 => 'General error',
  68. 2 => 'Misuse of shell builtins',
  69. 126 => 'Invoked command cannot execute',
  70. 127 => 'Command not found',
  71. 128 => 'Invalid exit argument',
  72. // signals
  73. 129 => 'Hangup',
  74. 130 => 'Interrupt',
  75. 131 => 'Quit and dump core',
  76. 132 => 'Illegal instruction',
  77. 133 => 'Trace/breakpoint trap',
  78. 134 => 'Process aborted',
  79. 135 => 'Bus error: "access to undefined portion of memory object"',
  80. 136 => 'Floating point exception: "erroneous arithmetic operation"',
  81. 137 => 'Kill (terminate immediately)',
  82. 138 => 'User-defined 1',
  83. 139 => 'Segmentation violation',
  84. 140 => 'User-defined 2',
  85. 141 => 'Write to pipe with no one reading',
  86. 142 => 'Signal raised by alarm',
  87. 143 => 'Termination (request to terminate)',
  88. // 144 - not defined
  89. 145 => 'Child process terminated, stopped (or continued*)',
  90. 146 => 'Continue if stopped',
  91. 147 => 'Stop executing temporarily',
  92. 148 => 'Terminal stop signal',
  93. 149 => 'Background process attempting to read from tty ("in")',
  94. 150 => 'Background process attempting to write to tty ("out")',
  95. 151 => 'Urgent data available on socket',
  96. 152 => 'CPU time limit exceeded',
  97. 153 => 'File size limit exceeded',
  98. 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
  99. 155 => 'Profiling timer expired',
  100. // 156 - not defined
  101. 157 => 'Pollable event',
  102. // 158 - not defined
  103. 159 => 'Bad syscall',
  104. );
  105. /**
  106. * Constructor.
  107. *
  108. * @param string $commandline The command line to run
  109. * @param string $cwd The working directory
  110. * @param array $env The environment variables or null to inherit
  111. * @param string $stdin The STDIN content
  112. * @param integer $timeout The timeout in seconds
  113. * @param array $options An array of options for proc_open
  114. *
  115. * @throws RuntimeException When proc_open is not installed
  116. *
  117. * @api
  118. */
  119. public function __construct($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array())
  120. {
  121. if (!function_exists('proc_open')) {
  122. throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.');
  123. }
  124. $this->commandline = $commandline;
  125. $this->cwd = $cwd;
  126. // on windows, if the cwd changed via chdir(), proc_open defaults to the dir where php was started
  127. // on gnu/linux, PHP builds with --enable-maintainer-zts are also affected
  128. // @see : https://bugs.php.net/bug.php?id=51800
  129. // @see : https://bugs.php.net/bug.php?id=50524
  130. if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || defined('PHP_WINDOWS_VERSION_BUILD'))) {
  131. $this->cwd = getcwd();
  132. }
  133. if (null !== $env) {
  134. $this->setEnv($env);
  135. } else {
  136. $this->env = null;
  137. }
  138. $this->stdin = $stdin;
  139. $this->setTimeout($timeout);
  140. $this->useFileHandles = defined('PHP_WINDOWS_VERSION_BUILD');
  141. $this->enhanceWindowsCompatibility = true;
  142. $this->enhanceSigchildCompatibility = !defined('PHP_WINDOWS_VERSION_BUILD') && $this->isSigchildEnabled();
  143. $this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options);
  144. }
  145. public function __destruct()
  146. {
  147. // stop() will check if we have a process running.
  148. $this->stop();
  149. }
  150. public function __clone()
  151. {
  152. $this->resetProcessData();
  153. }
  154. /**
  155. * Runs the process.
  156. *
  157. * The callback receives the type of output (out or err) and
  158. * some bytes from the output in real-time. It allows to have feedback
  159. * from the independent process during execution.
  160. *
  161. * The STDOUT and STDERR are also available after the process is finished
  162. * via the getOutput() and getErrorOutput() methods.
  163. *
  164. * @param callback|null $callback A PHP callback to run whenever there is some
  165. * output available on STDOUT or STDERR
  166. *
  167. * @return integer The exit status code
  168. *
  169. * @throws RuntimeException When process can't be launch or is stopped
  170. *
  171. * @api
  172. */
  173. public function run($callback = null)
  174. {
  175. $this->start($callback);
  176. return $this->wait();
  177. }
  178. /**
  179. * Starts the process and returns after sending the STDIN.
  180. *
  181. * This method blocks until all STDIN data is sent to the process then it
  182. * returns while the process runs in the background.
  183. *
  184. * The termination of the process can be awaited with wait().
  185. *
  186. * The callback receives the type of output (out or err) and some bytes from
  187. * the output in real-time while writing the standard input to the process.
  188. * It allows to have feedback from the independent process during execution.
  189. * If there is no callback passed, the wait() method can be called
  190. * with true as a second parameter then the callback will get all data occurred
  191. * in (and since) the start call.
  192. *
  193. * @param callback|null $callback A PHP callback to run whenever there is some
  194. * output available on STDOUT or STDERR
  195. *
  196. * @throws RuntimeException When process can't be launch or is stopped
  197. * @throws RuntimeException When process is already running
  198. */
  199. public function start($callback = null)
  200. {
  201. if ($this->isRunning()) {
  202. throw new RuntimeException('Process is already running');
  203. }
  204. $this->resetProcessData();
  205. $this->starttime = microtime(true);
  206. $this->callback = $this->buildCallback($callback);
  207. $descriptors = $this->getDescriptors();
  208. $commandline = $this->commandline;
  209. if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->enhanceWindowsCompatibility) {
  210. $commandline = 'cmd /V:ON /E:ON /C "'.$commandline.'"';
  211. if (!isset($this->options['bypass_shell'])) {
  212. $this->options['bypass_shell'] = true;
  213. }
  214. }
  215. $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options);
  216. if (!is_resource($this->process)) {
  217. throw new RuntimeException('Unable to launch a new process.');
  218. }
  219. $this->status = self::STATUS_STARTED;
  220. $this->processPipes->unblock();
  221. $this->processPipes->write(false, $this->stdin);
  222. $this->updateStatus(false);
  223. $this->checkTimeout();
  224. }
  225. /**
  226. * Restarts the process.
  227. *
  228. * Be warned that the process is cloned before being started.
  229. *
  230. * @param callable $callback A PHP callback to run whenever there is some
  231. * output available on STDOUT or STDERR
  232. *
  233. * @return Process The new process
  234. *
  235. * @throws RuntimeException When process can't be launch or is stopped
  236. * @throws RuntimeException When process is already running
  237. *
  238. * @see start()
  239. */
  240. public function restart($callback = null)
  241. {
  242. if ($this->isRunning()) {
  243. throw new RuntimeException('Process is already running');
  244. }
  245. $process = clone $this;
  246. $process->start($callback);
  247. return $process;
  248. }
  249. /**
  250. * Waits for the process to terminate.
  251. *
  252. * The callback receives the type of output (out or err) and some bytes
  253. * from the output in real-time while writing the standard input to the process.
  254. * It allows to have feedback from the independent process during execution.
  255. *
  256. * @param callback|null $callback A valid PHP callback
  257. *
  258. * @return integer The exitcode of the process
  259. *
  260. * @throws RuntimeException When process timed out
  261. * @throws RuntimeException When process stopped after receiving signal
  262. */
  263. public function wait($callback = null)
  264. {
  265. $this->updateStatus(false);
  266. if (null !== $callback) {
  267. $this->callback = $this->buildCallback($callback);
  268. }
  269. do {
  270. $this->checkTimeout();
  271. $running = defined('PHP_WINDOWS_VERSION_BUILD') ? $this->isRunning() : $this->processPipes->hasOpenHandles();
  272. $close = !defined('PHP_WINDOWS_VERSION_BUILD') || !$running;;
  273. $this->readPipes(true, $close);
  274. } while ($running);
  275. while ($this->isRunning()) {
  276. usleep(1000);
  277. }
  278. if ($this->processInformation['signaled']) {
  279. if ($this->isSigchildEnabled()) {
  280. throw new RuntimeException('The process has been signaled.');
  281. }
  282. throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig']));
  283. }
  284. return $this->exitcode;
  285. }
  286. /**
  287. * Returns the Pid (process identifier), if applicable.
  288. *
  289. * @return integer|null The process id if running, null otherwise
  290. *
  291. * @throws RuntimeException In case --enable-sigchild is activated
  292. */
  293. public function getPid()
  294. {
  295. if ($this->isSigchildEnabled()) {
  296. throw new RuntimeException('This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved.');
  297. }
  298. $this->updateStatus(false);
  299. return $this->isRunning() ? $this->processInformation['pid'] : null;
  300. }
  301. /**
  302. * Sends a posix signal to the process.
  303. *
  304. * @param integer $signal A valid posix signal (see http://www.php.net/manual/en/pcntl.constants.php)
  305. * @return Process
  306. *
  307. * @throws LogicException In case the process is not running
  308. * @throws RuntimeException In case --enable-sigchild is activated
  309. * @throws RuntimeException In case of failure
  310. */
  311. public function signal($signal)
  312. {
  313. if (!$this->isRunning()) {
  314. throw new LogicException('Can not send signal on a non running process.');
  315. }
  316. if ($this->isSigchildEnabled()) {
  317. throw new RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.');
  318. }
  319. if (true !== @proc_terminate($this->process, $signal)) {
  320. throw new RuntimeException(sprintf('Error while sending signal `%d`.', $signal));
  321. }
  322. return $this;
  323. }
  324. /**
  325. * Returns the current output of the process (STDOUT).
  326. *
  327. * @return string The process output
  328. *
  329. * @api
  330. */
  331. public function getOutput()
  332. {
  333. $this->readPipes(false, defined('PHP_WINDOWS_VERSION_BUILD') ? !$this->processInformation['running'] : true);
  334. return $this->stdout;
  335. }
  336. /**
  337. * Returns the output incrementally.
  338. *
  339. * In comparison with the getOutput method which always return the whole
  340. * output, this one returns the new output since the last call.
  341. *
  342. * @return string The process output since the last call
  343. */
  344. public function getIncrementalOutput()
  345. {
  346. $data = $this->getOutput();
  347. $latest = substr($data, $this->incrementalOutputOffset);
  348. $this->incrementalOutputOffset = strlen($data);
  349. return $latest;
  350. }
  351. /**
  352. * Returns the current error output of the process (STDERR).
  353. *
  354. * @return string The process error output
  355. *
  356. * @api
  357. */
  358. public function getErrorOutput()
  359. {
  360. $this->readPipes(false, defined('PHP_WINDOWS_VERSION_BUILD') ? !$this->processInformation['running'] : true);
  361. return $this->stderr;
  362. }
  363. /**
  364. * Returns the errorOutput incrementally.
  365. *
  366. * In comparison with the getErrorOutput method which always return the
  367. * whole error output, this one returns the new error output since the last
  368. * call.
  369. *
  370. * @return string The process error output since the last call
  371. */
  372. public function getIncrementalErrorOutput()
  373. {
  374. $data = $this->getErrorOutput();
  375. $latest = substr($data, $this->incrementalErrorOutputOffset);
  376. $this->incrementalErrorOutputOffset = strlen($data);
  377. return $latest;
  378. }
  379. /**
  380. * Returns the exit code returned by the process.
  381. *
  382. * @return integer The exit status code
  383. *
  384. * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled
  385. *
  386. * @api
  387. */
  388. public function getExitCode()
  389. {
  390. if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) {
  391. throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method');
  392. }
  393. $this->updateStatus(false);
  394. return $this->exitcode;
  395. }
  396. /**
  397. * Returns a string representation for the exit code returned by the process.
  398. *
  399. * This method relies on the Unix exit code status standardization
  400. * and might not be relevant for other operating systems.
  401. *
  402. * @return string A string representation for the exit status code
  403. *
  404. * @see http://tldp.org/LDP/abs/html/exitcodes.html
  405. * @see http://en.wikipedia.org/wiki/Unix_signal
  406. */
  407. public function getExitCodeText()
  408. {
  409. $exitcode = $this->getExitCode();
  410. return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error';
  411. }
  412. /**
  413. * Checks if the process ended successfully.
  414. *
  415. * @return Boolean true if the process ended successfully, false otherwise
  416. *
  417. * @api
  418. */
  419. public function isSuccessful()
  420. {
  421. return 0 === $this->getExitCode();
  422. }
  423. /**
  424. * Returns true if the child process has been terminated by an uncaught signal.
  425. *
  426. * It always returns false on Windows.
  427. *
  428. * @return Boolean
  429. *
  430. * @throws RuntimeException In case --enable-sigchild is activated
  431. *
  432. * @api
  433. */
  434. public function hasBeenSignaled()
  435. {
  436. if ($this->isSigchildEnabled()) {
  437. throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved');
  438. }
  439. $this->updateStatus(false);
  440. return $this->processInformation['signaled'];
  441. }
  442. /**
  443. * Returns the number of the signal that caused the child process to terminate its execution.
  444. *
  445. * It is only meaningful if hasBeenSignaled() returns true.
  446. *
  447. * @return integer
  448. *
  449. * @throws RuntimeException In case --enable-sigchild is activated
  450. *
  451. * @api
  452. */
  453. public function getTermSignal()
  454. {
  455. if ($this->isSigchildEnabled()) {
  456. throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved');
  457. }
  458. $this->updateStatus(false);
  459. return $this->processInformation['termsig'];
  460. }
  461. /**
  462. * Returns true if the child process has been stopped by a signal.
  463. *
  464. * It always returns false on Windows.
  465. *
  466. * @return Boolean
  467. *
  468. * @api
  469. */
  470. public function hasBeenStopped()
  471. {
  472. $this->updateStatus(false);
  473. return $this->processInformation['stopped'];
  474. }
  475. /**
  476. * Returns the number of the signal that caused the child process to stop its execution.
  477. *
  478. * It is only meaningful if hasBeenStopped() returns true.
  479. *
  480. * @return integer
  481. *
  482. * @api
  483. */
  484. public function getStopSignal()
  485. {
  486. $this->updateStatus(false);
  487. return $this->processInformation['stopsig'];
  488. }
  489. /**
  490. * Checks if the process is currently running.
  491. *
  492. * @return Boolean true if the process is currently running, false otherwise
  493. */
  494. public function isRunning()
  495. {
  496. if (self::STATUS_STARTED !== $this->status) {
  497. return false;
  498. }
  499. $this->updateStatus(false);
  500. return $this->processInformation['running'];
  501. }
  502. /**
  503. * Checks if the process has been started with no regard to the current state.
  504. *
  505. * @return Boolean true if status is ready, false otherwise
  506. */
  507. public function isStarted()
  508. {
  509. return $this->status != self::STATUS_READY;
  510. }
  511. /**
  512. * Checks if the process is terminated.
  513. *
  514. * @return Boolean true if process is terminated, false otherwise
  515. */
  516. public function isTerminated()
  517. {
  518. $this->updateStatus(false);
  519. return $this->status == self::STATUS_TERMINATED;
  520. }
  521. /**
  522. * Gets the process status.
  523. *
  524. * The status is one of: ready, started, terminated.
  525. *
  526. * @return string The current process status
  527. */
  528. public function getStatus()
  529. {
  530. $this->updateStatus(false);
  531. return $this->status;
  532. }
  533. /**
  534. * Stops the process.
  535. *
  536. * @param integer|float $timeout The timeout in seconds
  537. * @param integer $signal A posix signal to send in case the process has not stop at timeout, default is SIGKILL
  538. *
  539. * @return integer The exit-code of the process
  540. *
  541. * @throws RuntimeException if the process got signaled
  542. */
  543. public function stop($timeout = 10, $signal = null)
  544. {
  545. $timeoutMicro = microtime(true) + $timeout;
  546. if ($this->isRunning()) {
  547. proc_terminate($this->process);
  548. do {
  549. usleep(1000);
  550. } while ($this->isRunning() && microtime(true) < $timeoutMicro);
  551. if ($this->isRunning() && !$this->isSigchildEnabled()) {
  552. if (null !== $signal || defined('SIGKILL')) {
  553. $this->signal($signal ?: SIGKILL);
  554. }
  555. }
  556. }
  557. $this->updateStatus(false);
  558. if ($this->processInformation['running']) {
  559. $this->close();
  560. }
  561. $this->status = self::STATUS_TERMINATED;
  562. return $this->exitcode;
  563. }
  564. /**
  565. * Adds a line to the STDOUT stream.
  566. *
  567. * @param string $line The line to append
  568. */
  569. public function addOutput($line)
  570. {
  571. $this->stdout .= $line;
  572. }
  573. /**
  574. * Adds a line to the STDERR stream.
  575. *
  576. * @param string $line The line to append
  577. */
  578. public function addErrorOutput($line)
  579. {
  580. $this->stderr .= $line;
  581. }
  582. /**
  583. * Gets the command line to be executed.
  584. *
  585. * @return string The command to execute
  586. */
  587. public function getCommandLine()
  588. {
  589. return $this->commandline;
  590. }
  591. /**
  592. * Sets the command line to be executed.
  593. *
  594. * @param string $commandline The command to execute
  595. *
  596. * @return self The current Process instance
  597. */
  598. public function setCommandLine($commandline)
  599. {
  600. $this->commandline = $commandline;
  601. return $this;
  602. }
  603. /**
  604. * Gets the process timeout.
  605. *
  606. * @return integer|null The timeout in seconds or null if it's disabled
  607. */
  608. public function getTimeout()
  609. {
  610. return $this->timeout;
  611. }
  612. /**
  613. * Sets the process timeout.
  614. *
  615. * To disable the timeout, set this value to null.
  616. *
  617. * @param float|null $timeout The timeout in seconds
  618. *
  619. * @return self The current Process instance
  620. *
  621. * @throws InvalidArgumentException if the timeout is negative
  622. */
  623. public function setTimeout($timeout)
  624. {
  625. if (null === $timeout) {
  626. $this->timeout = null;
  627. return $this;
  628. }
  629. $timeout = (float) $timeout;
  630. if ($timeout < 0) {
  631. throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
  632. }
  633. $this->timeout = $timeout;
  634. return $this;
  635. }
  636. /**
  637. * Enables or disables the TTY mode.
  638. *
  639. * @param boolean $tty True to enabled and false to disable
  640. *
  641. * @return self The current Process instance
  642. */
  643. public function setTty($tty)
  644. {
  645. $this->tty = (Boolean) $tty;
  646. return $this;
  647. }
  648. /**
  649. * Checks if the TTY mode is enabled.
  650. *
  651. * @return Boolean true if the TTY mode is enabled, false otherwise
  652. */
  653. public function isTty()
  654. {
  655. return $this->tty;
  656. }
  657. /**
  658. * Gets the working directory.
  659. *
  660. * @return string The current working directory
  661. */
  662. public function getWorkingDirectory()
  663. {
  664. // This is for BC only
  665. if (null === $this->cwd) {
  666. // getcwd() will return false if any one of the parent directories does not have
  667. // the readable or search mode set, even if the current directory does
  668. return getcwd() ?: null;
  669. }
  670. return $this->cwd;
  671. }
  672. /**
  673. * Sets the current working directory.
  674. *
  675. * @param string $cwd The new working directory
  676. *
  677. * @return self The current Process instance
  678. */
  679. public function setWorkingDirectory($cwd)
  680. {
  681. $this->cwd = $cwd;
  682. return $this;
  683. }
  684. /**
  685. * Gets the environment variables.
  686. *
  687. * @return array The current environment variables
  688. */
  689. public function getEnv()
  690. {
  691. return $this->env;
  692. }
  693. /**
  694. * Sets the environment variables.
  695. *
  696. * An environment variable value should be a string.
  697. * If it is an array, the variable is ignored.
  698. *
  699. * That happens in PHP when 'argv' is registered into
  700. * the $_ENV array for instance.
  701. *
  702. * @param array $env The new environment variables
  703. *
  704. * @return self The current Process instance
  705. */
  706. public function setEnv(array $env)
  707. {
  708. // Process can not handle env values that are arrays
  709. $env = array_filter($env, function ($value) { if (!is_array($value)) { return true; } });
  710. $this->env = array();
  711. foreach ($env as $key => $value) {
  712. $this->env[(binary) $key] = (binary) $value;
  713. }
  714. return $this;
  715. }
  716. /**
  717. * Gets the contents of STDIN.
  718. *
  719. * @return string The current contents
  720. */
  721. public function getStdin()
  722. {
  723. return $this->stdin;
  724. }
  725. /**
  726. * Sets the contents of STDIN.
  727. *
  728. * @param string $stdin The new contents
  729. *
  730. * @return self The current Process instance
  731. */
  732. public function setStdin($stdin)
  733. {
  734. $this->stdin = $stdin;
  735. return $this;
  736. }
  737. /**
  738. * Gets the options for proc_open.
  739. *
  740. * @return array The current options
  741. */
  742. public function getOptions()
  743. {
  744. return $this->options;
  745. }
  746. /**
  747. * Sets the options for proc_open.
  748. *
  749. * @param array $options The new options
  750. *
  751. * @return self The current Process instance
  752. */
  753. public function setOptions(array $options)
  754. {
  755. $this->options = $options;
  756. return $this;
  757. }
  758. /**
  759. * Gets whether or not Windows compatibility is enabled.
  760. *
  761. * This is true by default.
  762. *
  763. * @return Boolean
  764. */
  765. public function getEnhanceWindowsCompatibility()
  766. {
  767. return $this->enhanceWindowsCompatibility;
  768. }
  769. /**
  770. * Sets whether or not Windows compatibility is enabled.
  771. *
  772. * @param Boolean $enhance
  773. *
  774. * @return self The current Process instance
  775. */
  776. public function setEnhanceWindowsCompatibility($enhance)
  777. {
  778. $this->enhanceWindowsCompatibility = (Boolean) $enhance;
  779. return $this;
  780. }
  781. /**
  782. * Returns whether sigchild compatibility mode is activated or not.
  783. *
  784. * @return Boolean
  785. */
  786. public function getEnhanceSigchildCompatibility()
  787. {
  788. return $this->enhanceSigchildCompatibility;
  789. }
  790. /**
  791. * Activates sigchild compatibility mode.
  792. *
  793. * Sigchild compatibility mode is required to get the exit code and
  794. * determine the success of a process when PHP has been compiled with
  795. * the --enable-sigchild option
  796. *
  797. * @param Boolean $enhance
  798. *
  799. * @return self The current Process instance
  800. */
  801. public function setEnhanceSigchildCompatibility($enhance)
  802. {
  803. $this->enhanceSigchildCompatibility = (Boolean) $enhance;
  804. return $this;
  805. }
  806. /**
  807. * Performs a check between the timeout definition and the time the process started.
  808. *
  809. * In case you run a background process (with the start method), you should
  810. * trigger this method regularly to ensure the process timeout
  811. *
  812. * @throws RuntimeException In case the timeout was reached
  813. */
  814. public function checkTimeout()
  815. {
  816. if (0 < $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
  817. $this->stop(0);
  818. throw new RuntimeException('The process timed-out.');
  819. }
  820. }
  821. /**
  822. * Creates the descriptors needed by the proc_open.
  823. *
  824. * @return array
  825. */
  826. private function getDescriptors()
  827. {
  828. $this->processPipes = new ProcessPipes($this->useFileHandles);
  829. $descriptors = $this->processPipes->getDescriptors();
  830. if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
  831. // last exit code is output on the fourth pipe and caught to work around --enable-sigchild
  832. $descriptors = array_merge($descriptors, array(array('pipe', 'w')));
  833. $this->commandline = '('.$this->commandline.') 3>/dev/null; code=$?; echo $code >&3; exit $code';
  834. }
  835. return $descriptors;
  836. }
  837. /**
  838. * Builds up the callback used by wait().
  839. *
  840. * The callbacks adds all occurred output to the specific buffer and calls
  841. * the user callback (if present) with the received output.
  842. *
  843. * @param callback|null $callback The user defined PHP callback
  844. *
  845. * @return callback A PHP callable
  846. */
  847. protected function buildCallback($callback)
  848. {
  849. $that = $this;
  850. $out = self::OUT;
  851. $err = self::ERR;
  852. $callback = function ($type, $data) use ($that, $callback, $out, $err) {
  853. if ($out == $type) {
  854. $that->addOutput($data);
  855. } else {
  856. $that->addErrorOutput($data);
  857. }
  858. if (null !== $callback) {
  859. call_user_func($callback, $type, $data);
  860. }
  861. };
  862. return $callback;
  863. }
  864. /**
  865. * Updates the status of the process, reads pipes.
  866. *
  867. * @param Boolean $blocking Whether to use a clocking read call.
  868. */
  869. protected function updateStatus($blocking)
  870. {
  871. if (self::STATUS_STARTED !== $this->status) {
  872. return;
  873. }
  874. $this->processInformation = proc_get_status($this->process);
  875. $this->captureExitCode();
  876. $this->readPipes($blocking, defined('PHP_WINDOWS_VERSION_BUILD') ? !$this->processInformation['running'] : true);
  877. if (!$this->processInformation['running']) {
  878. $this->close();
  879. $this->status = self::STATUS_TERMINATED;
  880. }
  881. }
  882. /**
  883. * Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
  884. *
  885. * @return Boolean
  886. */
  887. protected function isSigchildEnabled()
  888. {
  889. if (null !== self::$sigchild) {
  890. return self::$sigchild;
  891. }
  892. ob_start();
  893. phpinfo(INFO_GENERAL);
  894. return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
  895. }
  896. /**
  897. * Reads pipes, executes callback.
  898. *
  899. * @param Boolean $blocking Whether to use blocking calls or not.
  900. */
  901. private function readPipes($blocking, $close)
  902. {
  903. if ($close) {
  904. $result = $this->processPipes->readAndCloseHandles($blocking);
  905. } else {
  906. $result = $this->processPipes->read($blocking);
  907. }
  908. foreach ($result as $type => $data) {
  909. if (3 == $type) {
  910. $this->fallbackExitcode = (int) $data;
  911. } else {
  912. call_user_func($this->callback, $type === self::STDOUT ? self::OUT : self::ERR, $data);
  913. }
  914. }
  915. }
  916. /**
  917. * Captures the exitcode if mentioned in the process informations.
  918. */
  919. private function captureExitCode()
  920. {
  921. if (isset($this->processInformation['exitcode']) && -1 != $this->processInformation['exitcode']) {
  922. $this->exitcode = $this->processInformation['exitcode'];
  923. }
  924. }
  925. /**
  926. * Closes process resource, closes file handles, sets the exitcode.
  927. *
  928. * @return Integer The exitcode
  929. */
  930. private function close()
  931. {
  932. $exitcode = -1;
  933. $this->processPipes->close();
  934. if (is_resource($this->process)) {
  935. $exitcode = proc_close($this->process);
  936. }
  937. $this->exitcode = $this->exitcode !== null ? $this->exitcode : -1;
  938. $this->exitcode = -1 != $exitcode ? $exitcode : $this->exitcode;
  939. if (-1 == $this->exitcode && null !== $this->fallbackExitcode) {
  940. $this->exitcode = $this->fallbackExitcode;
  941. } elseif (-1 === $this->exitcode && $this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) {
  942. // if process has been signaled, no exitcode but a valid termsig, apply unix convention
  943. $this->exitcode = 128 + $this->processInformation['termsig'];
  944. }
  945. return $this->exitcode;
  946. }
  947. /**
  948. * Resets data related to the latest run of the process.
  949. */
  950. private function resetProcessData()
  951. {
  952. $this->starttime = null;
  953. $this->callback = null;
  954. $this->exitcode = null;
  955. $this->fallbackExitcode = null;
  956. $this->processInformation = null;
  957. $this->stdout = null;
  958. $this->stderr = null;
  959. $this->process = null;
  960. $this->status = self::STATUS_READY;
  961. $this->incrementalOutputOffset = 0;
  962. $this->incrementalErrorOutputOffset = 0;
  963. }
  964. }