scratch – Blame information for rev 115

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