PluginProbe
UpStream: a Project Management Plugin for WordPress / trunk
UpStream: a Project Management Plugin for WordPress vtrunk
trunk 1.39.0 1.39.1 1.39.2 1.39.3 2.0.7 2.1.0
upstream / vendor / symfony / process / Process.php

Process.php in UpStream: a Project Management Plugin for WordPress trunk, at vendor/symfony/process/Process.php

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