PageRenderTime 50ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Symfony/Component/Process/Process.php

https://github.com/Exercise/symfony
PHP | 661 lines | 379 code | 86 blank | 196 comment | 47 complexity | e15ae36d5e0d2da6cc57e3e2e385dcf6 MD5 | raw file
  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. /**
  12. * Process is a thin wrapper around proc_* functions to ease
  13. * start independent PHP processes.
  14. *
  15. * @author Fabien Potencier <fabien@symfony.com>
  16. *
  17. * @api
  18. */
  19. class Process
  20. {
  21. const ERR = 'err';
  22. const OUT = 'out';
  23. const STATUS_READY = 'ready';
  24. const STATUS_STARTED = 'started';
  25. const STATUS_TERMINATED = 'terminated';
  26. const STDIN = 0;
  27. const STDOUT = 1;
  28. const STDERR = 2;
  29. private $commandline;
  30. private $cwd;
  31. private $env;
  32. private $stdin;
  33. private $timeout;
  34. private $options;
  35. private $exitcode;
  36. private $processInformation;
  37. private $stdout;
  38. private $stderr;
  39. private $enhanceWindowsCompatibility;
  40. private $pipes;
  41. private $process;
  42. private $status = self::STATUS_READY;
  43. /**
  44. * Exit codes translation table.
  45. *
  46. * User-defined errors must use exit codes in the 64-113 range.
  47. *
  48. * @var array
  49. */
  50. static public $exitCodes = array(
  51. 0 => 'OK',
  52. 1 => 'General error',
  53. 2 => 'Misuse of shell builtins',
  54. 126 => 'Invoked command cannot execute',
  55. 127 => 'Command not found',
  56. 128 => 'Invalid exit argument',
  57. // signals
  58. 129 => 'Hangup',
  59. 130 => 'Interrupt',
  60. 131 => 'Quit and dump core',
  61. 132 => 'Illegal instruction',
  62. 133 => 'Trace/breakpoint trap',
  63. 134 => 'Process aborted',
  64. 135 => 'Bus error: "access to undefined portion of memory object"',
  65. 136 => 'Floating point exception: "erroneous arithmetic operation"',
  66. 137 => 'Kill (terminate immediately)',
  67. 138 => 'User-defined 1',
  68. 139 => 'Segmentation violation',
  69. 140 => 'User-defined 2',
  70. 141 => 'Write to pipe with no one reading',
  71. 142 => 'Signal raised by alarm',
  72. 143 => 'Termination (request to terminate)',
  73. // 144 - not defined
  74. 145 => 'Child process terminated, stopped (or continued*)',
  75. 146 => 'Continue if stopped',
  76. 147 => 'Stop executing temporarily',
  77. 148 => 'Terminal stop signal',
  78. 149 => 'Background process attempting to read from tty ("in")',
  79. 150 => 'Background process attempting to write to tty ("out")',
  80. 151 => 'Urgent data available on socket',
  81. 152 => 'CPU time limit exceeded',
  82. 153 => 'File size limit exceeded',
  83. 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
  84. 155 => 'Profiling timer expired',
  85. // 156 - not defined
  86. 157 => 'Pollable event',
  87. // 158 - not defined
  88. 159 => 'Bad syscall',
  89. );
  90. /**
  91. * Constructor.
  92. *
  93. * @param string $commandline The command line to run
  94. * @param string $cwd The working directory
  95. * @param array $env The environment variables or null to inherit
  96. * @param string $stdin The STDIN content
  97. * @param integer $timeout The timeout in seconds
  98. * @param array $options An array of options for proc_open
  99. *
  100. * @throws \RuntimeException When proc_open is not installed
  101. *
  102. * @api
  103. */
  104. public function __construct($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array())
  105. {
  106. if (!function_exists('proc_open')) {
  107. throw new \RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.');
  108. }
  109. $this->commandline = $commandline;
  110. $this->cwd = null === $cwd ? getcwd() : $cwd;
  111. if (null !== $env) {
  112. $this->env = array();
  113. foreach ($env as $key => $value) {
  114. $this->env[(binary) $key] = (binary) $value;
  115. }
  116. } else {
  117. $this->env = null;
  118. }
  119. $this->stdin = $stdin;
  120. $this->timeout = $timeout;
  121. $this->enhanceWindowsCompatibility = true;
  122. $this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options);
  123. }
  124. /**
  125. * Runs the process.
  126. *
  127. * The callback receives the type of output (out or err) and
  128. * some bytes from the output in real-time. It allows to have feedback
  129. * from the independent process during execution.
  130. *
  131. * The STDOUT and STDERR are also available after the process is finished
  132. * via the getOutput() and getErrorOutput() methods.
  133. *
  134. * @param Closure|string|array $callback A PHP callback to run whenever there is some
  135. * output available on STDOUT or STDERR
  136. *
  137. * @return integer The exit status code
  138. *
  139. * @throws \RuntimeException When process can't be launch or is stopped
  140. *
  141. * @api
  142. */
  143. public function run($callback = null)
  144. {
  145. $this->start($callback);
  146. return $this->wait($callback);
  147. }
  148. /**
  149. * Starts the process and returns after sending the STDIN.
  150. *
  151. * This method blocks until all STDIN data is sent to the process then it
  152. * returns while the process runs in the background.
  153. *
  154. * The termination of the process can be awaited with wait().
  155. *
  156. * The callback receives the type of output (out or err) and some bytes from
  157. * the output in real-time while writing the standard input to the process.
  158. * It allows to have feedback from the independent process during execution.
  159. * If there is no callback passed, the wait() method can be called
  160. * with true as a second parameter then the callback will get all data occured
  161. * in (and since) the start call.
  162. *
  163. * @param Closure|string|array $callback A PHP callback to run whenever there is some
  164. * output available on STDOUT or STDERR
  165. *
  166. * @throws \RuntimeException When process can't be launch or is stopped
  167. * @throws \RuntimeException When process is already running
  168. */
  169. public function start($callback = null)
  170. {
  171. if ($this->isRunning()) {
  172. throw new \RuntimeException('Process is already running');
  173. }
  174. $this->stdout = '';
  175. $this->stderr = '';
  176. $callback = $this->buildCallback($callback);
  177. $descriptors = array(array('pipe', 'r'), array('pipe', 'w'), array('pipe', 'w'));
  178. $commandline = $this->commandline;
  179. if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->enhanceWindowsCompatibility) {
  180. $commandline = 'cmd /V:ON /E:ON /C "'.$commandline.'"';
  181. if (!isset($this->options['bypass_shell'])) {
  182. $this->options['bypass_shell'] = true;
  183. }
  184. }
  185. $this->process = proc_open($commandline, $descriptors, $this->pipes, $this->cwd, $this->env, $this->options);
  186. if (!is_resource($this->process)) {
  187. throw new \RuntimeException('Unable to launch a new process.');
  188. }
  189. $this->status = self::STATUS_STARTED;
  190. foreach ($this->pipes as $pipe) {
  191. stream_set_blocking($pipe, false);
  192. }
  193. if (null === $this->stdin) {
  194. fclose($this->pipes[0]);
  195. unset($this->pipes[0]);
  196. return;
  197. } else {
  198. $writePipes = array($this->pipes[0]);
  199. unset($this->pipes[0]);
  200. $stdinLen = strlen($this->stdin);
  201. $stdinOffset = 0;
  202. }
  203. while ($writePipes) {
  204. $r = $this->pipes;
  205. $w = $writePipes;
  206. $e = null;
  207. $n = @stream_select($r, $w, $e, $this->timeout);
  208. if (false === $n) {
  209. break;
  210. } elseif ($n === 0) {
  211. proc_terminate($this->process);
  212. throw new \RuntimeException('The process timed out.');
  213. }
  214. if ($w) {
  215. $written = fwrite($writePipes[0], (binary) substr($this->stdin, $stdinOffset), 8192);
  216. if (false !== $written) {
  217. $stdinOffset += $written;
  218. }
  219. if ($stdinOffset >= $stdinLen) {
  220. fclose($writePipes[0]);
  221. $writePipes = null;
  222. }
  223. }
  224. foreach ($r as $pipe) {
  225. $type = array_search($pipe, $this->pipes);
  226. $data = fread($pipe, 8192);
  227. if (strlen($data) > 0) {
  228. call_user_func($callback, $type == 1 ? self::OUT : self::ERR, $data);
  229. }
  230. if (false === $data || feof($pipe)) {
  231. fclose($pipe);
  232. unset($this->pipes[$type]);
  233. }
  234. }
  235. }
  236. $this->processInformation = proc_get_status($this->process);
  237. }
  238. /**
  239. * Waits for the process to terminate.
  240. *
  241. * The callback receives the type of output (out or err) and some bytes
  242. * from the output in real-time while writing the standard input to the process.
  243. * It allows to have feedback from the independent process during execution.
  244. *
  245. * @param mixed $callback A valid PHP callback
  246. *
  247. * @return int The exitcode of the process
  248. *
  249. * @throws \RuntimeException
  250. */
  251. public function wait($callback = null)
  252. {
  253. $this->processInformation = proc_get_status($this->process);
  254. $callback = $this->buildCallback($callback);
  255. while ($this->pipes) {
  256. $r = $this->pipes;
  257. $w = null;
  258. $e = null;
  259. $n = @stream_select($r, $w, $e, $this->timeout);
  260. if (false === $n) {
  261. break;
  262. }
  263. if (0 === $n) {
  264. proc_terminate($this->process);
  265. throw new \RuntimeException('The process timed out.');
  266. }
  267. foreach ($r as $pipe) {
  268. $type = array_search($pipe, $this->pipes);
  269. $data = fread($pipe, 8192);
  270. if (strlen($data) > 0) {
  271. call_user_func($callback, $type == 1 ? self::OUT : self::ERR, $data);
  272. }
  273. if (false === $data || feof($pipe)) {
  274. fclose($pipe);
  275. unset($this->pipes[$type]);
  276. }
  277. }
  278. }
  279. $this->updateStatus();
  280. if ($this->processInformation['signaled']) {
  281. throw new \RuntimeException(sprintf('The process stopped because of a "%s" signal.', $this->processInformation['stopsig']));
  282. }
  283. $time = 0;
  284. while ($this->isRunning() && $time < 1000000) {
  285. $time += 1000;
  286. usleep(1000);
  287. }
  288. $exitcode = proc_close($this->process);
  289. if ($this->processInformation['signaled']) {
  290. throw new \RuntimeException(sprintf('The process stopped because of a "%s" signal.', $this->processInformation['stopsig']));
  291. }
  292. return $this->exitcode = $this->processInformation['running'] ? $exitcode : $this->processInformation['exitcode'];
  293. }
  294. /**
  295. * Returns the current output of the process (STDOUT).
  296. *
  297. * @return string The process output
  298. *
  299. * @api
  300. */
  301. public function getOutput()
  302. {
  303. $this->updateOutput();
  304. return $this->stdout;
  305. }
  306. /**
  307. * Returns the current error output of the process (STDERR).
  308. *
  309. * @return string The process error output
  310. *
  311. * @api
  312. */
  313. public function getErrorOutput()
  314. {
  315. $this->updateErrorOutput();
  316. return $this->stderr;
  317. }
  318. /**
  319. * Returns the exit code returned by the process.
  320. *
  321. * @return integer The exit status code
  322. *
  323. * @api
  324. */
  325. public function getExitCode()
  326. {
  327. $this->updateStatus();
  328. return $this->exitcode;
  329. }
  330. /**
  331. * Returns a string representation for the exit code returned by the process.
  332. *
  333. * This method relies on the Unix exit code status standardization
  334. * and might not be relevant for other operating systems.
  335. *
  336. * @return string A string representation for the exit status code
  337. *
  338. * @see http://tldp.org/LDP/abs/html/exitcodes.html
  339. * @see http://en.wikipedia.org/wiki/Unix_signal
  340. */
  341. public function getExitCodeText()
  342. {
  343. $this->updateStatus();
  344. return isset(self::$exitCodes[$this->exitcode]) ? self::$exitCodes[$this->exitcode] : 'Unknown error';
  345. }
  346. /**
  347. * Checks if the process ended successfully.
  348. *
  349. * @return Boolean true if the process ended successfully, false otherwise
  350. *
  351. * @api
  352. */
  353. public function isSuccessful()
  354. {
  355. $this->updateStatus();
  356. return 0 == $this->exitcode;
  357. }
  358. /**
  359. * Returns true if the child process has been terminated by an uncaught signal.
  360. *
  361. * It always returns false on Windows.
  362. *
  363. * @return Boolean
  364. *
  365. * @api
  366. */
  367. public function hasBeenSignaled()
  368. {
  369. $this->updateStatus();
  370. return $this->processInformation['signaled'];
  371. }
  372. /**
  373. * Returns the number of the signal that caused the child process to terminate its execution.
  374. *
  375. * It is only meaningful if hasBeenSignaled() returns true.
  376. *
  377. * @return integer
  378. *
  379. * @api
  380. */
  381. public function getTermSignal()
  382. {
  383. $this->updateStatus();
  384. return $this->processInformation['termsig'];
  385. }
  386. /**
  387. * Returns true if the child process has been stopped by a signal.
  388. *
  389. * It always returns false on Windows.
  390. *
  391. * @return Boolean
  392. *
  393. * @api
  394. */
  395. public function hasBeenStopped()
  396. {
  397. $this->updateStatus();
  398. return $this->processInformation['stopped'];
  399. }
  400. /**
  401. * Returns the number of the signal that caused the child process to stop its execution
  402. *
  403. * It is only meaningful if hasBeenStopped() returns true.
  404. *
  405. * @return integer
  406. *
  407. * @api
  408. */
  409. public function getStopSignal()
  410. {
  411. $this->updateStatus();
  412. return $this->processInformation['stopsig'];
  413. }
  414. /**
  415. * Checks if the process is currently running.
  416. *
  417. * @return Boolean true if the process is currently running, false otherwise
  418. */
  419. public function isRunning()
  420. {
  421. if (self::STATUS_STARTED !== $this->status) {
  422. return false;
  423. }
  424. $this->updateStatus();
  425. return $this->processInformation['running'];
  426. }
  427. /**
  428. * Stops the process.
  429. *
  430. * @param float $timeout The timeout in seconds
  431. *
  432. * @return int The exitcode of the process
  433. *
  434. * @throws \RuntimeException if the process got signaled
  435. */
  436. public function stop($timeout=10)
  437. {
  438. $timeoutMicro = (int) $timeout*10E6;
  439. if ($this->isRunning()) {
  440. proc_terminate($this->process);
  441. $time = 0;
  442. while (1 == $this->isRunning() && $time < $timeoutMicro) {
  443. $time += 1000;
  444. usleep(1000);
  445. }
  446. foreach ($this->pipes as $pipe) {
  447. fclose($pipe);
  448. }
  449. $this->pipes = array();
  450. $exitcode = proc_close($this->process);
  451. $this->exitcode = -1 === $this->processInformation['exitcode'] ? $exitcode : $this->processInformation['exitcode'];
  452. }
  453. $this->status = self::STATUS_TERMINATED;
  454. return $this->exitcode;
  455. }
  456. public function addOutput($line)
  457. {
  458. $this->stdout .= $line;
  459. }
  460. public function addErrorOutput($line)
  461. {
  462. $this->stderr .= $line;
  463. }
  464. public function getCommandLine()
  465. {
  466. return $this->commandline;
  467. }
  468. public function setCommandLine($commandline)
  469. {
  470. $this->commandline = $commandline;
  471. }
  472. public function getTimeout()
  473. {
  474. return $this->timeout;
  475. }
  476. public function setTimeout($timeout)
  477. {
  478. $this->timeout = $timeout;
  479. }
  480. public function getWorkingDirectory()
  481. {
  482. return $this->cwd;
  483. }
  484. public function setWorkingDirectory($cwd)
  485. {
  486. $this->cwd = $cwd;
  487. }
  488. public function getEnv()
  489. {
  490. return $this->env;
  491. }
  492. public function setEnv(array $env)
  493. {
  494. $this->env = $env;
  495. }
  496. public function getStdin()
  497. {
  498. return $this->stdin;
  499. }
  500. public function setStdin($stdin)
  501. {
  502. $this->stdin = $stdin;
  503. }
  504. public function getOptions()
  505. {
  506. return $this->options;
  507. }
  508. public function setOptions(array $options)
  509. {
  510. $this->options = $options;
  511. }
  512. public function getEnhanceWindowsCompatibility()
  513. {
  514. return $this->enhanceWindowsCompatibility;
  515. }
  516. public function setEnhanceWindowsCompatibility($enhance)
  517. {
  518. $this->enhanceWindowsCompatibility = (Boolean) $enhance;
  519. }
  520. /**
  521. * Builds up the callback used by wait().
  522. *
  523. * The callbacks adds all occured output to the specific buffer and calls
  524. * the usercallback (if present) with the received output.
  525. *
  526. * @param mixed $callback The user defined PHP callback
  527. *
  528. * @return mixed A PHP callable
  529. */
  530. protected function buildCallback($callback)
  531. {
  532. $that = $this;
  533. $out = self::OUT;
  534. $err = self::ERR;
  535. $callback = function ($type, $data) use ($that, $callback, $out, $err) {
  536. if ($out == $type) {
  537. $that->addOutput($data);
  538. } else {
  539. $that->addErrorOutput($data);
  540. }
  541. if (null !== $callback) {
  542. call_user_func($callback, $type, $data);
  543. }
  544. };
  545. return $callback;
  546. }
  547. /**
  548. * Updates the status of the process.
  549. */
  550. protected function updateStatus()
  551. {
  552. if (self::STATUS_STARTED !== $this->status) {
  553. return;
  554. }
  555. $this->processInformation = proc_get_status($this->process);
  556. if (!$this->processInformation['running']) {
  557. $this->status = self::STATUS_TERMINATED;
  558. if (-1 !== $this->processInformation['exitcode']) {
  559. $this->exitcode = $this->processInformation['exitcode'];
  560. }
  561. }
  562. }
  563. protected function updateErrorOutput()
  564. {
  565. if (isset($this->pipes[self::STDERR]) && is_resource($this->pipes[self::STDERR])) {
  566. $this->addErrorOutput(stream_get_contents($this->pipes[self::STDERR]));
  567. }
  568. }
  569. protected function updateOutput()
  570. {
  571. if (isset($this->pipes[self::STDOUT]) && is_resource($this->pipes[self::STDOUT])) {
  572. $this->addOutput(stream_get_contents($this->pipes[self::STDOUT]));
  573. }
  574. }
  575. }