PluginProbe
ManageWP Worker / 4.9.25
ManageWP Worker v4.9.25
4.9.38 4.9.37 4.9.36 4.9.35 4.9.34 3.8.7 3.8.8 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.18 3.9.19 3.9.2 3.9.20 3.9.21 3.9.22 3.9.23 3.9.24 All 73 releases
worker / src / Symfony / Process / Process.php

Process.php in ManageWP Worker 4.9.25, at src/Symfony/Process/Process.php

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