Projekt

Obecné

Profil

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