Process.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. <?php
  2. namespace React\ChildProcess;
  3. use Evenement\EventEmitter;
  4. use React\EventLoop\Loop;
  5. use React\EventLoop\LoopInterface;
  6. use React\Stream\ReadableResourceStream;
  7. use React\Stream\ReadableStreamInterface;
  8. use React\Stream\WritableResourceStream;
  9. use React\Stream\WritableStreamInterface;
  10. use React\Stream\DuplexResourceStream;
  11. use React\Stream\DuplexStreamInterface;
  12. /**
  13. * Process component.
  14. *
  15. * This class borrows logic from Symfony's Process component for ensuring
  16. * compatibility when PHP is compiled with the --enable-sigchild option.
  17. *
  18. * This class also implements the `EventEmitterInterface`
  19. * which allows you to react to certain events:
  20. *
  21. * exit event:
  22. * The `exit` event will be emitted whenever the process is no longer running.
  23. * Event listeners will receive the exit code and termination signal as two
  24. * arguments:
  25. *
  26. * ```php
  27. * $process = new Process('sleep 10');
  28. * $process->start();
  29. *
  30. * $process->on('exit', function ($code, $term) {
  31. * if ($term === null) {
  32. * echo 'exit with code ' . $code . PHP_EOL;
  33. * } else {
  34. * echo 'terminated with signal ' . $term . PHP_EOL;
  35. * }
  36. * });
  37. * ```
  38. *
  39. * Note that `$code` is `null` if the process has terminated, but the exit
  40. * code could not be determined (for example
  41. * [sigchild compatibility](#sigchild-compatibility) was disabled).
  42. * Similarly, `$term` is `null` unless the process has terminated in response to
  43. * an uncaught signal sent to it.
  44. * This is not a limitation of this project, but actual how exit codes and signals
  45. * are exposed on POSIX systems, for more details see also
  46. * [here](https://unix.stackexchange.com/questions/99112/default-exit-code-when-process-is-terminated).
  47. *
  48. * It's also worth noting that process termination depends on all file descriptors
  49. * being closed beforehand.
  50. * This means that all [process pipes](#stream-properties) will emit a `close`
  51. * event before the `exit` event and that no more `data` events will arrive after
  52. * the `exit` event.
  53. * Accordingly, if either of these pipes is in a paused state (`pause()` method
  54. * or internally due to a `pipe()` call), this detection may not trigger.
  55. */
  56. class Process extends EventEmitter
  57. {
  58. /**
  59. * @var WritableStreamInterface|null|DuplexStreamInterface|ReadableStreamInterface
  60. */
  61. public $stdin;
  62. /**
  63. * @var ReadableStreamInterface|null|DuplexStreamInterface|WritableStreamInterface
  64. */
  65. public $stdout;
  66. /**
  67. * @var ReadableStreamInterface|null|DuplexStreamInterface|WritableStreamInterface
  68. */
  69. public $stderr;
  70. /**
  71. * Array with all process pipes (once started)
  72. *
  73. * Unless explicitly configured otherwise during construction, the following
  74. * standard I/O pipes will be assigned by default:
  75. * - 0: STDIN (`WritableStreamInterface`)
  76. * - 1: STDOUT (`ReadableStreamInterface`)
  77. * - 2: STDERR (`ReadableStreamInterface`)
  78. *
  79. * @var array<ReadableStreamInterface|WritableStreamInterface|DuplexStreamInterface>
  80. */
  81. public $pipes = array();
  82. private $cmd;
  83. private $cwd;
  84. private $env;
  85. private $fds;
  86. private $enhanceSigchildCompatibility;
  87. private $sigchildPipe;
  88. private $process;
  89. private $status;
  90. private $exitCode;
  91. private $fallbackExitCode;
  92. private $stopSignal;
  93. private $termSignal;
  94. private static $sigchild;
  95. /**
  96. * Constructor.
  97. *
  98. * @param string $cmd Command line to run
  99. * @param null|string $cwd Current working directory or null to inherit
  100. * @param null|array $env Environment variables or null to inherit
  101. * @param null|array $fds File descriptors to allocate for this process (or null = default STDIO streams)
  102. * @throws \LogicException On windows or when proc_open() is not installed
  103. */
  104. public function __construct($cmd, $cwd = null, array $env = null, array $fds = null)
  105. {
  106. if (!\function_exists('proc_open')) {
  107. throw new \LogicException('The Process class relies on proc_open(), which is not available on your PHP installation.');
  108. }
  109. $this->cmd = $cmd;
  110. $this->cwd = $cwd;
  111. if (null !== $env) {
  112. $this->env = array();
  113. foreach ($env as $key => $value) {
  114. $this->env[(binary) $key] = (binary) $value;
  115. }
  116. }
  117. if ($fds === null) {
  118. $fds = array(
  119. array('pipe', 'r'), // stdin
  120. array('pipe', 'w'), // stdout
  121. array('pipe', 'w'), // stderr
  122. );
  123. }
  124. if (\DIRECTORY_SEPARATOR === '\\') {
  125. foreach ($fds as $fd) {
  126. if (isset($fd[0]) && $fd[0] === 'pipe') {
  127. throw new \LogicException('Process pipes are not supported on Windows due to their blocking nature on Windows');
  128. }
  129. }
  130. }
  131. $this->fds = $fds;
  132. $this->enhanceSigchildCompatibility = self::isSigchildEnabled();
  133. }
  134. /**
  135. * Start the process.
  136. *
  137. * After the process is started, the standard I/O streams will be constructed
  138. * and available via public properties.
  139. *
  140. * This method takes an optional `LoopInterface|null $loop` parameter that can be used to
  141. * pass the event loop instance to use for this process. You can use a `null` value
  142. * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
  143. * This value SHOULD NOT be given unless you're sure you want to explicitly use a
  144. * given event loop instance.
  145. *
  146. * @param ?LoopInterface $loop Loop interface for stream construction
  147. * @param float $interval Interval to periodically monitor process state (seconds)
  148. * @throws \RuntimeException If the process is already running or fails to start
  149. */
  150. public function start(LoopInterface $loop = null, $interval = 0.1)
  151. {
  152. if ($this->isRunning()) {
  153. throw new \RuntimeException('Process is already running');
  154. }
  155. $loop = $loop ?: Loop::get();
  156. $cmd = $this->cmd;
  157. $fdSpec = $this->fds;
  158. $sigchild = null;
  159. // Read exit code through fourth pipe to work around --enable-sigchild
  160. if ($this->enhanceSigchildCompatibility) {
  161. $fdSpec[] = array('pipe', 'w');
  162. \end($fdSpec);
  163. $sigchild = \key($fdSpec);
  164. // make sure this is fourth or higher (do not mess with STDIO)
  165. if ($sigchild < 3) {
  166. $fdSpec[3] = $fdSpec[$sigchild];
  167. unset($fdSpec[$sigchild]);
  168. $sigchild = 3;
  169. }
  170. $cmd = \sprintf('(%s) ' . $sigchild . '>/dev/null; code=$?; echo $code >&' . $sigchild . '; exit $code', $cmd);
  171. }
  172. // on Windows, we do not launch the given command line in a shell (cmd.exe) by default and omit any error dialogs
  173. // the cmd.exe shell can explicitly be given as part of the command as detailed in both documentation and tests
  174. $options = array();
  175. if (\DIRECTORY_SEPARATOR === '\\') {
  176. $options['bypass_shell'] = true;
  177. $options['suppress_errors'] = true;
  178. }
  179. $errstr = '';
  180. \set_error_handler(function ($_, $error) use (&$errstr) {
  181. // Match errstr from PHP's warning message.
  182. // proc_open(/dev/does-not-exist): Failed to open stream: No such file or directory
  183. $errstr = $error;
  184. });
  185. $pipes = array();
  186. $this->process = @\proc_open($cmd, $fdSpec, $pipes, $this->cwd, $this->env, $options);
  187. \restore_error_handler();
  188. if (!\is_resource($this->process)) {
  189. throw new \RuntimeException('Unable to launch a new process: ' . $errstr);
  190. }
  191. // count open process pipes and await close event for each to drain buffers before detecting exit
  192. $that = $this;
  193. $closeCount = 0;
  194. $streamCloseHandler = function () use (&$closeCount, $loop, $interval, $that) {
  195. $closeCount--;
  196. if ($closeCount > 0) {
  197. return;
  198. }
  199. // process already closed => report immediately
  200. if (!$that->isRunning()) {
  201. $that->close();
  202. $that->emit('exit', array($that->getExitCode(), $that->getTermSignal()));
  203. return;
  204. }
  205. // close not detected immediately => check regularly
  206. $loop->addPeriodicTimer($interval, function ($timer) use ($that, $loop) {
  207. if (!$that->isRunning()) {
  208. $loop->cancelTimer($timer);
  209. $that->close();
  210. $that->emit('exit', array($that->getExitCode(), $that->getTermSignal()));
  211. }
  212. });
  213. };
  214. if ($sigchild !== null) {
  215. $this->sigchildPipe = $pipes[$sigchild];
  216. unset($pipes[$sigchild]);
  217. }
  218. foreach ($pipes as $n => $fd) {
  219. // use open mode from stream meta data or fall back to pipe open mode for legacy HHVM
  220. $meta = \stream_get_meta_data($fd);
  221. $mode = $meta['mode'] === '' ? ($this->fds[$n][1] === 'r' ? 'w' : 'r') : $meta['mode'];
  222. if ($mode === 'r+') {
  223. $stream = new DuplexResourceStream($fd, $loop);
  224. $stream->on('close', $streamCloseHandler);
  225. $closeCount++;
  226. } elseif ($mode === 'w') {
  227. $stream = new WritableResourceStream($fd, $loop);
  228. } else {
  229. $stream = new ReadableResourceStream($fd, $loop);
  230. $stream->on('close', $streamCloseHandler);
  231. $closeCount++;
  232. }
  233. $this->pipes[$n] = $stream;
  234. }
  235. $this->stdin = isset($this->pipes[0]) ? $this->pipes[0] : null;
  236. $this->stdout = isset($this->pipes[1]) ? $this->pipes[1] : null;
  237. $this->stderr = isset($this->pipes[2]) ? $this->pipes[2] : null;
  238. // immediately start checking for process exit when started without any I/O pipes
  239. if (!$closeCount) {
  240. $streamCloseHandler();
  241. }
  242. }
  243. /**
  244. * Close the process.
  245. *
  246. * This method should only be invoked via the periodic timer that monitors
  247. * the process state.
  248. */
  249. public function close()
  250. {
  251. if ($this->process === null) {
  252. return;
  253. }
  254. foreach ($this->pipes as $pipe) {
  255. $pipe->close();
  256. }
  257. if ($this->enhanceSigchildCompatibility) {
  258. $this->pollExitCodePipe();
  259. $this->closeExitCodePipe();
  260. }
  261. $exitCode = \proc_close($this->process);
  262. $this->process = null;
  263. if ($this->exitCode === null && $exitCode !== -1) {
  264. $this->exitCode = $exitCode;
  265. }
  266. if ($this->exitCode === null && $this->status['exitcode'] !== -1) {
  267. $this->exitCode = $this->status['exitcode'];
  268. }
  269. if ($this->exitCode === null && $this->fallbackExitCode !== null) {
  270. $this->exitCode = $this->fallbackExitCode;
  271. $this->fallbackExitCode = null;
  272. }
  273. }
  274. /**
  275. * Terminate the process with an optional signal.
  276. *
  277. * @param int $signal Optional signal (default: SIGTERM)
  278. * @return bool Whether the signal was sent successfully
  279. */
  280. public function terminate($signal = null)
  281. {
  282. if ($this->process === null) {
  283. return false;
  284. }
  285. if ($signal !== null) {
  286. return \proc_terminate($this->process, $signal);
  287. }
  288. return \proc_terminate($this->process);
  289. }
  290. /**
  291. * Get the command string used to launch the process.
  292. *
  293. * @return string
  294. */
  295. public function getCommand()
  296. {
  297. return $this->cmd;
  298. }
  299. /**
  300. * Get the exit code returned by the process.
  301. *
  302. * This value is only meaningful if isRunning() has returned false. Null
  303. * will be returned if the process is still running.
  304. *
  305. * Null may also be returned if the process has terminated, but the exit
  306. * code could not be determined (e.g. sigchild compatibility was disabled).
  307. *
  308. * @return int|null
  309. */
  310. public function getExitCode()
  311. {
  312. return $this->exitCode;
  313. }
  314. /**
  315. * Get the process ID.
  316. *
  317. * @return int|null
  318. */
  319. public function getPid()
  320. {
  321. $status = $this->getCachedStatus();
  322. return $status !== null ? $status['pid'] : null;
  323. }
  324. /**
  325. * Get the signal that caused the process to stop its execution.
  326. *
  327. * This value is only meaningful if isStopped() has returned true. Null will
  328. * be returned if the process was never stopped.
  329. *
  330. * @return int|null
  331. */
  332. public function getStopSignal()
  333. {
  334. return $this->stopSignal;
  335. }
  336. /**
  337. * Get the signal that caused the process to terminate its execution.
  338. *
  339. * This value is only meaningful if isTerminated() has returned true. Null
  340. * will be returned if the process was never terminated.
  341. *
  342. * @return int|null
  343. */
  344. public function getTermSignal()
  345. {
  346. return $this->termSignal;
  347. }
  348. /**
  349. * Return whether the process is still running.
  350. *
  351. * @return bool
  352. */
  353. public function isRunning()
  354. {
  355. if ($this->process === null) {
  356. return false;
  357. }
  358. $status = $this->getFreshStatus();
  359. return $status !== null ? $status['running'] : false;
  360. }
  361. /**
  362. * Return whether the process has been stopped by a signal.
  363. *
  364. * @return bool
  365. */
  366. public function isStopped()
  367. {
  368. $status = $this->getFreshStatus();
  369. return $status !== null ? $status['stopped'] : false;
  370. }
  371. /**
  372. * Return whether the process has been terminated by an uncaught signal.
  373. *
  374. * @return bool
  375. */
  376. public function isTerminated()
  377. {
  378. $status = $this->getFreshStatus();
  379. return $status !== null ? $status['signaled'] : false;
  380. }
  381. /**
  382. * Return whether PHP has been compiled with the '--enable-sigchild' option.
  383. *
  384. * @see \Symfony\Component\Process\Process::isSigchildEnabled()
  385. * @return bool
  386. */
  387. public final static function isSigchildEnabled()
  388. {
  389. if (null !== self::$sigchild) {
  390. return self::$sigchild;
  391. }
  392. if (!\function_exists('phpinfo')) {
  393. return self::$sigchild = false; // @codeCoverageIgnore
  394. }
  395. \ob_start();
  396. \phpinfo(INFO_GENERAL);
  397. return self::$sigchild = false !== \strpos(\ob_get_clean(), '--enable-sigchild');
  398. }
  399. /**
  400. * Enable or disable sigchild compatibility mode.
  401. *
  402. * Sigchild compatibility mode is required to get the exit code and
  403. * determine the success of a process when PHP has been compiled with
  404. * the --enable-sigchild option.
  405. *
  406. * @param bool $sigchild
  407. * @return void
  408. */
  409. public final static function setSigchildEnabled($sigchild)
  410. {
  411. self::$sigchild = (bool) $sigchild;
  412. }
  413. /**
  414. * Check the fourth pipe for an exit code.
  415. *
  416. * This should only be used if --enable-sigchild compatibility was enabled.
  417. */
  418. private function pollExitCodePipe()
  419. {
  420. if ($this->sigchildPipe === null) {
  421. return;
  422. }
  423. $r = array($this->sigchildPipe);
  424. $w = $e = null;
  425. $n = @\stream_select($r, $w, $e, 0);
  426. if (1 !== $n) {
  427. return;
  428. }
  429. $data = \fread($r[0], 8192);
  430. if (\strlen($data) > 0) {
  431. $this->fallbackExitCode = (int) $data;
  432. }
  433. }
  434. /**
  435. * Close the fourth pipe used to relay an exit code.
  436. *
  437. * This should only be used if --enable-sigchild compatibility was enabled.
  438. */
  439. private function closeExitCodePipe()
  440. {
  441. if ($this->sigchildPipe === null) {
  442. return;
  443. }
  444. \fclose($this->sigchildPipe);
  445. $this->sigchildPipe = null;
  446. }
  447. /**
  448. * Return the cached process status.
  449. *
  450. * @return array
  451. */
  452. private function getCachedStatus()
  453. {
  454. if ($this->status === null) {
  455. $this->updateStatus();
  456. }
  457. return $this->status;
  458. }
  459. /**
  460. * Return the updated process status.
  461. *
  462. * @return array
  463. */
  464. private function getFreshStatus()
  465. {
  466. $this->updateStatus();
  467. return $this->status;
  468. }
  469. /**
  470. * Update the process status, stop/term signals, and exit code.
  471. *
  472. * Stop/term signals are only updated if the process is currently stopped or
  473. * signaled, respectively. Otherwise, signal values will remain as-is so the
  474. * corresponding getter methods may be used at a later point in time.
  475. */
  476. private function updateStatus()
  477. {
  478. if ($this->process === null) {
  479. return;
  480. }
  481. $this->status = \proc_get_status($this->process);
  482. if ($this->status === false) {
  483. throw new \UnexpectedValueException('proc_get_status() failed');
  484. }
  485. if ($this->status['stopped']) {
  486. $this->stopSignal = $this->status['stopsig'];
  487. }
  488. if ($this->status['signaled']) {
  489. $this->termSignal = $this->status['termsig'];
  490. }
  491. if (!$this->status['running'] && -1 !== $this->status['exitcode']) {
  492. $this->exitCode = $this->status['exitcode'];
  493. }
  494. }
  495. }