1<?php
2/*
3 * This file is part of PHPUnit.
4 *
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11/**
12 * Default utility for PHP sub-processes.
13 */
14class PHPUnit_Util_PHP_Default extends PHPUnit_Util_PHP
15{
16    /**
17     * @var string
18     */
19    protected $tempFile;
20
21    /**
22     * @var bool
23     */
24    protected $useTempFile = false;
25
26    /**
27     * Runs a single job (PHP code) using a separate PHP process.
28     *
29     * @param string $job
30     * @param array  $settings
31     *
32     * @return array
33     *
34     * @throws PHPUnit_Framework_Exception
35     */
36    public function runJob($job, array $settings = [])
37    {
38        if ($this->useTempFile || $this->stdin) {
39            if (!($this->tempFile = tempnam(sys_get_temp_dir(), 'PHPUnit')) ||
40                file_put_contents($this->tempFile, $job) === false) {
41                throw new PHPUnit_Framework_Exception(
42                    'Unable to write temporary file'
43                );
44            }
45
46            $job = $this->stdin;
47        }
48
49        return $this->runProcess($job, $settings);
50    }
51
52    /**
53     * Returns an array of file handles to be used in place of pipes
54     *
55     * @return array
56     */
57    protected function getHandles()
58    {
59        return [];
60    }
61
62    /**
63     * Handles creating the child process and returning the STDOUT and STDERR
64     *
65     * @param string $job
66     * @param array  $settings
67     *
68     * @return array
69     *
70     * @throws PHPUnit_Framework_Exception
71     */
72    protected function runProcess($job, $settings)
73    {
74        $handles = $this->getHandles();
75
76        $env = null;
77        if ($this->env) {
78            $env = isset($_SERVER) ? $_SERVER : [];
79            unset($env['argv'], $env['argc']);
80            $env = array_merge($env, $this->env);
81
82            foreach ($env as $envKey => $envVar) {
83                if (is_array($envVar)) {
84                    unset($env[$envKey]);
85                }
86            }
87        }
88
89        $pipeSpec = [
90            0 => isset($handles[0]) ? $handles[0] : ['pipe', 'r'],
91            1 => isset($handles[1]) ? $handles[1] : ['pipe', 'w'],
92            2 => isset($handles[2]) ? $handles[2] : ['pipe', 'w'],
93        ];
94        $process = proc_open(
95            $this->getCommand($settings, $this->tempFile),
96            $pipeSpec,
97            $pipes,
98            null,
99            $env
100        );
101
102        if (!is_resource($process)) {
103            throw new PHPUnit_Framework_Exception(
104                'Unable to spawn worker process'
105            );
106        }
107
108        if ($job) {
109            $this->process($pipes[0], $job);
110        }
111        fclose($pipes[0]);
112
113        if ($this->timeout) {
114            $stderr = $stdout = '';
115            unset($pipes[0]);
116
117            while (true) {
118                $r = $pipes;
119                $w = null;
120                $e = null;
121
122                $n = @stream_select($r, $w, $e, $this->timeout);
123
124                if ($n === false) {
125                    break;
126                } elseif ($n === 0) {
127                    proc_terminate($process, 9);
128                    throw new PHPUnit_Framework_Exception(sprintf('Job execution aborted after %d seconds', $this->timeout));
129                } elseif ($n > 0) {
130                    foreach ($r as $pipe) {
131                        $pipeOffset = 0;
132                        foreach ($pipes as $i => $origPipe) {
133                            if ($pipe == $origPipe) {
134                                $pipeOffset = $i;
135                                break;
136                            }
137                        }
138
139                        if (!$pipeOffset) {
140                            break;
141                        }
142
143                        $line = fread($pipe, 8192);
144                        if (strlen($line) == 0) {
145                            fclose($pipes[$pipeOffset]);
146                            unset($pipes[$pipeOffset]);
147                        } else {
148                            if ($pipeOffset == 1) {
149                                $stdout .= $line;
150                            } else {
151                                $stderr .= $line;
152                            }
153                        }
154                    }
155
156                    if (empty($pipes)) {
157                        break;
158                    }
159                }
160            }
161        } else {
162            if (isset($pipes[1])) {
163                $stdout = stream_get_contents($pipes[1]);
164                fclose($pipes[1]);
165            }
166
167            if (isset($pipes[2])) {
168                $stderr = stream_get_contents($pipes[2]);
169                fclose($pipes[2]);
170            }
171        }
172
173        if (isset($handles[1])) {
174            rewind($handles[1]);
175            $stdout = stream_get_contents($handles[1]);
176            fclose($handles[1]);
177        }
178
179        if (isset($handles[2])) {
180            rewind($handles[2]);
181            $stderr = stream_get_contents($handles[2]);
182            fclose($handles[2]);
183        }
184
185        proc_close($process);
186        $this->cleanup();
187
188        return ['stdout' => $stdout, 'stderr' => $stderr];
189    }
190
191    /**
192     * @param resource $pipe
193     * @param string   $job
194     *
195     * @throws PHPUnit_Framework_Exception
196     */
197    protected function process($pipe, $job)
198    {
199        fwrite($pipe, $job);
200    }
201
202    protected function cleanup()
203    {
204        if ($this->tempFile) {
205            unlink($this->tempFile);
206        }
207    }
208}
209