You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1206 lines
30 KiB

  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | ThinkPHP [ WE CAN DO IT JUST THINK ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // | Author: yunwuxin <448901948@qq.com>
  10. // +----------------------------------------------------------------------
  11. namespace think;
  12. use think\process\exception\Failed as ProcessFailedException;
  13. use think\process\exception\Timeout as ProcessTimeoutException;
  14. use think\process\pipes\Pipes;
  15. use think\process\pipes\Unix as UnixPipes;
  16. use think\process\pipes\Windows as WindowsPipes;
  17. use think\process\Utils;
  18. class Process
  19. {
  20. const ERR = 'err';
  21. const OUT = 'out';
  22. const STATUS_READY = 'ready';
  23. const STATUS_STARTED = 'started';
  24. const STATUS_TERMINATED = 'terminated';
  25. const STDIN = 0;
  26. const STDOUT = 1;
  27. const STDERR = 2;
  28. const TIMEOUT_PRECISION = 0.2;
  29. private $callback;
  30. private $commandline;
  31. private $cwd;
  32. private $env;
  33. private $input;
  34. private $starttime;
  35. private $lastOutputTime;
  36. private $timeout;
  37. private $idleTimeout;
  38. private $options;
  39. private $exitcode;
  40. private $fallbackExitcode;
  41. private $processInformation;
  42. private $outputDisabled = false;
  43. private $stdout;
  44. private $stderr;
  45. private $enhanceWindowsCompatibility = true;
  46. private $enhanceSigchildCompatibility;
  47. private $process;
  48. private $status = self::STATUS_READY;
  49. private $incrementalOutputOffset = 0;
  50. private $incrementalErrorOutputOffset = 0;
  51. private $tty;
  52. private $pty;
  53. private $useFileHandles = false;
  54. /** @var Pipes */
  55. private $processPipes;
  56. private $latestSignal;
  57. private static $sigchild;
  58. /**
  59. * @var array
  60. */
  61. public static $exitCodes = [
  62. 0 => 'OK',
  63. 1 => 'General error',
  64. 2 => 'Misuse of shell builtins',
  65. 126 => 'Invoked command cannot execute',
  66. 127 => 'Command not found',
  67. 128 => 'Invalid exit argument',
  68. // signals
  69. 129 => 'Hangup',
  70. 130 => 'Interrupt',
  71. 131 => 'Quit and dump core',
  72. 132 => 'Illegal instruction',
  73. 133 => 'Trace/breakpoint trap',
  74. 134 => 'Process aborted',
  75. 135 => 'Bus error: "access to undefined portion of memory object"',
  76. 136 => 'Floating point exception: "erroneous arithmetic operation"',
  77. 137 => 'Kill (terminate immediately)',
  78. 138 => 'User-defined 1',
  79. 139 => 'Segmentation violation',
  80. 140 => 'User-defined 2',
  81. 141 => 'Write to pipe with no one reading',
  82. 142 => 'Signal raised by alarm',
  83. 143 => 'Termination (request to terminate)',
  84. // 144 - not defined
  85. 145 => 'Child process terminated, stopped (or continued*)',
  86. 146 => 'Continue if stopped',
  87. 147 => 'Stop executing temporarily',
  88. 148 => 'Terminal stop signal',
  89. 149 => 'Background process attempting to read from tty ("in")',
  90. 150 => 'Background process attempting to write to tty ("out")',
  91. 151 => 'Urgent data available on socket',
  92. 152 => 'CPU time limit exceeded',
  93. 153 => 'File size limit exceeded',
  94. 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
  95. 155 => 'Profiling timer expired',
  96. // 156 - not defined
  97. 157 => 'Pollable event',
  98. // 158 - not defined
  99. 159 => 'Bad syscall',
  100. ];
  101. /**
  102. * 构造方法
  103. * @param string $commandline 指令
  104. * @param string|null $cwd 工作目录
  105. * @param array|null $env 环境变量
  106. * @param string|null $input 输入
  107. * @param int|float|null $timeout 超时时间
  108. * @param array $options proc_open的选项
  109. * @throws \RuntimeException
  110. * @api
  111. */
  112. public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = [])
  113. {
  114. if (!function_exists('proc_open')) {
  115. throw new \RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.');
  116. }
  117. $this->commandline = $commandline;
  118. $this->cwd = $cwd;
  119. if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DS)) {
  120. $this->cwd = getcwd();
  121. }
  122. if (null !== $env) {
  123. $this->setEnv($env);
  124. }
  125. $this->input = $input;
  126. $this->setTimeout($timeout);
  127. $this->useFileHandles = '\\' === DS;
  128. $this->pty = false;
  129. $this->enhanceWindowsCompatibility = true;
  130. $this->enhanceSigchildCompatibility = '\\' !== DS && $this->isSigchildEnabled();
  131. $this->options = array_replace([
  132. 'suppress_errors' => true,
  133. 'binary_pipes' => true,
  134. ], $options);
  135. }
  136. public function __destruct()
  137. {
  138. $this->stop();
  139. }
  140. public function __clone()
  141. {
  142. $this->resetProcessData();
  143. }
  144. /**
  145. * 运行指令
  146. * @param callback|null $callback
  147. * @return int
  148. */
  149. public function run($callback = null)
  150. {
  151. $this->start($callback);
  152. return $this->wait();
  153. }
  154. /**
  155. * 运行指令
  156. * @param callable|null $callback
  157. * @return self
  158. * @throws \RuntimeException
  159. * @throws ProcessFailedException
  160. */
  161. public function mustRun($callback = null)
  162. {
  163. if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) {
  164. throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
  165. }
  166. if (0 !== $this->run($callback)) {
  167. throw new ProcessFailedException($this);
  168. }
  169. return $this;
  170. }
  171. /**
  172. * 启动进程并写到 STDIN 输入后返回。
  173. * @param callable|null $callback
  174. * @throws \RuntimeException
  175. * @throws \RuntimeException
  176. * @throws \LogicException
  177. */
  178. public function start($callback = null)
  179. {
  180. if ($this->isRunning()) {
  181. throw new \RuntimeException('Process is already running');
  182. }
  183. if ($this->outputDisabled && null !== $callback) {
  184. throw new \LogicException('Output has been disabled, enable it to allow the use of a callback.');
  185. }
  186. $this->resetProcessData();
  187. $this->starttime = $this->lastOutputTime = microtime(true);
  188. $this->callback = $this->buildCallback($callback);
  189. $descriptors = $this->getDescriptors();
  190. $commandline = $this->commandline;
  191. if ('\\' === DS && $this->enhanceWindowsCompatibility) {
  192. $commandline = 'cmd /V:ON /E:ON /C "(' . $commandline . ')';
  193. foreach ($this->processPipes->getFiles() as $offset => $filename) {
  194. $commandline .= ' ' . $offset . '>' . Utils::escapeArgument($filename);
  195. }
  196. $commandline .= '"';
  197. if (!isset($this->options['bypass_shell'])) {
  198. $this->options['bypass_shell'] = true;
  199. }
  200. }
  201. $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options);
  202. if (!is_resource($this->process)) {
  203. throw new \RuntimeException('Unable to launch a new process.');
  204. }
  205. $this->status = self::STATUS_STARTED;
  206. if ($this->tty) {
  207. return;
  208. }
  209. $this->updateStatus(false);
  210. $this->checkTimeout();
  211. }
  212. /**
  213. * 重启进程
  214. * @param callable|null $callback
  215. * @return Process
  216. * @throws \RuntimeException
  217. * @throws \RuntimeException
  218. */
  219. public function restart($callback = null)
  220. {
  221. if ($this->isRunning()) {
  222. throw new \RuntimeException('Process is already running');
  223. }
  224. $process = clone $this;
  225. $process->start($callback);
  226. return $process;
  227. }
  228. /**
  229. * 等待要终止的进程
  230. * @param callable|null $callback
  231. * @return int
  232. */
  233. public function wait($callback = null)
  234. {
  235. $this->requireProcessIsStarted(__FUNCTION__);
  236. $this->updateStatus(false);
  237. if (null !== $callback) {
  238. $this->callback = $this->buildCallback($callback);
  239. }
  240. do {
  241. $this->checkTimeout();
  242. $running = '\\' === DS ? $this->isRunning() : $this->processPipes->areOpen();
  243. $close = '\\' !== DS || !$running;
  244. $this->readPipes(true, $close);
  245. } while ($running);
  246. while ($this->isRunning()) {
  247. usleep(1000);
  248. }
  249. if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
  250. throw new \RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig']));
  251. }
  252. return $this->exitcode;
  253. }
  254. /**
  255. * 获取PID
  256. * @return int|null
  257. * @throws \RuntimeException
  258. */
  259. public function getPid()
  260. {
  261. if ($this->isSigchildEnabled()) {
  262. throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved.');
  263. }
  264. $this->updateStatus(false);
  265. return $this->isRunning() ? $this->processInformation['pid'] : null;
  266. }
  267. /**
  268. * 将一个 POSIX 信号发送到进程中
  269. * @param int $signal
  270. * @return Process
  271. */
  272. public function signal($signal)
  273. {
  274. $this->doSignal($signal, true);
  275. return $this;
  276. }
  277. /**
  278. * 禁用从底层过程获取输出和错误输出。
  279. * @return Process
  280. */
  281. public function disableOutput()
  282. {
  283. if ($this->isRunning()) {
  284. throw new \RuntimeException('Disabling output while the process is running is not possible.');
  285. }
  286. if (null !== $this->idleTimeout) {
  287. throw new \LogicException('Output can not be disabled while an idle timeout is set.');
  288. }
  289. $this->outputDisabled = true;
  290. return $this;
  291. }
  292. /**
  293. * 开启从底层过程获取输出和错误输出。
  294. * @return Process
  295. * @throws \RuntimeException
  296. */
  297. public function enableOutput()
  298. {
  299. if ($this->isRunning()) {
  300. throw new \RuntimeException('Enabling output while the process is running is not possible.');
  301. }
  302. $this->outputDisabled = false;
  303. return $this;
  304. }
  305. /**
  306. * 输出是否禁用
  307. * @return bool
  308. */
  309. public function isOutputDisabled()
  310. {
  311. return $this->outputDisabled;
  312. }
  313. /**
  314. * 获取当前的输出管道
  315. * @return string
  316. * @throws \LogicException
  317. * @throws \LogicException
  318. * @api
  319. */
  320. public function getOutput()
  321. {
  322. if ($this->outputDisabled) {
  323. throw new \LogicException('Output has been disabled.');
  324. }
  325. $this->requireProcessIsStarted(__FUNCTION__);
  326. $this->readPipes(false, '\\' === DS ? !$this->processInformation['running'] : true);
  327. return $this->stdout;
  328. }
  329. /**
  330. * 以增量方式返回的输出结果。
  331. * @return string
  332. */
  333. public function getIncrementalOutput()
  334. {
  335. $this->requireProcessIsStarted(__FUNCTION__);
  336. $data = $this->getOutput();
  337. $latest = substr($data, $this->incrementalOutputOffset);
  338. if (false === $latest) {
  339. return '';
  340. }
  341. $this->incrementalOutputOffset = strlen($data);
  342. return $latest;
  343. }
  344. /**
  345. * 清空输出
  346. * @return Process
  347. */
  348. public function clearOutput()
  349. {
  350. $this->stdout = '';
  351. $this->incrementalOutputOffset = 0;
  352. return $this;
  353. }
  354. /**
  355. * 返回当前的错误输出的过程 (STDERR)。
  356. * @return string
  357. */
  358. public function getErrorOutput()
  359. {
  360. if ($this->outputDisabled) {
  361. throw new \LogicException('Output has been disabled.');
  362. }
  363. $this->requireProcessIsStarted(__FUNCTION__);
  364. $this->readPipes(false, '\\' === DS ? !$this->processInformation['running'] : true);
  365. return $this->stderr;
  366. }
  367. /**
  368. * 以增量方式返回 errorOutput
  369. * @return string
  370. */
  371. public function getIncrementalErrorOutput()
  372. {
  373. $this->requireProcessIsStarted(__FUNCTION__);
  374. $data = $this->getErrorOutput();
  375. $latest = substr($data, $this->incrementalErrorOutputOffset);
  376. if (false === $latest) {
  377. return '';
  378. }
  379. $this->incrementalErrorOutputOffset = strlen($data);
  380. return $latest;
  381. }
  382. /**
  383. * 清空 errorOutput
  384. * @return Process
  385. */
  386. public function clearErrorOutput()
  387. {
  388. $this->stderr = '';
  389. $this->incrementalErrorOutputOffset = 0;
  390. return $this;
  391. }
  392. /**
  393. * 获取退出码
  394. * @return null|int
  395. */
  396. public function getExitCode()
  397. {
  398. if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) {
  399. throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
  400. }
  401. $this->updateStatus(false);
  402. return $this->exitcode;
  403. }
  404. /**
  405. * 获取退出文本
  406. * @return null|string
  407. */
  408. public function getExitCodeText()
  409. {
  410. if (null === $exitcode = $this->getExitCode()) {
  411. return;
  412. }
  413. return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error';
  414. }
  415. /**
  416. * 检查是否成功
  417. * @return bool
  418. */
  419. public function isSuccessful()
  420. {
  421. return 0 === $this->getExitCode();
  422. }
  423. /**
  424. * 是否未捕获的信号已被终止子进程
  425. * @return bool
  426. */
  427. public function hasBeenSignaled()
  428. {
  429. $this->requireProcessIsTerminated(__FUNCTION__);
  430. if ($this->isSigchildEnabled()) {
  431. throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
  432. }
  433. $this->updateStatus(false);
  434. return $this->processInformation['signaled'];
  435. }
  436. /**
  437. * 返回导致子进程终止其执行的数。
  438. * @return int
  439. */
  440. public function getTermSignal()
  441. {
  442. $this->requireProcessIsTerminated(__FUNCTION__);
  443. if ($this->isSigchildEnabled()) {
  444. throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
  445. }
  446. $this->updateStatus(false);
  447. return $this->processInformation['termsig'];
  448. }
  449. /**
  450. * 检查子进程信号是否已停止
  451. * @return bool
  452. */
  453. public function hasBeenStopped()
  454. {
  455. $this->requireProcessIsTerminated(__FUNCTION__);
  456. $this->updateStatus(false);
  457. return $this->processInformation['stopped'];
  458. }
  459. /**
  460. * 返回导致子进程停止其执行的数。
  461. * @return int
  462. */
  463. public function getStopSignal()
  464. {
  465. $this->requireProcessIsTerminated(__FUNCTION__);
  466. $this->updateStatus(false);
  467. return $this->processInformation['stopsig'];
  468. }
  469. /**
  470. * 检查是否正在运行
  471. * @return bool
  472. */
  473. public function isRunning()
  474. {
  475. if (self::STATUS_STARTED !== $this->status) {
  476. return false;
  477. }
  478. $this->updateStatus(false);
  479. return $this->processInformation['running'];
  480. }
  481. /**
  482. * 检查是否已开始
  483. * @return bool
  484. */
  485. public function isStarted()
  486. {
  487. return self::STATUS_READY != $this->status;
  488. }
  489. /**
  490. * 检查是否已终止
  491. * @return bool
  492. */
  493. public function isTerminated()
  494. {
  495. $this->updateStatus(false);
  496. return self::STATUS_TERMINATED == $this->status;
  497. }
  498. /**
  499. * 获取当前的状态
  500. * @return string
  501. */
  502. public function getStatus()
  503. {
  504. $this->updateStatus(false);
  505. return $this->status;
  506. }
  507. /**
  508. * 终止进程
  509. */
  510. public function stop()
  511. {
  512. if ($this->isRunning()) {
  513. if ('\\' === DS && !$this->isSigchildEnabled()) {
  514. exec(sprintf('taskkill /F /T /PID %d 2>&1', $this->getPid()), $output, $exitCode);
  515. if ($exitCode > 0) {
  516. throw new \RuntimeException('Unable to kill the process');
  517. }
  518. } else {
  519. $pids = preg_split('/\s+/', `ps -o pid --no-heading --ppid {$this->getPid()}`);
  520. foreach ($pids as $pid) {
  521. if (is_numeric($pid)) {
  522. posix_kill($pid, 9);
  523. }
  524. }
  525. }
  526. }
  527. $this->updateStatus(false);
  528. if ($this->processInformation['running']) {
  529. $this->close();
  530. }
  531. return $this->exitcode;
  532. }
  533. /**
  534. * 添加一行输出
  535. * @param string $line
  536. */
  537. public function addOutput($line)
  538. {
  539. $this->lastOutputTime = microtime(true);
  540. $this->stdout .= $line;
  541. }
  542. /**
  543. * 添加一行错误输出
  544. * @param string $line
  545. */
  546. public function addErrorOutput($line)
  547. {
  548. $this->lastOutputTime = microtime(true);
  549. $this->stderr .= $line;
  550. }
  551. /**
  552. * 获取被执行的指令
  553. * @return string
  554. */
  555. public function getCommandLine()
  556. {
  557. return $this->commandline;
  558. }
  559. /**
  560. * 设置指令
  561. * @param string $commandline
  562. * @return self
  563. */
  564. public function setCommandLine($commandline)
  565. {
  566. $this->commandline = $commandline;
  567. return $this;
  568. }
  569. /**
  570. * 获取超时时间
  571. * @return float|null
  572. */
  573. public function getTimeout()
  574. {
  575. return $this->timeout;
  576. }
  577. /**
  578. * 获取idle超时时间
  579. * @return float|null
  580. */
  581. public function getIdleTimeout()
  582. {
  583. return $this->idleTimeout;
  584. }
  585. /**
  586. * 设置超时时间
  587. * @param int|float|null $timeout
  588. * @return self
  589. */
  590. public function setTimeout($timeout)
  591. {
  592. $this->timeout = $this->validateTimeout($timeout);
  593. return $this;
  594. }
  595. /**
  596. * 设置idle超时时间
  597. * @param int|float|null $timeout
  598. * @return self
  599. */
  600. public function setIdleTimeout($timeout)
  601. {
  602. if (null !== $timeout && $this->outputDisabled) {
  603. throw new \LogicException('Idle timeout can not be set while the output is disabled.');
  604. }
  605. $this->idleTimeout = $this->validateTimeout($timeout);
  606. return $this;
  607. }
  608. /**
  609. * 设置TTY
  610. * @param bool $tty
  611. * @return self
  612. */
  613. public function setTty($tty)
  614. {
  615. if ('\\' === DS && $tty) {
  616. throw new \RuntimeException('TTY mode is not supported on Windows platform.');
  617. }
  618. if ($tty && (!file_exists('/dev/tty') || !is_readable('/dev/tty'))) {
  619. throw new \RuntimeException('TTY mode requires /dev/tty to be readable.');
  620. }
  621. $this->tty = (bool) $tty;
  622. return $this;
  623. }
  624. /**
  625. * 检查是否是tty模式
  626. * @return bool
  627. */
  628. public function isTty()
  629. {
  630. return $this->tty;
  631. }
  632. /**
  633. * 设置pty模式
  634. * @param bool $bool
  635. * @return self
  636. */
  637. public function setPty($bool)
  638. {
  639. $this->pty = (bool) $bool;
  640. return $this;
  641. }
  642. /**
  643. * 是否是pty模式
  644. * @return bool
  645. */
  646. public function isPty()
  647. {
  648. return $this->pty;
  649. }
  650. /**
  651. * 获取工作目录
  652. * @return string|null
  653. */
  654. public function getWorkingDirectory()
  655. {
  656. if (null === $this->cwd) {
  657. return getcwd() ?: null;
  658. }
  659. return $this->cwd;
  660. }
  661. /**
  662. * 设置工作目录
  663. * @param string $cwd
  664. * @return self
  665. */
  666. public function setWorkingDirectory($cwd)
  667. {
  668. $this->cwd = $cwd;
  669. return $this;
  670. }
  671. /**
  672. * 获取环境变量
  673. * @return array
  674. */
  675. public function getEnv()
  676. {
  677. return $this->env;
  678. }
  679. /**
  680. * 设置环境变量
  681. * @param array $env
  682. * @return self
  683. */
  684. public function setEnv(array $env)
  685. {
  686. $env = array_filter($env, function ($value) {
  687. return !is_array($value);
  688. });
  689. $this->env = [];
  690. foreach ($env as $key => $value) {
  691. $this->env[(binary) $key] = (binary) $value;
  692. }
  693. return $this;
  694. }
  695. /**
  696. * 获取输入
  697. * @return null|string
  698. */
  699. public function getInput()
  700. {
  701. return $this->input;
  702. }
  703. /**
  704. * 设置输入
  705. * @param mixed $input
  706. * @return self
  707. */
  708. public function setInput($input)
  709. {
  710. if ($this->isRunning()) {
  711. throw new \LogicException('Input can not be set while the process is running.');
  712. }
  713. $this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input);
  714. return $this;
  715. }
  716. /**
  717. * 获取proc_open的选项
  718. * @return array
  719. */
  720. public function getOptions()
  721. {
  722. return $this->options;
  723. }
  724. /**
  725. * 设置proc_open的选项
  726. * @param array $options
  727. * @return self
  728. */
  729. public function setOptions(array $options)
  730. {
  731. $this->options = $options;
  732. return $this;
  733. }
  734. /**
  735. * 是否兼容windows
  736. * @return bool
  737. */
  738. public function getEnhanceWindowsCompatibility()
  739. {
  740. return $this->enhanceWindowsCompatibility;
  741. }
  742. /**
  743. * 设置是否兼容windows
  744. * @param bool $enhance
  745. * @return self
  746. */
  747. public function setEnhanceWindowsCompatibility($enhance)
  748. {
  749. $this->enhanceWindowsCompatibility = (bool) $enhance;
  750. return $this;
  751. }
  752. /**
  753. * 返回是否 sigchild 兼容模式激活
  754. * @return bool
  755. */
  756. public function getEnhanceSigchildCompatibility()
  757. {
  758. return $this->enhanceSigchildCompatibility;
  759. }
  760. /**
  761. * 激活 sigchild 兼容性模式。
  762. * @param bool $enhance
  763. * @return self
  764. */
  765. public function setEnhanceSigchildCompatibility($enhance)
  766. {
  767. $this->enhanceSigchildCompatibility = (bool) $enhance;
  768. return $this;
  769. }
  770. /**
  771. * 是否超时
  772. */
  773. public function checkTimeout()
  774. {
  775. if (self::STATUS_STARTED !== $this->status) {
  776. return;
  777. }
  778. if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
  779. $this->stop();
  780. throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_GENERAL);
  781. }
  782. if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
  783. $this->stop();
  784. throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_IDLE);
  785. }
  786. }
  787. /**
  788. * 是否支持pty
  789. * @return bool
  790. */
  791. public static function isPtySupported()
  792. {
  793. static $result;
  794. if (null !== $result) {
  795. return $result;
  796. }
  797. if ('\\' === DS) {
  798. return $result = false;
  799. }
  800. $proc = @proc_open('echo 1', [['pty'], ['pty'], ['pty']], $pipes);
  801. if (is_resource($proc)) {
  802. proc_close($proc);
  803. return $result = true;
  804. }
  805. return $result = false;
  806. }
  807. /**
  808. * 创建所需的 proc_open 的描述符
  809. * @return array
  810. */
  811. private function getDescriptors()
  812. {
  813. if ('\\' === DS) {
  814. $this->processPipes = WindowsPipes::create($this, $this->input);
  815. } else {
  816. $this->processPipes = UnixPipes::create($this, $this->input);
  817. }
  818. $descriptors = $this->processPipes->getDescriptors($this->outputDisabled);
  819. if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
  820. $descriptors = array_merge($descriptors, [['pipe', 'w']]);
  821. $this->commandline = '(' . $this->commandline . ') 3>/dev/null; code=$?; echo $code >&3; exit $code';
  822. }
  823. return $descriptors;
  824. }
  825. /**
  826. * 建立 wait () 使用的回调。
  827. * @param callable|null $callback
  828. * @return callable
  829. */
  830. protected function buildCallback($callback)
  831. {
  832. $out = self::OUT;
  833. $callback = function ($type, $data) use ($callback, $out) {
  834. if ($out == $type) {
  835. $this->addOutput($data);
  836. } else {
  837. $this->addErrorOutput($data);
  838. }
  839. if (null !== $callback) {
  840. call_user_func($callback, $type, $data);
  841. }
  842. };
  843. return $callback;
  844. }
  845. /**
  846. * 更新状态
  847. * @param bool $blocking
  848. */
  849. protected function updateStatus($blocking)
  850. {
  851. if (self::STATUS_STARTED !== $this->status) {
  852. return;
  853. }
  854. $this->processInformation = proc_get_status($this->process);
  855. $this->captureExitCode();
  856. $this->readPipes($blocking, '\\' === DS ? !$this->processInformation['running'] : true);
  857. if (!$this->processInformation['running']) {
  858. $this->close();
  859. }
  860. }
  861. /**
  862. * 是否开启 '--enable-sigchild'
  863. * @return bool
  864. */
  865. protected function isSigchildEnabled()
  866. {
  867. if (null !== self::$sigchild) {
  868. return self::$sigchild;
  869. }
  870. if (!function_exists('phpinfo')) {
  871. return self::$sigchild = false;
  872. }
  873. ob_start();
  874. phpinfo(INFO_GENERAL);
  875. return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
  876. }
  877. /**
  878. * 验证是否超时
  879. * @param int|float|null $timeout
  880. * @return float|null
  881. */
  882. private function validateTimeout($timeout)
  883. {
  884. $timeout = (float) $timeout;
  885. if (0.0 === $timeout) {
  886. $timeout = null;
  887. } elseif ($timeout < 0) {
  888. throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
  889. }
  890. return $timeout;
  891. }
  892. /**
  893. * 读取pipes
  894. * @param bool $blocking
  895. * @param bool $close
  896. */
  897. private function readPipes($blocking, $close)
  898. {
  899. $result = $this->processPipes->readAndWrite($blocking, $close);
  900. $callback = $this->callback;
  901. foreach ($result as $type => $data) {
  902. if (3 == $type) {
  903. $this->fallbackExitcode = (int) $data;
  904. } else {
  905. $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data);
  906. }
  907. }
  908. }
  909. /**
  910. * 捕获退出码
  911. */
  912. private function captureExitCode()
  913. {
  914. if (isset($this->processInformation['exitcode']) && -1 != $this->processInformation['exitcode']) {
  915. $this->exitcode = $this->processInformation['exitcode'];
  916. }
  917. }
  918. /**
  919. * 关闭资源
  920. * @return int 退出码
  921. */
  922. private function close()
  923. {
  924. $this->processPipes->close();
  925. if (is_resource($this->process)) {
  926. $exitcode = proc_close($this->process);
  927. } else {
  928. $exitcode = -1;
  929. }
  930. $this->exitcode = -1 !== $exitcode ? $exitcode : (null !== $this->exitcode ? $this->exitcode : -1);
  931. $this->status = self::STATUS_TERMINATED;
  932. if (-1 === $this->exitcode && null !== $this->fallbackExitcode) {
  933. $this->exitcode = $this->fallbackExitcode;
  934. } elseif (-1 === $this->exitcode && $this->processInformation['signaled']
  935. && 0 < $this->processInformation['termsig']
  936. ) {
  937. $this->exitcode = 128 + $this->processInformation['termsig'];
  938. }
  939. return $this->exitcode;
  940. }
  941. /**
  942. * 重置数据
  943. */
  944. private function resetProcessData()
  945. {
  946. $this->starttime = null;
  947. $this->callback = null;
  948. $this->exitcode = null;
  949. $this->fallbackExitcode = null;
  950. $this->processInformation = null;
  951. $this->stdout = null;
  952. $this->stderr = null;
  953. $this->process = null;
  954. $this->latestSignal = null;
  955. $this->status = self::STATUS_READY;
  956. $this->incrementalOutputOffset = 0;
  957. $this->incrementalErrorOutputOffset = 0;
  958. }
  959. /**
  960. * 将一个 POSIX 信号发送到进程中。
  961. * @param int $signal
  962. * @param bool $throwException
  963. * @return bool
  964. */
  965. private function doSignal($signal, $throwException)
  966. {
  967. if (!$this->isRunning()) {
  968. if ($throwException) {
  969. throw new \LogicException('Can not send signal on a non running process.');
  970. }
  971. return false;
  972. }
  973. if ($this->isSigchildEnabled()) {
  974. if ($throwException) {
  975. throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.');
  976. }
  977. return false;
  978. }
  979. if (true !== @proc_terminate($this->process, $signal)) {
  980. if ($throwException) {
  981. throw new \RuntimeException(sprintf('Error while sending signal `%s`.', $signal));
  982. }
  983. return false;
  984. }
  985. $this->latestSignal = $signal;
  986. return true;
  987. }
  988. /**
  989. * 确保进程已经开启
  990. * @param string $functionName
  991. */
  992. private function requireProcessIsStarted($functionName)
  993. {
  994. if (!$this->isStarted()) {
  995. throw new \LogicException(sprintf('Process must be started before calling %s.', $functionName));
  996. }
  997. }
  998. /**
  999. * 确保进程已经终止
  1000. * @param string $functionName
  1001. */
  1002. private function requireProcessIsTerminated($functionName)
  1003. {
  1004. if (!$this->isTerminated()) {
  1005. throw new \LogicException(sprintf('Process must be terminated before calling %s.', $functionName));
  1006. }
  1007. }
  1008. }