Mercurial > hg > cmmr2012-drupal-site
comparison vendor/symfony/process/Process.php @ 0:c75dbcec494b
Initial commit from drush-created site
author | Chris Cannam |
---|---|
date | Thu, 05 Jul 2018 14:24:15 +0000 |
parents | |
children | a9cd425dd02b |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:c75dbcec494b |
---|---|
1 <?php | |
2 | |
3 /* | |
4 * This file is part of the Symfony package. | |
5 * | |
6 * (c) Fabien Potencier <fabien@symfony.com> | |
7 * | |
8 * For the full copyright and license information, please view the LICENSE | |
9 * file that was distributed with this source code. | |
10 */ | |
11 | |
12 namespace Symfony\Component\Process; | |
13 | |
14 use Symfony\Component\Process\Exception\InvalidArgumentException; | |
15 use Symfony\Component\Process\Exception\LogicException; | |
16 use Symfony\Component\Process\Exception\ProcessFailedException; | |
17 use Symfony\Component\Process\Exception\ProcessTimedOutException; | |
18 use Symfony\Component\Process\Exception\RuntimeException; | |
19 use Symfony\Component\Process\Pipes\PipesInterface; | |
20 use Symfony\Component\Process\Pipes\UnixPipes; | |
21 use Symfony\Component\Process\Pipes\WindowsPipes; | |
22 | |
23 /** | |
24 * Process is a thin wrapper around proc_* functions to easily | |
25 * start independent PHP processes. | |
26 * | |
27 * @author Fabien Potencier <fabien@symfony.com> | |
28 * @author Romain Neutron <imprec@gmail.com> | |
29 */ | |
30 class Process implements \IteratorAggregate | |
31 { | |
32 const ERR = 'err'; | |
33 const OUT = 'out'; | |
34 | |
35 const STATUS_READY = 'ready'; | |
36 const STATUS_STARTED = 'started'; | |
37 const STATUS_TERMINATED = 'terminated'; | |
38 | |
39 const STDIN = 0; | |
40 const STDOUT = 1; | |
41 const STDERR = 2; | |
42 | |
43 // Timeout Precision in seconds. | |
44 const TIMEOUT_PRECISION = 0.2; | |
45 | |
46 const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking | |
47 const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory | |
48 const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating | |
49 const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating | |
50 | |
51 private $callback; | |
52 private $hasCallback = false; | |
53 private $commandline; | |
54 private $cwd; | |
55 private $env; | |
56 private $input; | |
57 private $starttime; | |
58 private $lastOutputTime; | |
59 private $timeout; | |
60 private $idleTimeout; | |
61 private $options = array('suppress_errors' => true); | |
62 private $exitcode; | |
63 private $fallbackStatus = array(); | |
64 private $processInformation; | |
65 private $outputDisabled = false; | |
66 private $stdout; | |
67 private $stderr; | |
68 private $enhanceWindowsCompatibility = true; | |
69 private $enhanceSigchildCompatibility; | |
70 private $process; | |
71 private $status = self::STATUS_READY; | |
72 private $incrementalOutputOffset = 0; | |
73 private $incrementalErrorOutputOffset = 0; | |
74 private $tty; | |
75 private $pty; | |
76 private $inheritEnv = false; | |
77 | |
78 private $useFileHandles = false; | |
79 /** @var PipesInterface */ | |
80 private $processPipes; | |
81 | |
82 private $latestSignal; | |
83 | |
84 private static $sigchild; | |
85 | |
86 /** | |
87 * Exit codes translation table. | |
88 * | |
89 * User-defined errors must use exit codes in the 64-113 range. | |
90 */ | |
91 public static $exitCodes = array( | |
92 0 => 'OK', | |
93 1 => 'General error', | |
94 2 => 'Misuse of shell builtins', | |
95 | |
96 126 => 'Invoked command cannot execute', | |
97 127 => 'Command not found', | |
98 128 => 'Invalid exit argument', | |
99 | |
100 // signals | |
101 129 => 'Hangup', | |
102 130 => 'Interrupt', | |
103 131 => 'Quit and dump core', | |
104 132 => 'Illegal instruction', | |
105 133 => 'Trace/breakpoint trap', | |
106 134 => 'Process aborted', | |
107 135 => 'Bus error: "access to undefined portion of memory object"', | |
108 136 => 'Floating point exception: "erroneous arithmetic operation"', | |
109 137 => 'Kill (terminate immediately)', | |
110 138 => 'User-defined 1', | |
111 139 => 'Segmentation violation', | |
112 140 => 'User-defined 2', | |
113 141 => 'Write to pipe with no one reading', | |
114 142 => 'Signal raised by alarm', | |
115 143 => 'Termination (request to terminate)', | |
116 // 144 - not defined | |
117 145 => 'Child process terminated, stopped (or continued*)', | |
118 146 => 'Continue if stopped', | |
119 147 => 'Stop executing temporarily', | |
120 148 => 'Terminal stop signal', | |
121 149 => 'Background process attempting to read from tty ("in")', | |
122 150 => 'Background process attempting to write to tty ("out")', | |
123 151 => 'Urgent data available on socket', | |
124 152 => 'CPU time limit exceeded', | |
125 153 => 'File size limit exceeded', | |
126 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', | |
127 155 => 'Profiling timer expired', | |
128 // 156 - not defined | |
129 157 => 'Pollable event', | |
130 // 158 - not defined | |
131 159 => 'Bad syscall', | |
132 ); | |
133 | |
134 /** | |
135 * @param string|array $commandline The command line to run | |
136 * @param string|null $cwd The working directory or null to use the working dir of the current PHP process | |
137 * @param array|null $env The environment variables or null to use the same environment as the current PHP process | |
138 * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input | |
139 * @param int|float|null $timeout The timeout in seconds or null to disable | |
140 * @param array $options An array of options for proc_open | |
141 * | |
142 * @throws RuntimeException When proc_open is not installed | |
143 */ | |
144 public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = null) | |
145 { | |
146 if (!function_exists('proc_open')) { | |
147 throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); | |
148 } | |
149 | |
150 $this->commandline = $commandline; | |
151 $this->cwd = $cwd; | |
152 | |
153 // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started | |
154 // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected | |
155 // @see : https://bugs.php.net/bug.php?id=51800 | |
156 // @see : https://bugs.php.net/bug.php?id=50524 | |
157 if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DIRECTORY_SEPARATOR)) { | |
158 $this->cwd = getcwd(); | |
159 } | |
160 if (null !== $env) { | |
161 $this->setEnv($env); | |
162 } | |
163 | |
164 $this->setInput($input); | |
165 $this->setTimeout($timeout); | |
166 $this->useFileHandles = '\\' === DIRECTORY_SEPARATOR; | |
167 $this->pty = false; | |
168 $this->enhanceSigchildCompatibility = '\\' !== DIRECTORY_SEPARATOR && $this->isSigchildEnabled(); | |
169 if (null !== $options) { | |
170 @trigger_error(sprintf('The $options parameter of the %s constructor is deprecated since Symfony 3.3 and will be removed in 4.0.', __CLASS__), E_USER_DEPRECATED); | |
171 $this->options = array_replace($this->options, $options); | |
172 } | |
173 } | |
174 | |
175 public function __destruct() | |
176 { | |
177 $this->stop(0); | |
178 } | |
179 | |
180 public function __clone() | |
181 { | |
182 $this->resetProcessData(); | |
183 } | |
184 | |
185 /** | |
186 * Runs the process. | |
187 * | |
188 * The callback receives the type of output (out or err) and | |
189 * some bytes from the output in real-time. It allows to have feedback | |
190 * from the independent process during execution. | |
191 * | |
192 * The STDOUT and STDERR are also available after the process is finished | |
193 * via the getOutput() and getErrorOutput() methods. | |
194 * | |
195 * @param callable|null $callback A PHP callback to run whenever there is some | |
196 * output available on STDOUT or STDERR | |
197 * @param array $env An array of additional env vars to set when running the process | |
198 * | |
199 * @return int The exit status code | |
200 * | |
201 * @throws RuntimeException When process can't be launched | |
202 * @throws RuntimeException When process stopped after receiving signal | |
203 * @throws LogicException In case a callback is provided and output has been disabled | |
204 * | |
205 * @final since version 3.3 | |
206 */ | |
207 public function run($callback = null/*, array $env = array()*/) | |
208 { | |
209 $env = 1 < func_num_args() ? func_get_arg(1) : null; | |
210 $this->start($callback, $env); | |
211 | |
212 return $this->wait(); | |
213 } | |
214 | |
215 /** | |
216 * Runs the process. | |
217 * | |
218 * This is identical to run() except that an exception is thrown if the process | |
219 * exits with a non-zero exit code. | |
220 * | |
221 * @param callable|null $callback | |
222 * @param array $env An array of additional env vars to set when running the process | |
223 * | |
224 * @return self | |
225 * | |
226 * @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled | |
227 * @throws ProcessFailedException if the process didn't terminate successfully | |
228 * | |
229 * @final since version 3.3 | |
230 */ | |
231 public function mustRun(callable $callback = null/*, array $env = array()*/) | |
232 { | |
233 if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { | |
234 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); | |
235 } | |
236 $env = 1 < func_num_args() ? func_get_arg(1) : null; | |
237 | |
238 if (0 !== $this->run($callback, $env)) { | |
239 throw new ProcessFailedException($this); | |
240 } | |
241 | |
242 return $this; | |
243 } | |
244 | |
245 /** | |
246 * Starts the process and returns after writing the input to STDIN. | |
247 * | |
248 * This method blocks until all STDIN data is sent to the process then it | |
249 * returns while the process runs in the background. | |
250 * | |
251 * The termination of the process can be awaited with wait(). | |
252 * | |
253 * The callback receives the type of output (out or err) and some bytes from | |
254 * the output in real-time while writing the standard input to the process. | |
255 * It allows to have feedback from the independent process during execution. | |
256 * | |
257 * @param callable|null $callback A PHP callback to run whenever there is some | |
258 * output available on STDOUT or STDERR | |
259 * @param array $env An array of additional env vars to set when running the process | |
260 * | |
261 * @throws RuntimeException When process can't be launched | |
262 * @throws RuntimeException When process is already running | |
263 * @throws LogicException In case a callback is provided and output has been disabled | |
264 */ | |
265 public function start(callable $callback = null/*, array $env = array()*/) | |
266 { | |
267 if ($this->isRunning()) { | |
268 throw new RuntimeException('Process is already running'); | |
269 } | |
270 if (2 <= func_num_args()) { | |
271 $env = func_get_arg(1); | |
272 } else { | |
273 if (__CLASS__ !== static::class) { | |
274 $r = new \ReflectionMethod($this, __FUNCTION__); | |
275 if (__CLASS__ !== $r->getDeclaringClass()->getName() && (2 > $r->getNumberOfParameters() || 'env' !== $r->getParameters()[0]->name)) { | |
276 @trigger_error(sprintf('The %s::start() method expects a second "$env" argument since Symfony 3.3. It will be made mandatory in 4.0.', static::class), E_USER_DEPRECATED); | |
277 } | |
278 } | |
279 $env = null; | |
280 } | |
281 | |
282 $this->resetProcessData(); | |
283 $this->starttime = $this->lastOutputTime = microtime(true); | |
284 $this->callback = $this->buildCallback($callback); | |
285 $this->hasCallback = null !== $callback; | |
286 $descriptors = $this->getDescriptors(); | |
287 $inheritEnv = $this->inheritEnv; | |
288 | |
289 if (is_array($commandline = $this->commandline)) { | |
290 $commandline = implode(' ', array_map(array($this, 'escapeArgument'), $commandline)); | |
291 | |
292 if ('\\' !== DIRECTORY_SEPARATOR) { | |
293 // exec is mandatory to deal with sending a signal to the process | |
294 $commandline = 'exec '.$commandline; | |
295 } | |
296 } | |
297 | |
298 if (null === $env) { | |
299 $env = $this->env; | |
300 } else { | |
301 if ($this->env) { | |
302 $env += $this->env; | |
303 } | |
304 $inheritEnv = true; | |
305 } | |
306 | |
307 if (null !== $env && $inheritEnv) { | |
308 $env += $this->getDefaultEnv(); | |
309 } elseif (null !== $env) { | |
310 @trigger_error('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', E_USER_DEPRECATED); | |
311 } else { | |
312 $env = $this->getDefaultEnv(); | |
313 } | |
314 if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) { | |
315 $this->options['bypass_shell'] = true; | |
316 $commandline = $this->prepareWindowsCommandLine($commandline, $env); | |
317 } elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { | |
318 // last exit code is output on the fourth pipe and caught to work around --enable-sigchild | |
319 $descriptors[3] = array('pipe', 'w'); | |
320 | |
321 // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input | |
322 $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; | |
323 $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code'; | |
324 | |
325 // Workaround for the bug, when PTS functionality is enabled. | |
326 // @see : https://bugs.php.net/69442 | |
327 $ptsWorkaround = fopen(__FILE__, 'r'); | |
328 } | |
329 if (defined('HHVM_VERSION')) { | |
330 $envPairs = $env; | |
331 } else { | |
332 $envPairs = array(); | |
333 foreach ($env as $k => $v) { | |
334 if (false !== $v) { | |
335 $envPairs[] = $k.'='.$v; | |
336 } | |
337 } | |
338 } | |
339 | |
340 if (!is_dir($this->cwd)) { | |
341 @trigger_error('The provided cwd does not exist. Command is currently ran against getcwd(). This behavior is deprecated since Symfony 3.4 and will be removed in 4.0.', E_USER_DEPRECATED); | |
342 } | |
343 | |
344 $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); | |
345 | |
346 if (!is_resource($this->process)) { | |
347 throw new RuntimeException('Unable to launch a new process.'); | |
348 } | |
349 $this->status = self::STATUS_STARTED; | |
350 | |
351 if (isset($descriptors[3])) { | |
352 $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]); | |
353 } | |
354 | |
355 if ($this->tty) { | |
356 return; | |
357 } | |
358 | |
359 $this->updateStatus(false); | |
360 $this->checkTimeout(); | |
361 } | |
362 | |
363 /** | |
364 * Restarts the process. | |
365 * | |
366 * Be warned that the process is cloned before being started. | |
367 * | |
368 * @param callable|null $callback A PHP callback to run whenever there is some | |
369 * output available on STDOUT or STDERR | |
370 * @param array $env An array of additional env vars to set when running the process | |
371 * | |
372 * @return $this | |
373 * | |
374 * @throws RuntimeException When process can't be launched | |
375 * @throws RuntimeException When process is already running | |
376 * | |
377 * @see start() | |
378 * | |
379 * @final since version 3.3 | |
380 */ | |
381 public function restart(callable $callback = null/*, array $env = array()*/) | |
382 { | |
383 if ($this->isRunning()) { | |
384 throw new RuntimeException('Process is already running'); | |
385 } | |
386 $env = 1 < func_num_args() ? func_get_arg(1) : null; | |
387 | |
388 $process = clone $this; | |
389 $process->start($callback, $env); | |
390 | |
391 return $process; | |
392 } | |
393 | |
394 /** | |
395 * Waits for the process to terminate. | |
396 * | |
397 * The callback receives the type of output (out or err) and some bytes | |
398 * from the output in real-time while writing the standard input to the process. | |
399 * It allows to have feedback from the independent process during execution. | |
400 * | |
401 * @param callable|null $callback A valid PHP callback | |
402 * | |
403 * @return int The exitcode of the process | |
404 * | |
405 * @throws RuntimeException When process timed out | |
406 * @throws RuntimeException When process stopped after receiving signal | |
407 * @throws LogicException When process is not yet started | |
408 */ | |
409 public function wait(callable $callback = null) | |
410 { | |
411 $this->requireProcessIsStarted(__FUNCTION__); | |
412 | |
413 $this->updateStatus(false); | |
414 | |
415 if (null !== $callback) { | |
416 if (!$this->processPipes->haveReadSupport()) { | |
417 $this->stop(0); | |
418 throw new \LogicException('Pass the callback to the Process::start method or enableOutput to use a callback with Process::wait'); | |
419 } | |
420 $this->callback = $this->buildCallback($callback); | |
421 } | |
422 | |
423 do { | |
424 $this->checkTimeout(); | |
425 $running = '\\' === DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); | |
426 $this->readPipes($running, '\\' !== DIRECTORY_SEPARATOR || !$running); | |
427 } while ($running); | |
428 | |
429 while ($this->isRunning()) { | |
430 usleep(1000); | |
431 } | |
432 | |
433 if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { | |
434 throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); | |
435 } | |
436 | |
437 return $this->exitcode; | |
438 } | |
439 | |
440 /** | |
441 * Returns the Pid (process identifier), if applicable. | |
442 * | |
443 * @return int|null The process id if running, null otherwise | |
444 */ | |
445 public function getPid() | |
446 { | |
447 return $this->isRunning() ? $this->processInformation['pid'] : null; | |
448 } | |
449 | |
450 /** | |
451 * Sends a POSIX signal to the process. | |
452 * | |
453 * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php) | |
454 * | |
455 * @return $this | |
456 * | |
457 * @throws LogicException In case the process is not running | |
458 * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed | |
459 * @throws RuntimeException In case of failure | |
460 */ | |
461 public function signal($signal) | |
462 { | |
463 $this->doSignal($signal, true); | |
464 | |
465 return $this; | |
466 } | |
467 | |
468 /** | |
469 * Disables fetching output and error output from the underlying process. | |
470 * | |
471 * @return $this | |
472 * | |
473 * @throws RuntimeException In case the process is already running | |
474 * @throws LogicException if an idle timeout is set | |
475 */ | |
476 public function disableOutput() | |
477 { | |
478 if ($this->isRunning()) { | |
479 throw new RuntimeException('Disabling output while the process is running is not possible.'); | |
480 } | |
481 if (null !== $this->idleTimeout) { | |
482 throw new LogicException('Output can not be disabled while an idle timeout is set.'); | |
483 } | |
484 | |
485 $this->outputDisabled = true; | |
486 | |
487 return $this; | |
488 } | |
489 | |
490 /** | |
491 * Enables fetching output and error output from the underlying process. | |
492 * | |
493 * @return $this | |
494 * | |
495 * @throws RuntimeException In case the process is already running | |
496 */ | |
497 public function enableOutput() | |
498 { | |
499 if ($this->isRunning()) { | |
500 throw new RuntimeException('Enabling output while the process is running is not possible.'); | |
501 } | |
502 | |
503 $this->outputDisabled = false; | |
504 | |
505 return $this; | |
506 } | |
507 | |
508 /** | |
509 * Returns true in case the output is disabled, false otherwise. | |
510 * | |
511 * @return bool | |
512 */ | |
513 public function isOutputDisabled() | |
514 { | |
515 return $this->outputDisabled; | |
516 } | |
517 | |
518 /** | |
519 * Returns the current output of the process (STDOUT). | |
520 * | |
521 * @return string The process output | |
522 * | |
523 * @throws LogicException in case the output has been disabled | |
524 * @throws LogicException In case the process is not started | |
525 */ | |
526 public function getOutput() | |
527 { | |
528 $this->readPipesForOutput(__FUNCTION__); | |
529 | |
530 if (false === $ret = stream_get_contents($this->stdout, -1, 0)) { | |
531 return ''; | |
532 } | |
533 | |
534 return $ret; | |
535 } | |
536 | |
537 /** | |
538 * Returns the output incrementally. | |
539 * | |
540 * In comparison with the getOutput method which always return the whole | |
541 * output, this one returns the new output since the last call. | |
542 * | |
543 * @return string The process output since the last call | |
544 * | |
545 * @throws LogicException in case the output has been disabled | |
546 * @throws LogicException In case the process is not started | |
547 */ | |
548 public function getIncrementalOutput() | |
549 { | |
550 $this->readPipesForOutput(__FUNCTION__); | |
551 | |
552 $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); | |
553 $this->incrementalOutputOffset = ftell($this->stdout); | |
554 | |
555 if (false === $latest) { | |
556 return ''; | |
557 } | |
558 | |
559 return $latest; | |
560 } | |
561 | |
562 /** | |
563 * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR). | |
564 * | |
565 * @param int $flags A bit field of Process::ITER_* flags | |
566 * | |
567 * @throws LogicException in case the output has been disabled | |
568 * @throws LogicException In case the process is not started | |
569 * | |
570 * @return \Generator | |
571 */ | |
572 public function getIterator($flags = 0) | |
573 { | |
574 $this->readPipesForOutput(__FUNCTION__, false); | |
575 | |
576 $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags); | |
577 $blocking = !(self::ITER_NON_BLOCKING & $flags); | |
578 $yieldOut = !(self::ITER_SKIP_OUT & $flags); | |
579 $yieldErr = !(self::ITER_SKIP_ERR & $flags); | |
580 | |
581 while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) { | |
582 if ($yieldOut) { | |
583 $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); | |
584 | |
585 if (isset($out[0])) { | |
586 if ($clearOutput) { | |
587 $this->clearOutput(); | |
588 } else { | |
589 $this->incrementalOutputOffset = ftell($this->stdout); | |
590 } | |
591 | |
592 yield self::OUT => $out; | |
593 } | |
594 } | |
595 | |
596 if ($yieldErr) { | |
597 $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); | |
598 | |
599 if (isset($err[0])) { | |
600 if ($clearOutput) { | |
601 $this->clearErrorOutput(); | |
602 } else { | |
603 $this->incrementalErrorOutputOffset = ftell($this->stderr); | |
604 } | |
605 | |
606 yield self::ERR => $err; | |
607 } | |
608 } | |
609 | |
610 if (!$blocking && !isset($out[0]) && !isset($err[0])) { | |
611 yield self::OUT => ''; | |
612 } | |
613 | |
614 $this->checkTimeout(); | |
615 $this->readPipesForOutput(__FUNCTION__, $blocking); | |
616 } | |
617 } | |
618 | |
619 /** | |
620 * Clears the process output. | |
621 * | |
622 * @return $this | |
623 */ | |
624 public function clearOutput() | |
625 { | |
626 ftruncate($this->stdout, 0); | |
627 fseek($this->stdout, 0); | |
628 $this->incrementalOutputOffset = 0; | |
629 | |
630 return $this; | |
631 } | |
632 | |
633 /** | |
634 * Returns the current error output of the process (STDERR). | |
635 * | |
636 * @return string The process error output | |
637 * | |
638 * @throws LogicException in case the output has been disabled | |
639 * @throws LogicException In case the process is not started | |
640 */ | |
641 public function getErrorOutput() | |
642 { | |
643 $this->readPipesForOutput(__FUNCTION__); | |
644 | |
645 if (false === $ret = stream_get_contents($this->stderr, -1, 0)) { | |
646 return ''; | |
647 } | |
648 | |
649 return $ret; | |
650 } | |
651 | |
652 /** | |
653 * Returns the errorOutput incrementally. | |
654 * | |
655 * In comparison with the getErrorOutput method which always return the | |
656 * whole error output, this one returns the new error output since the last | |
657 * call. | |
658 * | |
659 * @return string The process error output since the last call | |
660 * | |
661 * @throws LogicException in case the output has been disabled | |
662 * @throws LogicException In case the process is not started | |
663 */ | |
664 public function getIncrementalErrorOutput() | |
665 { | |
666 $this->readPipesForOutput(__FUNCTION__); | |
667 | |
668 $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); | |
669 $this->incrementalErrorOutputOffset = ftell($this->stderr); | |
670 | |
671 if (false === $latest) { | |
672 return ''; | |
673 } | |
674 | |
675 return $latest; | |
676 } | |
677 | |
678 /** | |
679 * Clears the process output. | |
680 * | |
681 * @return $this | |
682 */ | |
683 public function clearErrorOutput() | |
684 { | |
685 ftruncate($this->stderr, 0); | |
686 fseek($this->stderr, 0); | |
687 $this->incrementalErrorOutputOffset = 0; | |
688 | |
689 return $this; | |
690 } | |
691 | |
692 /** | |
693 * Returns the exit code returned by the process. | |
694 * | |
695 * @return null|int The exit status code, null if the Process is not terminated | |
696 * | |
697 * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled | |
698 */ | |
699 public function getExitCode() | |
700 { | |
701 if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { | |
702 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); | |
703 } | |
704 | |
705 $this->updateStatus(false); | |
706 | |
707 return $this->exitcode; | |
708 } | |
709 | |
710 /** | |
711 * Returns a string representation for the exit code returned by the process. | |
712 * | |
713 * This method relies on the Unix exit code status standardization | |
714 * and might not be relevant for other operating systems. | |
715 * | |
716 * @return null|string A string representation for the exit status code, null if the Process is not terminated | |
717 * | |
718 * @see http://tldp.org/LDP/abs/html/exitcodes.html | |
719 * @see http://en.wikipedia.org/wiki/Unix_signal | |
720 */ | |
721 public function getExitCodeText() | |
722 { | |
723 if (null === $exitcode = $this->getExitCode()) { | |
724 return; | |
725 } | |
726 | |
727 return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; | |
728 } | |
729 | |
730 /** | |
731 * Checks if the process ended successfully. | |
732 * | |
733 * @return bool true if the process ended successfully, false otherwise | |
734 */ | |
735 public function isSuccessful() | |
736 { | |
737 return 0 === $this->getExitCode(); | |
738 } | |
739 | |
740 /** | |
741 * Returns true if the child process has been terminated by an uncaught signal. | |
742 * | |
743 * It always returns false on Windows. | |
744 * | |
745 * @return bool | |
746 * | |
747 * @throws RuntimeException In case --enable-sigchild is activated | |
748 * @throws LogicException In case the process is not terminated | |
749 */ | |
750 public function hasBeenSignaled() | |
751 { | |
752 $this->requireProcessIsTerminated(__FUNCTION__); | |
753 | |
754 if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { | |
755 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); | |
756 } | |
757 | |
758 return $this->processInformation['signaled']; | |
759 } | |
760 | |
761 /** | |
762 * Returns the number of the signal that caused the child process to terminate its execution. | |
763 * | |
764 * It is only meaningful if hasBeenSignaled() returns true. | |
765 * | |
766 * @return int | |
767 * | |
768 * @throws RuntimeException In case --enable-sigchild is activated | |
769 * @throws LogicException In case the process is not terminated | |
770 */ | |
771 public function getTermSignal() | |
772 { | |
773 $this->requireProcessIsTerminated(__FUNCTION__); | |
774 | |
775 if ($this->isSigchildEnabled() && (!$this->enhanceSigchildCompatibility || -1 === $this->processInformation['termsig'])) { | |
776 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); | |
777 } | |
778 | |
779 return $this->processInformation['termsig']; | |
780 } | |
781 | |
782 /** | |
783 * Returns true if the child process has been stopped by a signal. | |
784 * | |
785 * It always returns false on Windows. | |
786 * | |
787 * @return bool | |
788 * | |
789 * @throws LogicException In case the process is not terminated | |
790 */ | |
791 public function hasBeenStopped() | |
792 { | |
793 $this->requireProcessIsTerminated(__FUNCTION__); | |
794 | |
795 return $this->processInformation['stopped']; | |
796 } | |
797 | |
798 /** | |
799 * Returns the number of the signal that caused the child process to stop its execution. | |
800 * | |
801 * It is only meaningful if hasBeenStopped() returns true. | |
802 * | |
803 * @return int | |
804 * | |
805 * @throws LogicException In case the process is not terminated | |
806 */ | |
807 public function getStopSignal() | |
808 { | |
809 $this->requireProcessIsTerminated(__FUNCTION__); | |
810 | |
811 return $this->processInformation['stopsig']; | |
812 } | |
813 | |
814 /** | |
815 * Checks if the process is currently running. | |
816 * | |
817 * @return bool true if the process is currently running, false otherwise | |
818 */ | |
819 public function isRunning() | |
820 { | |
821 if (self::STATUS_STARTED !== $this->status) { | |
822 return false; | |
823 } | |
824 | |
825 $this->updateStatus(false); | |
826 | |
827 return $this->processInformation['running']; | |
828 } | |
829 | |
830 /** | |
831 * Checks if the process has been started with no regard to the current state. | |
832 * | |
833 * @return bool true if status is ready, false otherwise | |
834 */ | |
835 public function isStarted() | |
836 { | |
837 return self::STATUS_READY != $this->status; | |
838 } | |
839 | |
840 /** | |
841 * Checks if the process is terminated. | |
842 * | |
843 * @return bool true if process is terminated, false otherwise | |
844 */ | |
845 public function isTerminated() | |
846 { | |
847 $this->updateStatus(false); | |
848 | |
849 return self::STATUS_TERMINATED == $this->status; | |
850 } | |
851 | |
852 /** | |
853 * Gets the process status. | |
854 * | |
855 * The status is one of: ready, started, terminated. | |
856 * | |
857 * @return string The current process status | |
858 */ | |
859 public function getStatus() | |
860 { | |
861 $this->updateStatus(false); | |
862 | |
863 return $this->status; | |
864 } | |
865 | |
866 /** | |
867 * Stops the process. | |
868 * | |
869 * @param int|float $timeout The timeout in seconds | |
870 * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) | |
871 * | |
872 * @return int The exit-code of the process | |
873 */ | |
874 public function stop($timeout = 10, $signal = null) | |
875 { | |
876 $timeoutMicro = microtime(true) + $timeout; | |
877 if ($this->isRunning()) { | |
878 // given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here | |
879 $this->doSignal(15, false); | |
880 do { | |
881 usleep(1000); | |
882 } while ($this->isRunning() && microtime(true) < $timeoutMicro); | |
883 | |
884 if ($this->isRunning()) { | |
885 // Avoid exception here: process is supposed to be running, but it might have stopped just | |
886 // after this line. In any case, let's silently discard the error, we cannot do anything. | |
887 $this->doSignal($signal ?: 9, false); | |
888 } | |
889 } | |
890 | |
891 if ($this->isRunning()) { | |
892 if (isset($this->fallbackStatus['pid'])) { | |
893 unset($this->fallbackStatus['pid']); | |
894 | |
895 return $this->stop(0, $signal); | |
896 } | |
897 $this->close(); | |
898 } | |
899 | |
900 return $this->exitcode; | |
901 } | |
902 | |
903 /** | |
904 * Adds a line to the STDOUT stream. | |
905 * | |
906 * @internal | |
907 * | |
908 * @param string $line The line to append | |
909 */ | |
910 public function addOutput($line) | |
911 { | |
912 $this->lastOutputTime = microtime(true); | |
913 | |
914 fseek($this->stdout, 0, SEEK_END); | |
915 fwrite($this->stdout, $line); | |
916 fseek($this->stdout, $this->incrementalOutputOffset); | |
917 } | |
918 | |
919 /** | |
920 * Adds a line to the STDERR stream. | |
921 * | |
922 * @internal | |
923 * | |
924 * @param string $line The line to append | |
925 */ | |
926 public function addErrorOutput($line) | |
927 { | |
928 $this->lastOutputTime = microtime(true); | |
929 | |
930 fseek($this->stderr, 0, SEEK_END); | |
931 fwrite($this->stderr, $line); | |
932 fseek($this->stderr, $this->incrementalErrorOutputOffset); | |
933 } | |
934 | |
935 /** | |
936 * Gets the command line to be executed. | |
937 * | |
938 * @return string The command to execute | |
939 */ | |
940 public function getCommandLine() | |
941 { | |
942 return is_array($this->commandline) ? implode(' ', array_map(array($this, 'escapeArgument'), $this->commandline)) : $this->commandline; | |
943 } | |
944 | |
945 /** | |
946 * Sets the command line to be executed. | |
947 * | |
948 * @param string|array $commandline The command to execute | |
949 * | |
950 * @return self The current Process instance | |
951 */ | |
952 public function setCommandLine($commandline) | |
953 { | |
954 $this->commandline = $commandline; | |
955 | |
956 return $this; | |
957 } | |
958 | |
959 /** | |
960 * Gets the process timeout (max. runtime). | |
961 * | |
962 * @return float|null The timeout in seconds or null if it's disabled | |
963 */ | |
964 public function getTimeout() | |
965 { | |
966 return $this->timeout; | |
967 } | |
968 | |
969 /** | |
970 * Gets the process idle timeout (max. time since last output). | |
971 * | |
972 * @return float|null The timeout in seconds or null if it's disabled | |
973 */ | |
974 public function getIdleTimeout() | |
975 { | |
976 return $this->idleTimeout; | |
977 } | |
978 | |
979 /** | |
980 * Sets the process timeout (max. runtime). | |
981 * | |
982 * To disable the timeout, set this value to null. | |
983 * | |
984 * @param int|float|null $timeout The timeout in seconds | |
985 * | |
986 * @return self The current Process instance | |
987 * | |
988 * @throws InvalidArgumentException if the timeout is negative | |
989 */ | |
990 public function setTimeout($timeout) | |
991 { | |
992 $this->timeout = $this->validateTimeout($timeout); | |
993 | |
994 return $this; | |
995 } | |
996 | |
997 /** | |
998 * Sets the process idle timeout (max. time since last output). | |
999 * | |
1000 * To disable the timeout, set this value to null. | |
1001 * | |
1002 * @param int|float|null $timeout The timeout in seconds | |
1003 * | |
1004 * @return self The current Process instance | |
1005 * | |
1006 * @throws LogicException if the output is disabled | |
1007 * @throws InvalidArgumentException if the timeout is negative | |
1008 */ | |
1009 public function setIdleTimeout($timeout) | |
1010 { | |
1011 if (null !== $timeout && $this->outputDisabled) { | |
1012 throw new LogicException('Idle timeout can not be set while the output is disabled.'); | |
1013 } | |
1014 | |
1015 $this->idleTimeout = $this->validateTimeout($timeout); | |
1016 | |
1017 return $this; | |
1018 } | |
1019 | |
1020 /** | |
1021 * Enables or disables the TTY mode. | |
1022 * | |
1023 * @param bool $tty True to enabled and false to disable | |
1024 * | |
1025 * @return self The current Process instance | |
1026 * | |
1027 * @throws RuntimeException In case the TTY mode is not supported | |
1028 */ | |
1029 public function setTty($tty) | |
1030 { | |
1031 if ('\\' === DIRECTORY_SEPARATOR && $tty) { | |
1032 throw new RuntimeException('TTY mode is not supported on Windows platform.'); | |
1033 } | |
1034 if ($tty) { | |
1035 static $isTtySupported; | |
1036 | |
1037 if (null === $isTtySupported) { | |
1038 $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); | |
1039 } | |
1040 | |
1041 if (!$isTtySupported) { | |
1042 throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.'); | |
1043 } | |
1044 } | |
1045 | |
1046 $this->tty = (bool) $tty; | |
1047 | |
1048 return $this; | |
1049 } | |
1050 | |
1051 /** | |
1052 * Checks if the TTY mode is enabled. | |
1053 * | |
1054 * @return bool true if the TTY mode is enabled, false otherwise | |
1055 */ | |
1056 public function isTty() | |
1057 { | |
1058 return $this->tty; | |
1059 } | |
1060 | |
1061 /** | |
1062 * Sets PTY mode. | |
1063 * | |
1064 * @param bool $bool | |
1065 * | |
1066 * @return self | |
1067 */ | |
1068 public function setPty($bool) | |
1069 { | |
1070 $this->pty = (bool) $bool; | |
1071 | |
1072 return $this; | |
1073 } | |
1074 | |
1075 /** | |
1076 * Returns PTY state. | |
1077 * | |
1078 * @return bool | |
1079 */ | |
1080 public function isPty() | |
1081 { | |
1082 return $this->pty; | |
1083 } | |
1084 | |
1085 /** | |
1086 * Gets the working directory. | |
1087 * | |
1088 * @return string|null The current working directory or null on failure | |
1089 */ | |
1090 public function getWorkingDirectory() | |
1091 { | |
1092 if (null === $this->cwd) { | |
1093 // getcwd() will return false if any one of the parent directories does not have | |
1094 // the readable or search mode set, even if the current directory does | |
1095 return getcwd() ?: null; | |
1096 } | |
1097 | |
1098 return $this->cwd; | |
1099 } | |
1100 | |
1101 /** | |
1102 * Sets the current working directory. | |
1103 * | |
1104 * @param string $cwd The new working directory | |
1105 * | |
1106 * @return self The current Process instance | |
1107 */ | |
1108 public function setWorkingDirectory($cwd) | |
1109 { | |
1110 $this->cwd = $cwd; | |
1111 | |
1112 return $this; | |
1113 } | |
1114 | |
1115 /** | |
1116 * Gets the environment variables. | |
1117 * | |
1118 * @return array The current environment variables | |
1119 */ | |
1120 public function getEnv() | |
1121 { | |
1122 return $this->env; | |
1123 } | |
1124 | |
1125 /** | |
1126 * Sets the environment variables. | |
1127 * | |
1128 * Each environment variable value should be a string. | |
1129 * If it is an array, the variable is ignored. | |
1130 * If it is false or null, it will be removed when | |
1131 * env vars are otherwise inherited. | |
1132 * | |
1133 * That happens in PHP when 'argv' is registered into | |
1134 * the $_ENV array for instance. | |
1135 * | |
1136 * @param array $env The new environment variables | |
1137 * | |
1138 * @return self The current Process instance | |
1139 */ | |
1140 public function setEnv(array $env) | |
1141 { | |
1142 // Process can not handle env values that are arrays | |
1143 $env = array_filter($env, function ($value) { | |
1144 return !is_array($value); | |
1145 }); | |
1146 | |
1147 $this->env = $env; | |
1148 | |
1149 return $this; | |
1150 } | |
1151 | |
1152 /** | |
1153 * Gets the Process input. | |
1154 * | |
1155 * @return resource|string|\Iterator|null The Process input | |
1156 */ | |
1157 public function getInput() | |
1158 { | |
1159 return $this->input; | |
1160 } | |
1161 | |
1162 /** | |
1163 * Sets the input. | |
1164 * | |
1165 * This content will be passed to the underlying process standard input. | |
1166 * | |
1167 * @param string|int|float|bool|resource|\Traversable|null $input The content | |
1168 * | |
1169 * @return self The current Process instance | |
1170 * | |
1171 * @throws LogicException In case the process is running | |
1172 */ | |
1173 public function setInput($input) | |
1174 { | |
1175 if ($this->isRunning()) { | |
1176 throw new LogicException('Input can not be set while the process is running.'); | |
1177 } | |
1178 | |
1179 $this->input = ProcessUtils::validateInput(__METHOD__, $input); | |
1180 | |
1181 return $this; | |
1182 } | |
1183 | |
1184 /** | |
1185 * Gets the options for proc_open. | |
1186 * | |
1187 * @return array The current options | |
1188 * | |
1189 * @deprecated since version 3.3, to be removed in 4.0. | |
1190 */ | |
1191 public function getOptions() | |
1192 { | |
1193 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED); | |
1194 | |
1195 return $this->options; | |
1196 } | |
1197 | |
1198 /** | |
1199 * Sets the options for proc_open. | |
1200 * | |
1201 * @param array $options The new options | |
1202 * | |
1203 * @return self The current Process instance | |
1204 * | |
1205 * @deprecated since version 3.3, to be removed in 4.0. | |
1206 */ | |
1207 public function setOptions(array $options) | |
1208 { | |
1209 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED); | |
1210 | |
1211 $this->options = $options; | |
1212 | |
1213 return $this; | |
1214 } | |
1215 | |
1216 /** | |
1217 * Gets whether or not Windows compatibility is enabled. | |
1218 * | |
1219 * This is true by default. | |
1220 * | |
1221 * @return bool | |
1222 * | |
1223 * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled. | |
1224 */ | |
1225 public function getEnhanceWindowsCompatibility() | |
1226 { | |
1227 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED); | |
1228 | |
1229 return $this->enhanceWindowsCompatibility; | |
1230 } | |
1231 | |
1232 /** | |
1233 * Sets whether or not Windows compatibility is enabled. | |
1234 * | |
1235 * @param bool $enhance | |
1236 * | |
1237 * @return self The current Process instance | |
1238 * | |
1239 * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled. | |
1240 */ | |
1241 public function setEnhanceWindowsCompatibility($enhance) | |
1242 { | |
1243 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED); | |
1244 | |
1245 $this->enhanceWindowsCompatibility = (bool) $enhance; | |
1246 | |
1247 return $this; | |
1248 } | |
1249 | |
1250 /** | |
1251 * Returns whether sigchild compatibility mode is activated or not. | |
1252 * | |
1253 * @return bool | |
1254 * | |
1255 * @deprecated since version 3.3, to be removed in 4.0. Sigchild compatibility will always be enabled. | |
1256 */ | |
1257 public function getEnhanceSigchildCompatibility() | |
1258 { | |
1259 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED); | |
1260 | |
1261 return $this->enhanceSigchildCompatibility; | |
1262 } | |
1263 | |
1264 /** | |
1265 * Activates sigchild compatibility mode. | |
1266 * | |
1267 * Sigchild compatibility mode is required to get the exit code and | |
1268 * determine the success of a process when PHP has been compiled with | |
1269 * the --enable-sigchild option | |
1270 * | |
1271 * @param bool $enhance | |
1272 * | |
1273 * @return self The current Process instance | |
1274 * | |
1275 * @deprecated since version 3.3, to be removed in 4.0. | |
1276 */ | |
1277 public function setEnhanceSigchildCompatibility($enhance) | |
1278 { | |
1279 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED); | |
1280 | |
1281 $this->enhanceSigchildCompatibility = (bool) $enhance; | |
1282 | |
1283 return $this; | |
1284 } | |
1285 | |
1286 /** | |
1287 * Sets whether environment variables will be inherited or not. | |
1288 * | |
1289 * @param bool $inheritEnv | |
1290 * | |
1291 * @return self The current Process instance | |
1292 */ | |
1293 public function inheritEnvironmentVariables($inheritEnv = true) | |
1294 { | |
1295 if (!$inheritEnv) { | |
1296 @trigger_error('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', E_USER_DEPRECATED); | |
1297 } | |
1298 | |
1299 $this->inheritEnv = (bool) $inheritEnv; | |
1300 | |
1301 return $this; | |
1302 } | |
1303 | |
1304 /** | |
1305 * Returns whether environment variables will be inherited or not. | |
1306 * | |
1307 * @return bool | |
1308 * | |
1309 * @deprecated since version 3.3, to be removed in 4.0. Environment variables will always be inherited. | |
1310 */ | |
1311 public function areEnvironmentVariablesInherited() | |
1312 { | |
1313 @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Environment variables will always be inherited.', __METHOD__), E_USER_DEPRECATED); | |
1314 | |
1315 return $this->inheritEnv; | |
1316 } | |
1317 | |
1318 /** | |
1319 * Performs a check between the timeout definition and the time the process started. | |
1320 * | |
1321 * In case you run a background process (with the start method), you should | |
1322 * trigger this method regularly to ensure the process timeout | |
1323 * | |
1324 * @throws ProcessTimedOutException In case the timeout was reached | |
1325 */ | |
1326 public function checkTimeout() | |
1327 { | |
1328 if (self::STATUS_STARTED !== $this->status) { | |
1329 return; | |
1330 } | |
1331 | |
1332 if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { | |
1333 $this->stop(0); | |
1334 | |
1335 throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL); | |
1336 } | |
1337 | |
1338 if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { | |
1339 $this->stop(0); | |
1340 | |
1341 throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE); | |
1342 } | |
1343 } | |
1344 | |
1345 /** | |
1346 * Returns whether PTY is supported on the current operating system. | |
1347 * | |
1348 * @return bool | |
1349 */ | |
1350 public static function isPtySupported() | |
1351 { | |
1352 static $result; | |
1353 | |
1354 if (null !== $result) { | |
1355 return $result; | |
1356 } | |
1357 | |
1358 if ('\\' === DIRECTORY_SEPARATOR) { | |
1359 return $result = false; | |
1360 } | |
1361 | |
1362 return $result = (bool) @proc_open('echo 1 >/dev/null', array(array('pty'), array('pty'), array('pty')), $pipes); | |
1363 } | |
1364 | |
1365 /** | |
1366 * Creates the descriptors needed by the proc_open. | |
1367 * | |
1368 * @return array | |
1369 */ | |
1370 private function getDescriptors() | |
1371 { | |
1372 if ($this->input instanceof \Iterator) { | |
1373 $this->input->rewind(); | |
1374 } | |
1375 if ('\\' === DIRECTORY_SEPARATOR) { | |
1376 $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback); | |
1377 } else { | |
1378 $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback); | |
1379 } | |
1380 | |
1381 return $this->processPipes->getDescriptors(); | |
1382 } | |
1383 | |
1384 /** | |
1385 * Builds up the callback used by wait(). | |
1386 * | |
1387 * The callbacks adds all occurred output to the specific buffer and calls | |
1388 * the user callback (if present) with the received output. | |
1389 * | |
1390 * @param callable|null $callback The user defined PHP callback | |
1391 * | |
1392 * @return \Closure A PHP closure | |
1393 */ | |
1394 protected function buildCallback(callable $callback = null) | |
1395 { | |
1396 if ($this->outputDisabled) { | |
1397 return function ($type, $data) use ($callback) { | |
1398 if (null !== $callback) { | |
1399 call_user_func($callback, $type, $data); | |
1400 } | |
1401 }; | |
1402 } | |
1403 | |
1404 $out = self::OUT; | |
1405 | |
1406 return function ($type, $data) use ($callback, $out) { | |
1407 if ($out == $type) { | |
1408 $this->addOutput($data); | |
1409 } else { | |
1410 $this->addErrorOutput($data); | |
1411 } | |
1412 | |
1413 if (null !== $callback) { | |
1414 call_user_func($callback, $type, $data); | |
1415 } | |
1416 }; | |
1417 } | |
1418 | |
1419 /** | |
1420 * Updates the status of the process, reads pipes. | |
1421 * | |
1422 * @param bool $blocking Whether to use a blocking read call | |
1423 */ | |
1424 protected function updateStatus($blocking) | |
1425 { | |
1426 if (self::STATUS_STARTED !== $this->status) { | |
1427 return; | |
1428 } | |
1429 | |
1430 $this->processInformation = proc_get_status($this->process); | |
1431 $running = $this->processInformation['running']; | |
1432 | |
1433 $this->readPipes($running && $blocking, '\\' !== DIRECTORY_SEPARATOR || !$running); | |
1434 | |
1435 if ($this->fallbackStatus && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { | |
1436 $this->processInformation = $this->fallbackStatus + $this->processInformation; | |
1437 } | |
1438 | |
1439 if (!$running) { | |
1440 $this->close(); | |
1441 } | |
1442 } | |
1443 | |
1444 /** | |
1445 * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. | |
1446 * | |
1447 * @return bool | |
1448 */ | |
1449 protected function isSigchildEnabled() | |
1450 { | |
1451 if (null !== self::$sigchild) { | |
1452 return self::$sigchild; | |
1453 } | |
1454 | |
1455 if (!function_exists('phpinfo') || defined('HHVM_VERSION')) { | |
1456 return self::$sigchild = false; | |
1457 } | |
1458 | |
1459 ob_start(); | |
1460 phpinfo(INFO_GENERAL); | |
1461 | |
1462 return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); | |
1463 } | |
1464 | |
1465 /** | |
1466 * Reads pipes for the freshest output. | |
1467 * | |
1468 * @param string $caller The name of the method that needs fresh outputs | |
1469 * @param bool $blocking Whether to use blocking calls or not | |
1470 * | |
1471 * @throws LogicException in case output has been disabled or process is not started | |
1472 */ | |
1473 private function readPipesForOutput($caller, $blocking = false) | |
1474 { | |
1475 if ($this->outputDisabled) { | |
1476 throw new LogicException('Output has been disabled.'); | |
1477 } | |
1478 | |
1479 $this->requireProcessIsStarted($caller); | |
1480 | |
1481 $this->updateStatus($blocking); | |
1482 } | |
1483 | |
1484 /** | |
1485 * Validates and returns the filtered timeout. | |
1486 * | |
1487 * @param int|float|null $timeout | |
1488 * | |
1489 * @return float|null | |
1490 * | |
1491 * @throws InvalidArgumentException if the given timeout is a negative number | |
1492 */ | |
1493 private function validateTimeout($timeout) | |
1494 { | |
1495 $timeout = (float) $timeout; | |
1496 | |
1497 if (0.0 === $timeout) { | |
1498 $timeout = null; | |
1499 } elseif ($timeout < 0) { | |
1500 throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); | |
1501 } | |
1502 | |
1503 return $timeout; | |
1504 } | |
1505 | |
1506 /** | |
1507 * Reads pipes, executes callback. | |
1508 * | |
1509 * @param bool $blocking Whether to use blocking calls or not | |
1510 * @param bool $close Whether to close file handles or not | |
1511 */ | |
1512 private function readPipes($blocking, $close) | |
1513 { | |
1514 $result = $this->processPipes->readAndWrite($blocking, $close); | |
1515 | |
1516 $callback = $this->callback; | |
1517 foreach ($result as $type => $data) { | |
1518 if (3 !== $type) { | |
1519 $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); | |
1520 } elseif (!isset($this->fallbackStatus['signaled'])) { | |
1521 $this->fallbackStatus['exitcode'] = (int) $data; | |
1522 } | |
1523 } | |
1524 } | |
1525 | |
1526 /** | |
1527 * Closes process resource, closes file handles, sets the exitcode. | |
1528 * | |
1529 * @return int The exitcode | |
1530 */ | |
1531 private function close() | |
1532 { | |
1533 $this->processPipes->close(); | |
1534 if (is_resource($this->process)) { | |
1535 proc_close($this->process); | |
1536 } | |
1537 $this->exitcode = $this->processInformation['exitcode']; | |
1538 $this->status = self::STATUS_TERMINATED; | |
1539 | |
1540 if (-1 === $this->exitcode) { | |
1541 if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) { | |
1542 // if process has been signaled, no exitcode but a valid termsig, apply Unix convention | |
1543 $this->exitcode = 128 + $this->processInformation['termsig']; | |
1544 } elseif ($this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { | |
1545 $this->processInformation['signaled'] = true; | |
1546 $this->processInformation['termsig'] = -1; | |
1547 } | |
1548 } | |
1549 | |
1550 // Free memory from self-reference callback created by buildCallback | |
1551 // Doing so in other contexts like __destruct or by garbage collector is ineffective | |
1552 // Now pipes are closed, so the callback is no longer necessary | |
1553 $this->callback = null; | |
1554 | |
1555 return $this->exitcode; | |
1556 } | |
1557 | |
1558 /** | |
1559 * Resets data related to the latest run of the process. | |
1560 */ | |
1561 private function resetProcessData() | |
1562 { | |
1563 $this->starttime = null; | |
1564 $this->callback = null; | |
1565 $this->exitcode = null; | |
1566 $this->fallbackStatus = array(); | |
1567 $this->processInformation = null; | |
1568 $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'wb+'); | |
1569 $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'wb+'); | |
1570 $this->process = null; | |
1571 $this->latestSignal = null; | |
1572 $this->status = self::STATUS_READY; | |
1573 $this->incrementalOutputOffset = 0; | |
1574 $this->incrementalErrorOutputOffset = 0; | |
1575 } | |
1576 | |
1577 /** | |
1578 * Sends a POSIX signal to the process. | |
1579 * | |
1580 * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php) | |
1581 * @param bool $throwException Whether to throw exception in case signal failed | |
1582 * | |
1583 * @return bool True if the signal was sent successfully, false otherwise | |
1584 * | |
1585 * @throws LogicException In case the process is not running | |
1586 * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed | |
1587 * @throws RuntimeException In case of failure | |
1588 */ | |
1589 private function doSignal($signal, $throwException) | |
1590 { | |
1591 if (null === $pid = $this->getPid()) { | |
1592 if ($throwException) { | |
1593 throw new LogicException('Can not send signal on a non running process.'); | |
1594 } | |
1595 | |
1596 return false; | |
1597 } | |
1598 | |
1599 if ('\\' === DIRECTORY_SEPARATOR) { | |
1600 exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); | |
1601 if ($exitCode && $this->isRunning()) { | |
1602 if ($throwException) { | |
1603 throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output))); | |
1604 } | |
1605 | |
1606 return false; | |
1607 } | |
1608 } else { | |
1609 if (!$this->enhanceSigchildCompatibility || !$this->isSigchildEnabled()) { | |
1610 $ok = @proc_terminate($this->process, $signal); | |
1611 } elseif (function_exists('posix_kill')) { | |
1612 $ok = @posix_kill($pid, $signal); | |
1613 } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), array(2 => array('pipe', 'w')), $pipes)) { | |
1614 $ok = false === fgets($pipes[2]); | |
1615 } | |
1616 if (!$ok) { | |
1617 if ($throwException) { | |
1618 throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal)); | |
1619 } | |
1620 | |
1621 return false; | |
1622 } | |
1623 } | |
1624 | |
1625 $this->latestSignal = (int) $signal; | |
1626 $this->fallbackStatus['signaled'] = true; | |
1627 $this->fallbackStatus['exitcode'] = -1; | |
1628 $this->fallbackStatus['termsig'] = $this->latestSignal; | |
1629 | |
1630 return true; | |
1631 } | |
1632 | |
1633 private function prepareWindowsCommandLine($cmd, array &$env) | |
1634 { | |
1635 $uid = uniqid('', true); | |
1636 $varCount = 0; | |
1637 $varCache = array(); | |
1638 $cmd = preg_replace_callback( | |
1639 '/"(?:( | |
1640 [^"%!^]*+ | |
1641 (?: | |
1642 (?: !LF! | "(?:\^[%!^])?+" ) | |
1643 [^"%!^]*+ | |
1644 )++ | |
1645 ) | [^"]*+ )"/x', | |
1646 function ($m) use (&$env, &$varCache, &$varCount, $uid) { | |
1647 if (!isset($m[1])) { | |
1648 return $m[0]; | |
1649 } | |
1650 if (isset($varCache[$m[0]])) { | |
1651 return $varCache[$m[0]]; | |
1652 } | |
1653 if (false !== strpos($value = $m[1], "\0")) { | |
1654 $value = str_replace("\0", '?', $value); | |
1655 } | |
1656 if (false === strpbrk($value, "\"%!\n")) { | |
1657 return '"'.$value.'"'; | |
1658 } | |
1659 | |
1660 $value = str_replace(array('!LF!', '"^!"', '"^%"', '"^^"', '""'), array("\n", '!', '%', '^', '"'), $value); | |
1661 $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"'; | |
1662 $var = $uid.++$varCount; | |
1663 | |
1664 $env[$var] = $value; | |
1665 | |
1666 return $varCache[$m[0]] = '!'.$var.'!'; | |
1667 }, | |
1668 $cmd | |
1669 ); | |
1670 | |
1671 $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; | |
1672 foreach ($this->processPipes->getFiles() as $offset => $filename) { | |
1673 $cmd .= ' '.$offset.'>"'.$filename.'"'; | |
1674 } | |
1675 | |
1676 return $cmd; | |
1677 } | |
1678 | |
1679 /** | |
1680 * Ensures the process is running or terminated, throws a LogicException if the process has a not started. | |
1681 * | |
1682 * @param string $functionName The function name that was called | |
1683 * | |
1684 * @throws LogicException if the process has not run | |
1685 */ | |
1686 private function requireProcessIsStarted($functionName) | |
1687 { | |
1688 if (!$this->isStarted()) { | |
1689 throw new LogicException(sprintf('Process must be started before calling %s.', $functionName)); | |
1690 } | |
1691 } | |
1692 | |
1693 /** | |
1694 * Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`. | |
1695 * | |
1696 * @param string $functionName The function name that was called | |
1697 * | |
1698 * @throws LogicException if the process is not yet terminated | |
1699 */ | |
1700 private function requireProcessIsTerminated($functionName) | |
1701 { | |
1702 if (!$this->isTerminated()) { | |
1703 throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); | |
1704 } | |
1705 } | |
1706 | |
1707 /** | |
1708 * Escapes a string to be used as a shell argument. | |
1709 * | |
1710 * @param string $argument The argument that will be escaped | |
1711 * | |
1712 * @return string The escaped argument | |
1713 */ | |
1714 private function escapeArgument($argument) | |
1715 { | |
1716 if ('\\' !== DIRECTORY_SEPARATOR) { | |
1717 return "'".str_replace("'", "'\\''", $argument)."'"; | |
1718 } | |
1719 if ('' === $argument = (string) $argument) { | |
1720 return '""'; | |
1721 } | |
1722 if (false !== strpos($argument, "\0")) { | |
1723 $argument = str_replace("\0", '?', $argument); | |
1724 } | |
1725 if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) { | |
1726 return $argument; | |
1727 } | |
1728 $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); | |
1729 | |
1730 return '"'.str_replace(array('"', '^', '%', '!', "\n"), array('""', '"^^"', '"^%"', '"^!"', '!LF!'), $argument).'"'; | |
1731 } | |
1732 | |
1733 private function getDefaultEnv() | |
1734 { | |
1735 $env = array(); | |
1736 | |
1737 foreach ($_SERVER as $k => $v) { | |
1738 if (is_string($v) && false !== $v = getenv($k)) { | |
1739 $env[$k] = $v; | |
1740 } | |
1741 } | |
1742 | |
1743 foreach ($_ENV as $k => $v) { | |
1744 if (is_string($v)) { | |
1745 $env[$k] = $v; | |
1746 } | |
1747 } | |
1748 | |
1749 return $env; | |
1750 } | |
1751 } |