PluginProbe
ManageWP Worker / trunk
ManageWP Worker vtrunk
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 trunk, at src/Symfony/Process/Process.php

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