Process.php 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509
  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\ProcessFailedException;
  14. use Symfony\Component\Process\Exception\ProcessTimedOutException;
  15. use Symfony\Component\Process\Exception\RuntimeException;
  16. use Symfony\Component\Process\Pipes\PipesInterface;
  17. use Symfony\Component\Process\Pipes\UnixPipes;
  18. use Symfony\Component\Process\Pipes\WindowsPipes;
  19. /**
  20. * Process is a thin wrapper around proc_* functions to easily
  21. * start independent PHP processes.
  22. *
  23. * @author Fabien Potencier <fabien@symfony.com>
  24. * @author Romain Neutron <imprec@gmail.com>
  25. */
  26. class Process
  27. {
  28. const ERR = 'err';
  29. const OUT = 'out';
  30. const STATUS_READY = 'ready';
  31. const STATUS_STARTED = 'started';
  32. const STATUS_TERMINATED = 'terminated';
  33. const STDIN = 0;
  34. const STDOUT = 1;
  35. const STDERR = 2;
  36. // Timeout Precision in seconds.
  37. const TIMEOUT_PRECISION = 0.2;
  38. private $callback;
  39. private $commandline;
  40. private $cwd;
  41. private $env;
  42. private $input;
  43. private $starttime;
  44. private $lastOutputTime;
  45. private $timeout;
  46. private $idleTimeout;
  47. private $options;
  48. private $exitcode;
  49. private $fallbackStatus = array();
  50. private $processInformation;
  51. private $outputDisabled = false;
  52. private $stdout;
  53. private $stderr;
  54. private $enhanceWindowsCompatibility = true;
  55. private $enhanceSigchildCompatibility;
  56. private $process;
  57. private $status = self::STATUS_READY;
  58. private $incrementalOutputOffset = 0;
  59. private $incrementalErrorOutputOffset = 0;
  60. private $tty;
  61. private $pty;
  62. private $useFileHandles = false;
  63. /** @var PipesInterface */
  64. private $processPipes;
  65. private $latestSignal;
  66. private static $sigchild;
  67. /**
  68. * Exit codes translation table.
  69. *
  70. * User-defined errors must use exit codes in the 64-113 range.
  71. */
  72. public static $exitCodes = array(
  73. 0 => 'OK',
  74. 1 => 'General error',
  75. 2 => 'Misuse of shell builtins',
  76. 126 => 'Invoked command cannot execute',
  77. 127 => 'Command not found',
  78. 128 => 'Invalid exit argument',
  79. // signals
  80. 129 => 'Hangup',
  81. 130 => 'Interrupt',
  82. 131 => 'Quit and dump core',
  83. 132 => 'Illegal instruction',
  84. 133 => 'Trace/breakpoint trap',
  85. 134 => 'Process aborted',
  86. 135 => 'Bus error: "access to undefined portion of memory object"',
  87. 136 => 'Floating point exception: "erroneous arithmetic operation"',
  88. 137 => 'Kill (terminate immediately)',
  89. 138 => 'User-defined 1',
  90. 139 => 'Segmentation violation',
  91. 140 => 'User-defined 2',
  92. 141 => 'Write to pipe with no one reading',
  93. 142 => 'Signal raised by alarm',
  94. 143 => 'Termination (request to terminate)',
  95. // 144 - not defined
  96. 145 => 'Child process terminated, stopped (or continued*)',
  97. 146 => 'Continue if stopped',
  98. 147 => 'Stop executing temporarily',
  99. 148 => 'Terminal stop signal',
  100. 149 => 'Background process attempting to read from tty ("in")',
  101. 150 => 'Background process attempting to write to tty ("out")',
  102. 151 => 'Urgent data available on socket',
  103. 152 => 'CPU time limit exceeded',
  104. 153 => 'File size limit exceeded',
  105. 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
  106. 155 => 'Profiling timer expired',
  107. // 156 - not defined
  108. 157 => 'Pollable event',
  109. // 158 - not defined
  110. 159 => 'Bad syscall',
  111. );
  112. /**
  113. * @param string $commandline The command line to run
  114. * @param string|null $cwd The working directory or null to use the working dir of the current PHP process
  115. * @param array|null $env The environment variables or null to use the same environment as the current PHP process
  116. * @param string|null $input The input
  117. * @param int|float|null $timeout The timeout in seconds or null to disable
  118. * @param array $options An array of options for proc_open
  119. *
  120. * @throws RuntimeException When proc_open is not installed
  121. */
  122. public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array())
  123. {
  124. if (!\function_exists('proc_open')) {
  125. throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.');
  126. }
  127. $this->commandline = $commandline;
  128. $this->cwd = $cwd;
  129. // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started
  130. // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected
  131. // @see : https://bugs.php.net/bug.php?id=51800
  132. // @see : https://bugs.php.net/bug.php?id=50524
  133. if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) {
  134. $this->cwd = getcwd();
  135. }
  136. if (null !== $env) {
  137. $this->setEnv($env);
  138. }
  139. $this->setInput($input);
  140. $this->setTimeout($timeout);
  141. $this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR;
  142. $this->pty = false;
  143. $this->enhanceSigchildCompatibility = '\\' !== \DIRECTORY_SEPARATOR && $this->isSigchildEnabled();
  144. $this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options);
  145. }
  146. public function __destruct()
  147. {
  148. $this->stop(0);
  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 callable|null $callback A PHP callback to run whenever there is some
  165. * output available on STDOUT or STDERR
  166. *
  167. * @return int The exit status code
  168. *
  169. * @throws RuntimeException When process can't be launched
  170. * @throws RuntimeException When process stopped after receiving signal
  171. * @throws LogicException In case a callback is provided and output has been disabled
  172. */
  173. public function run($callback = null)
  174. {
  175. $this->start($callback);
  176. return $this->wait();
  177. }
  178. /**
  179. * Runs the process.
  180. *
  181. * This is identical to run() except that an exception is thrown if the process
  182. * exits with a non-zero exit code.
  183. *
  184. * @param callable|null $callback
  185. *
  186. * @return self
  187. *
  188. * @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled
  189. * @throws ProcessFailedException if the process didn't terminate successfully
  190. */
  191. public function mustRun($callback = null)
  192. {
  193. if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
  194. throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
  195. }
  196. if (0 !== $this->run($callback)) {
  197. throw new ProcessFailedException($this);
  198. }
  199. return $this;
  200. }
  201. /**
  202. * Starts the process and returns after writing the input to STDIN.
  203. *
  204. * This method blocks until all STDIN data is sent to the process then it
  205. * returns while the process runs in the background.
  206. *
  207. * The termination of the process can be awaited with wait().
  208. *
  209. * The callback receives the type of output (out or err) and some bytes from
  210. * the output in real-time while writing the standard input to the process.
  211. * It allows to have feedback from the independent process during execution.
  212. *
  213. * @param callable|null $callback A PHP callback to run whenever there is some
  214. * output available on STDOUT or STDERR
  215. *
  216. * @throws RuntimeException When process can't be launched
  217. * @throws RuntimeException When process is already running
  218. * @throws LogicException In case a callback is provided and output has been disabled
  219. */
  220. public function start($callback = null)
  221. {
  222. if ($this->isRunning()) {
  223. throw new RuntimeException('Process is already running');
  224. }
  225. if ($this->outputDisabled && null !== $callback) {
  226. throw new LogicException('Output has been disabled, enable it to allow the use of a callback.');
  227. }
  228. $this->resetProcessData();
  229. $this->starttime = $this->lastOutputTime = microtime(true);
  230. $this->callback = $this->buildCallback($callback);
  231. $descriptors = $this->getDescriptors();
  232. $commandline = $this->commandline;
  233. if ('\\' === \DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) {
  234. $commandline = 'cmd /V:ON /E:ON /D /C "('.$commandline.')';
  235. foreach ($this->processPipes->getFiles() as $offset => $filename) {
  236. $commandline .= ' '.$offset.'>'.ProcessUtils::escapeArgument($filename);
  237. }
  238. $commandline .= '"';
  239. if (!isset($this->options['bypass_shell'])) {
  240. $this->options['bypass_shell'] = true;
  241. }
  242. } elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
  243. // last exit code is output on the fourth pipe and caught to work around --enable-sigchild
  244. $descriptors[3] = array('pipe', 'w');
  245. // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input
  246. $commandline = '{ ('.$this->commandline.') <&3 3<&- 3>/dev/null & } 3<&0;';
  247. $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code';
  248. // Workaround for the bug, when PTS functionality is enabled.
  249. // @see : https://bugs.php.net/69442
  250. $ptsWorkaround = fopen(__FILE__, 'r');
  251. }
  252. $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options);
  253. if (!\is_resource($this->process)) {
  254. throw new RuntimeException('Unable to launch a new process.');
  255. }
  256. $this->status = self::STATUS_STARTED;
  257. if (isset($descriptors[3])) {
  258. $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]);
  259. }
  260. if ($this->tty) {
  261. return;
  262. }
  263. $this->updateStatus(false);
  264. $this->checkTimeout();
  265. }
  266. /**
  267. * Restarts the process.
  268. *
  269. * Be warned that the process is cloned before being started.
  270. *
  271. * @param callable|null $callback A PHP callback to run whenever there is some
  272. * output available on STDOUT or STDERR
  273. *
  274. * @return $this
  275. *
  276. * @throws RuntimeException When process can't be launched
  277. * @throws RuntimeException When process is already running
  278. *
  279. * @see start()
  280. */
  281. public function restart($callback = null)
  282. {
  283. if ($this->isRunning()) {
  284. throw new RuntimeException('Process is already running');
  285. }
  286. $process = clone $this;
  287. $process->start($callback);
  288. return $process;
  289. }
  290. /**
  291. * Waits for the process to terminate.
  292. *
  293. * The callback receives the type of output (out or err) and some bytes
  294. * from the output in real-time while writing the standard input to the process.
  295. * It allows to have feedback from the independent process during execution.
  296. *
  297. * @param callable|null $callback A valid PHP callback
  298. *
  299. * @return int The exitcode of the process
  300. *
  301. * @throws RuntimeException When process timed out
  302. * @throws RuntimeException When process stopped after receiving signal
  303. * @throws LogicException When process is not yet started
  304. */
  305. public function wait($callback = null)
  306. {
  307. $this->requireProcessIsStarted(__FUNCTION__);
  308. $this->updateStatus(false);
  309. if (null !== $callback) {
  310. $this->callback = $this->buildCallback($callback);
  311. }
  312. do {
  313. $this->checkTimeout();
  314. $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
  315. $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
  316. } while ($running);
  317. while ($this->isRunning()) {
  318. usleep(1000);
  319. }
  320. if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
  321. throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig']));
  322. }
  323. return $this->exitcode;
  324. }
  325. /**
  326. * Returns the Pid (process identifier), if applicable.
  327. *
  328. * @return int|null The process id if running, null otherwise
  329. */
  330. public function getPid()
  331. {
  332. return $this->isRunning() ? $this->processInformation['pid'] : null;
  333. }
  334. /**
  335. * Sends a POSIX signal to the process.
  336. *
  337. * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php)
  338. *
  339. * @return $this
  340. *
  341. * @throws LogicException In case the process is not running
  342. * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
  343. * @throws RuntimeException In case of failure
  344. */
  345. public function signal($signal)
  346. {
  347. $this->doSignal($signal, true);
  348. return $this;
  349. }
  350. /**
  351. * Disables fetching output and error output from the underlying process.
  352. *
  353. * @return $this
  354. *
  355. * @throws RuntimeException In case the process is already running
  356. * @throws LogicException if an idle timeout is set
  357. */
  358. public function disableOutput()
  359. {
  360. if ($this->isRunning()) {
  361. throw new RuntimeException('Disabling output while the process is running is not possible.');
  362. }
  363. if (null !== $this->idleTimeout) {
  364. throw new LogicException('Output can not be disabled while an idle timeout is set.');
  365. }
  366. $this->outputDisabled = true;
  367. return $this;
  368. }
  369. /**
  370. * Enables fetching output and error output from the underlying process.
  371. *
  372. * @return $this
  373. *
  374. * @throws RuntimeException In case the process is already running
  375. */
  376. public function enableOutput()
  377. {
  378. if ($this->isRunning()) {
  379. throw new RuntimeException('Enabling output while the process is running is not possible.');
  380. }
  381. $this->outputDisabled = false;
  382. return $this;
  383. }
  384. /**
  385. * Returns true in case the output is disabled, false otherwise.
  386. *
  387. * @return bool
  388. */
  389. public function isOutputDisabled()
  390. {
  391. return $this->outputDisabled;
  392. }
  393. /**
  394. * Returns the current output of the process (STDOUT).
  395. *
  396. * @return string The process output
  397. *
  398. * @throws LogicException in case the output has been disabled
  399. * @throws LogicException In case the process is not started
  400. */
  401. public function getOutput()
  402. {
  403. $this->readPipesForOutput(__FUNCTION__);
  404. if (false === $ret = stream_get_contents($this->stdout, -1, 0)) {
  405. return '';
  406. }
  407. return $ret;
  408. }
  409. /**
  410. * Returns the output incrementally.
  411. *
  412. * In comparison with the getOutput method which always return the whole
  413. * output, this one returns the new output since the last call.
  414. *
  415. * @return string The process output since the last call
  416. *
  417. * @throws LogicException in case the output has been disabled
  418. * @throws LogicException In case the process is not started
  419. */
  420. public function getIncrementalOutput()
  421. {
  422. $this->readPipesForOutput(__FUNCTION__);
  423. $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
  424. $this->incrementalOutputOffset = ftell($this->stdout);
  425. if (false === $latest) {
  426. return '';
  427. }
  428. return $latest;
  429. }
  430. /**
  431. * Clears the process output.
  432. *
  433. * @return $this
  434. */
  435. public function clearOutput()
  436. {
  437. ftruncate($this->stdout, 0);
  438. fseek($this->stdout, 0);
  439. $this->incrementalOutputOffset = 0;
  440. return $this;
  441. }
  442. /**
  443. * Returns the current error output of the process (STDERR).
  444. *
  445. * @return string The process error output
  446. *
  447. * @throws LogicException in case the output has been disabled
  448. * @throws LogicException In case the process is not started
  449. */
  450. public function getErrorOutput()
  451. {
  452. $this->readPipesForOutput(__FUNCTION__);
  453. if (false === $ret = stream_get_contents($this->stderr, -1, 0)) {
  454. return '';
  455. }
  456. return $ret;
  457. }
  458. /**
  459. * Returns the errorOutput incrementally.
  460. *
  461. * In comparison with the getErrorOutput method which always return the
  462. * whole error output, this one returns the new error output since the last
  463. * call.
  464. *
  465. * @return string The process error output since the last call
  466. *
  467. * @throws LogicException in case the output has been disabled
  468. * @throws LogicException In case the process is not started
  469. */
  470. public function getIncrementalErrorOutput()
  471. {
  472. $this->readPipesForOutput(__FUNCTION__);
  473. $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
  474. $this->incrementalErrorOutputOffset = ftell($this->stderr);
  475. if (false === $latest) {
  476. return '';
  477. }
  478. return $latest;
  479. }
  480. /**
  481. * Clears the process output.
  482. *
  483. * @return $this
  484. */
  485. public function clearErrorOutput()
  486. {
  487. ftruncate($this->stderr, 0);
  488. fseek($this->stderr, 0);
  489. $this->incrementalErrorOutputOffset = 0;
  490. return $this;
  491. }
  492. /**
  493. * Returns the exit code returned by the process.
  494. *
  495. * @return int|null The exit status code, null if the Process is not terminated
  496. *
  497. * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled
  498. */
  499. public function getExitCode()
  500. {
  501. if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
  502. throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
  503. }
  504. $this->updateStatus(false);
  505. return $this->exitcode;
  506. }
  507. /**
  508. * Returns a string representation for the exit code returned by the process.
  509. *
  510. * This method relies on the Unix exit code status standardization
  511. * and might not be relevant for other operating systems.
  512. *
  513. * @return string|null A string representation for the exit status code, null if the Process is not terminated
  514. *
  515. * @see http://tldp.org/LDP/abs/html/exitcodes.html
  516. * @see http://en.wikipedia.org/wiki/Unix_signal
  517. */
  518. public function getExitCodeText()
  519. {
  520. if (null === $exitcode = $this->getExitCode()) {
  521. return;
  522. }
  523. return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error';
  524. }
  525. /**
  526. * Checks if the process ended successfully.
  527. *
  528. * @return bool true if the process ended successfully, false otherwise
  529. */
  530. public function isSuccessful()
  531. {
  532. return 0 === $this->getExitCode();
  533. }
  534. /**
  535. * Returns true if the child process has been terminated by an uncaught signal.
  536. *
  537. * It always returns false on Windows.
  538. *
  539. * @return bool
  540. *
  541. * @throws RuntimeException In case --enable-sigchild is activated
  542. * @throws LogicException In case the process is not terminated
  543. */
  544. public function hasBeenSignaled()
  545. {
  546. $this->requireProcessIsTerminated(__FUNCTION__);
  547. if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
  548. throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
  549. }
  550. return $this->processInformation['signaled'];
  551. }
  552. /**
  553. * Returns the number of the signal that caused the child process to terminate its execution.
  554. *
  555. * It is only meaningful if hasBeenSignaled() returns true.
  556. *
  557. * @return int
  558. *
  559. * @throws RuntimeException In case --enable-sigchild is activated
  560. * @throws LogicException In case the process is not terminated
  561. */
  562. public function getTermSignal()
  563. {
  564. $this->requireProcessIsTerminated(__FUNCTION__);
  565. if ($this->isSigchildEnabled() && (!$this->enhanceSigchildCompatibility || -1 === $this->processInformation['termsig'])) {
  566. throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
  567. }
  568. return $this->processInformation['termsig'];
  569. }
  570. /**
  571. * Returns true if the child process has been stopped by a signal.
  572. *
  573. * It always returns false on Windows.
  574. *
  575. * @return bool
  576. *
  577. * @throws LogicException In case the process is not terminated
  578. */
  579. public function hasBeenStopped()
  580. {
  581. $this->requireProcessIsTerminated(__FUNCTION__);
  582. return $this->processInformation['stopped'];
  583. }
  584. /**
  585. * Returns the number of the signal that caused the child process to stop its execution.
  586. *
  587. * It is only meaningful if hasBeenStopped() returns true.
  588. *
  589. * @return int
  590. *
  591. * @throws LogicException In case the process is not terminated
  592. */
  593. public function getStopSignal()
  594. {
  595. $this->requireProcessIsTerminated(__FUNCTION__);
  596. return $this->processInformation['stopsig'];
  597. }
  598. /**
  599. * Checks if the process is currently running.
  600. *
  601. * @return bool true if the process is currently running, false otherwise
  602. */
  603. public function isRunning()
  604. {
  605. if (self::STATUS_STARTED !== $this->status) {
  606. return false;
  607. }
  608. $this->updateStatus(false);
  609. return $this->processInformation['running'];
  610. }
  611. /**
  612. * Checks if the process has been started with no regard to the current state.
  613. *
  614. * @return bool true if status is ready, false otherwise
  615. */
  616. public function isStarted()
  617. {
  618. return self::STATUS_READY != $this->status;
  619. }
  620. /**
  621. * Checks if the process is terminated.
  622. *
  623. * @return bool true if process is terminated, false otherwise
  624. */
  625. public function isTerminated()
  626. {
  627. $this->updateStatus(false);
  628. return self::STATUS_TERMINATED == $this->status;
  629. }
  630. /**
  631. * Gets the process status.
  632. *
  633. * The status is one of: ready, started, terminated.
  634. *
  635. * @return string The current process status
  636. */
  637. public function getStatus()
  638. {
  639. $this->updateStatus(false);
  640. return $this->status;
  641. }
  642. /**
  643. * Stops the process.
  644. *
  645. * @param int|float $timeout The timeout in seconds
  646. * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
  647. *
  648. * @return int The exit-code of the process
  649. */
  650. public function stop($timeout = 10, $signal = null)
  651. {
  652. $timeoutMicro = microtime(true) + $timeout;
  653. if ($this->isRunning()) {
  654. // given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here
  655. $this->doSignal(15, false);
  656. do {
  657. usleep(1000);
  658. } while ($this->isRunning() && microtime(true) < $timeoutMicro);
  659. if ($this->isRunning()) {
  660. // Avoid exception here: process is supposed to be running, but it might have stopped just
  661. // after this line. In any case, let's silently discard the error, we cannot do anything.
  662. $this->doSignal($signal ?: 9, false);
  663. }
  664. }
  665. if ($this->isRunning()) {
  666. if (isset($this->fallbackStatus['pid'])) {
  667. unset($this->fallbackStatus['pid']);
  668. return $this->stop(0, $signal);
  669. }
  670. $this->close();
  671. }
  672. return $this->exitcode;
  673. }
  674. /**
  675. * Adds a line to the STDOUT stream.
  676. *
  677. * @internal
  678. *
  679. * @param string $line The line to append
  680. */
  681. public function addOutput($line)
  682. {
  683. $this->lastOutputTime = microtime(true);
  684. fseek($this->stdout, 0, SEEK_END);
  685. fwrite($this->stdout, $line);
  686. fseek($this->stdout, $this->incrementalOutputOffset);
  687. }
  688. /**
  689. * Adds a line to the STDERR stream.
  690. *
  691. * @internal
  692. *
  693. * @param string $line The line to append
  694. */
  695. public function addErrorOutput($line)
  696. {
  697. $this->lastOutputTime = microtime(true);
  698. fseek($this->stderr, 0, SEEK_END);
  699. fwrite($this->stderr, $line);
  700. fseek($this->stderr, $this->incrementalErrorOutputOffset);
  701. }
  702. /**
  703. * Gets the command line to be executed.
  704. *
  705. * @return string The command to execute
  706. */
  707. public function getCommandLine()
  708. {
  709. return $this->commandline;
  710. }
  711. /**
  712. * Sets the command line to be executed.
  713. *
  714. * @param string $commandline The command to execute
  715. *
  716. * @return self The current Process instance
  717. */
  718. public function setCommandLine($commandline)
  719. {
  720. $this->commandline = $commandline;
  721. return $this;
  722. }
  723. /**
  724. * Gets the process timeout (max. runtime).
  725. *
  726. * @return float|null The timeout in seconds or null if it's disabled
  727. */
  728. public function getTimeout()
  729. {
  730. return $this->timeout;
  731. }
  732. /**
  733. * Gets the process idle timeout (max. time since last output).
  734. *
  735. * @return float|null The timeout in seconds or null if it's disabled
  736. */
  737. public function getIdleTimeout()
  738. {
  739. return $this->idleTimeout;
  740. }
  741. /**
  742. * Sets the process timeout (max. runtime).
  743. *
  744. * To disable the timeout, set this value to null.
  745. *
  746. * @param int|float|null $timeout The timeout in seconds
  747. *
  748. * @return self The current Process instance
  749. *
  750. * @throws InvalidArgumentException if the timeout is negative
  751. */
  752. public function setTimeout($timeout)
  753. {
  754. $this->timeout = $this->validateTimeout($timeout);
  755. return $this;
  756. }
  757. /**
  758. * Sets the process idle timeout (max. time since last output).
  759. *
  760. * To disable the timeout, set this value to null.
  761. *
  762. * @param int|float|null $timeout The timeout in seconds
  763. *
  764. * @return self The current Process instance
  765. *
  766. * @throws LogicException if the output is disabled
  767. * @throws InvalidArgumentException if the timeout is negative
  768. */
  769. public function setIdleTimeout($timeout)
  770. {
  771. if (null !== $timeout && $this->outputDisabled) {
  772. throw new LogicException('Idle timeout can not be set while the output is disabled.');
  773. }
  774. $this->idleTimeout = $this->validateTimeout($timeout);
  775. return $this;
  776. }
  777. /**
  778. * Enables or disables the TTY mode.
  779. *
  780. * @param bool $tty True to enabled and false to disable
  781. *
  782. * @return self The current Process instance
  783. *
  784. * @throws RuntimeException In case the TTY mode is not supported
  785. */
  786. public function setTty($tty)
  787. {
  788. if ('\\' === \DIRECTORY_SEPARATOR && $tty) {
  789. throw new RuntimeException('TTY mode is not supported on Windows platform.');
  790. }
  791. if ($tty) {
  792. static $isTtySupported;
  793. if (null === $isTtySupported) {
  794. $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', array(array('file', '/dev/tty', 'r'), array('file', '/dev/tty', 'w'), array('file', '/dev/tty', 'w')), $pipes);
  795. }
  796. if (!$isTtySupported) {
  797. throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.');
  798. }
  799. }
  800. $this->tty = (bool) $tty;
  801. return $this;
  802. }
  803. /**
  804. * Checks if the TTY mode is enabled.
  805. *
  806. * @return bool true if the TTY mode is enabled, false otherwise
  807. */
  808. public function isTty()
  809. {
  810. return $this->tty;
  811. }
  812. /**
  813. * Sets PTY mode.
  814. *
  815. * @param bool $bool
  816. *
  817. * @return self
  818. */
  819. public function setPty($bool)
  820. {
  821. $this->pty = (bool) $bool;
  822. return $this;
  823. }
  824. /**
  825. * Returns PTY state.
  826. *
  827. * @return bool
  828. */
  829. public function isPty()
  830. {
  831. return $this->pty;
  832. }
  833. /**
  834. * Gets the working directory.
  835. *
  836. * @return string|null The current working directory or null on failure
  837. */
  838. public function getWorkingDirectory()
  839. {
  840. if (null === $this->cwd) {
  841. // getcwd() will return false if any one of the parent directories does not have
  842. // the readable or search mode set, even if the current directory does
  843. return getcwd() ?: null;
  844. }
  845. return $this->cwd;
  846. }
  847. /**
  848. * Sets the current working directory.
  849. *
  850. * @param string $cwd The new working directory
  851. *
  852. * @return self The current Process instance
  853. */
  854. public function setWorkingDirectory($cwd)
  855. {
  856. $this->cwd = $cwd;
  857. return $this;
  858. }
  859. /**
  860. * Gets the environment variables.
  861. *
  862. * @return array The current environment variables
  863. */
  864. public function getEnv()
  865. {
  866. return $this->env;
  867. }
  868. /**
  869. * Sets the environment variables.
  870. *
  871. * Each environment variable value should be a string.
  872. * If it is an array, the variable is ignored.
  873. *
  874. * That happens in PHP when 'argv' is registered into
  875. * the $_ENV array for instance.
  876. *
  877. * @param array $env The new environment variables
  878. *
  879. * @return self The current Process instance
  880. */
  881. public function setEnv(array $env)
  882. {
  883. // Process can not handle env values that are arrays
  884. $env = array_filter($env, function ($value) {
  885. return !\is_array($value);
  886. });
  887. $this->env = array();
  888. foreach ($env as $key => $value) {
  889. $this->env[$key] = (string) $value;
  890. }
  891. return $this;
  892. }
  893. /**
  894. * Gets the contents of STDIN.
  895. *
  896. * @return string|null The current contents
  897. *
  898. * @deprecated since version 2.5, to be removed in 3.0.
  899. * Use setInput() instead.
  900. * This method is deprecated in favor of getInput.
  901. */
  902. public function getStdin()
  903. {
  904. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.5 and will be removed in 3.0. Use the getInput() method instead.', E_USER_DEPRECATED);
  905. return $this->getInput();
  906. }
  907. /**
  908. * Gets the Process input.
  909. *
  910. * @return string|null The Process input
  911. */
  912. public function getInput()
  913. {
  914. return $this->input;
  915. }
  916. /**
  917. * Sets the contents of STDIN.
  918. *
  919. * @param string|null $stdin The new contents
  920. *
  921. * @return self The current Process instance
  922. *
  923. * @deprecated since version 2.5, to be removed in 3.0.
  924. * Use setInput() instead.
  925. *
  926. * @throws LogicException In case the process is running
  927. * @throws InvalidArgumentException In case the argument is invalid
  928. */
  929. public function setStdin($stdin)
  930. {
  931. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.5 and will be removed in 3.0. Use the setInput() method instead.', E_USER_DEPRECATED);
  932. return $this->setInput($stdin);
  933. }
  934. /**
  935. * Sets the input.
  936. *
  937. * This content will be passed to the underlying process standard input.
  938. *
  939. * @param mixed $input The content
  940. *
  941. * @return self The current Process instance
  942. *
  943. * @throws LogicException In case the process is running
  944. *
  945. * Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0.
  946. */
  947. public function setInput($input)
  948. {
  949. if ($this->isRunning()) {
  950. throw new LogicException('Input can not be set while the process is running.');
  951. }
  952. $this->input = ProcessUtils::validateInput(__METHOD__, $input);
  953. return $this;
  954. }
  955. /**
  956. * Gets the options for proc_open.
  957. *
  958. * @return array The current options
  959. */
  960. public function getOptions()
  961. {
  962. return $this->options;
  963. }
  964. /**
  965. * Sets the options for proc_open.
  966. *
  967. * @param array $options The new options
  968. *
  969. * @return self The current Process instance
  970. */
  971. public function setOptions(array $options)
  972. {
  973. $this->options = $options;
  974. return $this;
  975. }
  976. /**
  977. * Gets whether or not Windows compatibility is enabled.
  978. *
  979. * This is true by default.
  980. *
  981. * @return bool
  982. */
  983. public function getEnhanceWindowsCompatibility()
  984. {
  985. return $this->enhanceWindowsCompatibility;
  986. }
  987. /**
  988. * Sets whether or not Windows compatibility is enabled.
  989. *
  990. * @param bool $enhance
  991. *
  992. * @return self The current Process instance
  993. */
  994. public function setEnhanceWindowsCompatibility($enhance)
  995. {
  996. $this->enhanceWindowsCompatibility = (bool) $enhance;
  997. return $this;
  998. }
  999. /**
  1000. * Returns whether sigchild compatibility mode is activated or not.
  1001. *
  1002. * @return bool
  1003. */
  1004. public function getEnhanceSigchildCompatibility()
  1005. {
  1006. return $this->enhanceSigchildCompatibility;
  1007. }
  1008. /**
  1009. * Activates sigchild compatibility mode.
  1010. *
  1011. * Sigchild compatibility mode is required to get the exit code and
  1012. * determine the success of a process when PHP has been compiled with
  1013. * the --enable-sigchild option
  1014. *
  1015. * @param bool $enhance
  1016. *
  1017. * @return self The current Process instance
  1018. */
  1019. public function setEnhanceSigchildCompatibility($enhance)
  1020. {
  1021. $this->enhanceSigchildCompatibility = (bool) $enhance;
  1022. return $this;
  1023. }
  1024. /**
  1025. * Performs a check between the timeout definition and the time the process started.
  1026. *
  1027. * In case you run a background process (with the start method), you should
  1028. * trigger this method regularly to ensure the process timeout
  1029. *
  1030. * @throws ProcessTimedOutException In case the timeout was reached
  1031. */
  1032. public function checkTimeout()
  1033. {
  1034. if (self::STATUS_STARTED !== $this->status) {
  1035. return;
  1036. }
  1037. if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
  1038. $this->stop(0);
  1039. throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL);
  1040. }
  1041. if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
  1042. $this->stop(0);
  1043. throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE);
  1044. }
  1045. }
  1046. /**
  1047. * Returns whether PTY is supported on the current operating system.
  1048. *
  1049. * @return bool
  1050. */
  1051. public static function isPtySupported()
  1052. {
  1053. static $result;
  1054. if (null !== $result) {
  1055. return $result;
  1056. }
  1057. if ('\\' === \DIRECTORY_SEPARATOR) {
  1058. return $result = false;
  1059. }
  1060. return $result = (bool) @proc_open('echo 1 >/dev/null', array(array('pty'), array('pty'), array('pty')), $pipes);
  1061. }
  1062. /**
  1063. * Creates the descriptors needed by the proc_open.
  1064. *
  1065. * @return array
  1066. */
  1067. private function getDescriptors()
  1068. {
  1069. if ('\\' === \DIRECTORY_SEPARATOR) {
  1070. $this->processPipes = WindowsPipes::create($this, $this->input);
  1071. } else {
  1072. $this->processPipes = UnixPipes::create($this, $this->input);
  1073. }
  1074. return $this->processPipes->getDescriptors();
  1075. }
  1076. /**
  1077. * Builds up the callback used by wait().
  1078. *
  1079. * The callbacks adds all occurred output to the specific buffer and calls
  1080. * the user callback (if present) with the received output.
  1081. *
  1082. * @param callable|null $callback The user defined PHP callback
  1083. *
  1084. * @return \Closure A PHP closure
  1085. */
  1086. protected function buildCallback($callback)
  1087. {
  1088. $that = $this;
  1089. $out = self::OUT;
  1090. $callback = function ($type, $data) use ($that, $callback, $out) {
  1091. if ($out == $type) {
  1092. $that->addOutput($data);
  1093. } else {
  1094. $that->addErrorOutput($data);
  1095. }
  1096. if (null !== $callback) {
  1097. \call_user_func($callback, $type, $data);
  1098. }
  1099. };
  1100. return $callback;
  1101. }
  1102. /**
  1103. * Updates the status of the process, reads pipes.
  1104. *
  1105. * @param bool $blocking Whether to use a blocking read call
  1106. */
  1107. protected function updateStatus($blocking)
  1108. {
  1109. if (self::STATUS_STARTED !== $this->status) {
  1110. return;
  1111. }
  1112. $this->processInformation = proc_get_status($this->process);
  1113. $running = $this->processInformation['running'];
  1114. $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running);
  1115. if ($this->fallbackStatus && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
  1116. $this->processInformation = $this->fallbackStatus + $this->processInformation;
  1117. }
  1118. if (!$running) {
  1119. $this->close();
  1120. }
  1121. }
  1122. /**
  1123. * Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
  1124. *
  1125. * @return bool
  1126. */
  1127. protected function isSigchildEnabled()
  1128. {
  1129. if (null !== self::$sigchild) {
  1130. return self::$sigchild;
  1131. }
  1132. if (!\function_exists('phpinfo') || \defined('HHVM_VERSION')) {
  1133. return self::$sigchild = false;
  1134. }
  1135. ob_start();
  1136. phpinfo(INFO_GENERAL);
  1137. return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
  1138. }
  1139. /**
  1140. * Reads pipes for the freshest output.
  1141. *
  1142. * @param string $caller The name of the method that needs fresh outputs
  1143. *
  1144. * @throws LogicException in case output has been disabled or process is not started
  1145. */
  1146. private function readPipesForOutput($caller)
  1147. {
  1148. if ($this->outputDisabled) {
  1149. throw new LogicException('Output has been disabled.');
  1150. }
  1151. $this->requireProcessIsStarted($caller);
  1152. $this->updateStatus(false);
  1153. }
  1154. /**
  1155. * Validates and returns the filtered timeout.
  1156. *
  1157. * @param int|float|null $timeout
  1158. *
  1159. * @return float|null
  1160. *
  1161. * @throws InvalidArgumentException if the given timeout is a negative number
  1162. */
  1163. private function validateTimeout($timeout)
  1164. {
  1165. $timeout = (float) $timeout;
  1166. if (0.0 === $timeout) {
  1167. $timeout = null;
  1168. } elseif ($timeout < 0) {
  1169. throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
  1170. }
  1171. return $timeout;
  1172. }
  1173. /**
  1174. * Reads pipes, executes callback.
  1175. *
  1176. * @param bool $blocking Whether to use blocking calls or not
  1177. * @param bool $close Whether to close file handles or not
  1178. */
  1179. private function readPipes($blocking, $close)
  1180. {
  1181. $result = $this->processPipes->readAndWrite($blocking, $close);
  1182. $callback = $this->callback;
  1183. foreach ($result as $type => $data) {
  1184. if (3 !== $type) {
  1185. $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data);
  1186. } elseif (!isset($this->fallbackStatus['signaled'])) {
  1187. $this->fallbackStatus['exitcode'] = (int) $data;
  1188. }
  1189. }
  1190. }
  1191. /**
  1192. * Closes process resource, closes file handles, sets the exitcode.
  1193. *
  1194. * @return int The exitcode
  1195. */
  1196. private function close()
  1197. {
  1198. $this->processPipes->close();
  1199. if (\is_resource($this->process)) {
  1200. proc_close($this->process);
  1201. }
  1202. $this->exitcode = $this->processInformation['exitcode'];
  1203. $this->status = self::STATUS_TERMINATED;
  1204. if (-1 === $this->exitcode) {
  1205. if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) {
  1206. // if process has been signaled, no exitcode but a valid termsig, apply Unix convention
  1207. $this->exitcode = 128 + $this->processInformation['termsig'];
  1208. } elseif ($this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
  1209. $this->processInformation['signaled'] = true;
  1210. $this->processInformation['termsig'] = -1;
  1211. }
  1212. }
  1213. // Free memory from self-reference callback created by buildCallback
  1214. // Doing so in other contexts like __destruct or by garbage collector is ineffective
  1215. // Now pipes are closed, so the callback is no longer necessary
  1216. $this->callback = null;
  1217. return $this->exitcode;
  1218. }
  1219. /**
  1220. * Resets data related to the latest run of the process.
  1221. */
  1222. private function resetProcessData()
  1223. {
  1224. $this->starttime = null;
  1225. $this->callback = null;
  1226. $this->exitcode = null;
  1227. $this->fallbackStatus = array();
  1228. $this->processInformation = null;
  1229. $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b');
  1230. $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b');
  1231. $this->process = null;
  1232. $this->latestSignal = null;
  1233. $this->status = self::STATUS_READY;
  1234. $this->incrementalOutputOffset = 0;
  1235. $this->incrementalErrorOutputOffset = 0;
  1236. }
  1237. /**
  1238. * Sends a POSIX signal to the process.
  1239. *
  1240. * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php)
  1241. * @param bool $throwException Whether to throw exception in case signal failed
  1242. *
  1243. * @return bool True if the signal was sent successfully, false otherwise
  1244. *
  1245. * @throws LogicException In case the process is not running
  1246. * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
  1247. * @throws RuntimeException In case of failure
  1248. */
  1249. private function doSignal($signal, $throwException)
  1250. {
  1251. if (null === $pid = $this->getPid()) {
  1252. if ($throwException) {
  1253. throw new LogicException('Can not send signal on a non running process.');
  1254. }
  1255. return false;
  1256. }
  1257. if ('\\' === \DIRECTORY_SEPARATOR) {
  1258. exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode);
  1259. if ($exitCode && $this->isRunning()) {
  1260. if ($throwException) {
  1261. throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output)));
  1262. }
  1263. return false;
  1264. }
  1265. } else {
  1266. if (!$this->enhanceSigchildCompatibility || !$this->isSigchildEnabled()) {
  1267. $ok = @proc_terminate($this->process, $signal);
  1268. } elseif (\function_exists('posix_kill')) {
  1269. $ok = @posix_kill($pid, $signal);
  1270. } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), array(2 => array('pipe', 'w')), $pipes)) {
  1271. $ok = false === fgets($pipes[2]);
  1272. }
  1273. if (!$ok) {
  1274. if ($throwException) {
  1275. throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal));
  1276. }
  1277. return false;
  1278. }
  1279. }
  1280. $this->latestSignal = (int) $signal;
  1281. $this->fallbackStatus['signaled'] = true;
  1282. $this->fallbackStatus['exitcode'] = -1;
  1283. $this->fallbackStatus['termsig'] = $this->latestSignal;
  1284. return true;
  1285. }
  1286. /**
  1287. * Ensures the process is running or terminated, throws a LogicException if the process has a not started.
  1288. *
  1289. * @param string $functionName The function name that was called
  1290. *
  1291. * @throws LogicException if the process has not run
  1292. */
  1293. private function requireProcessIsStarted($functionName)
  1294. {
  1295. if (!$this->isStarted()) {
  1296. throw new LogicException(sprintf('Process must be started before calling %s.', $functionName));
  1297. }
  1298. }
  1299. /**
  1300. * Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`.
  1301. *
  1302. * @param string $functionName The function name that was called
  1303. *
  1304. * @throws LogicException if the process is not yet terminated
  1305. */
  1306. private function requireProcessIsTerminated($functionName)
  1307. {
  1308. if (!$this->isTerminated()) {
  1309. throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName));
  1310. }
  1311. }
  1312. }