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 }