|
1 <?php |
|
2 |
|
3 /* |
|
4 * This file is part of the Symfony package. |
|
5 * |
|
6 * (c) Fabien Potencier <fabien@symfony.com> |
|
7 * |
|
8 * For the full copyright and license information, please view the LICENSE |
|
9 * file that was distributed with this source code. |
|
10 */ |
|
11 |
|
12 namespace Symfony\Component\Process; |
|
13 |
|
14 /** |
|
15 * Process is a thin wrapper around proc_* functions to ease |
|
16 * start independent PHP processes. |
|
17 * |
|
18 * @author Fabien Potencier <fabien@symfony.com> |
|
19 * |
|
20 * @api |
|
21 */ |
|
22 class Process |
|
23 { |
|
24 private $commandline; |
|
25 private $cwd; |
|
26 private $env; |
|
27 private $stdin; |
|
28 private $timeout; |
|
29 private $options; |
|
30 private $exitcode; |
|
31 private $status; |
|
32 private $stdout; |
|
33 private $stderr; |
|
34 |
|
35 /** |
|
36 * Constructor. |
|
37 * |
|
38 * @param string $commandline The command line to run |
|
39 * @param string $cwd The working directory |
|
40 * @param array $env The environment variables |
|
41 * @param string $stdin The STDIN content |
|
42 * @param integer $timeout The timeout in seconds |
|
43 * @param array $options An array of options for proc_open |
|
44 * |
|
45 * @throws \RuntimeException When proc_open is not installed |
|
46 * |
|
47 * @api |
|
48 */ |
|
49 public function __construct($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array()) |
|
50 { |
|
51 if (!function_exists('proc_open')) { |
|
52 throw new \RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); |
|
53 } |
|
54 |
|
55 $this->commandline = $commandline; |
|
56 $this->cwd = null === $cwd ? getcwd() : $cwd; |
|
57 if (null !== $env) { |
|
58 $this->env = array(); |
|
59 foreach ($env as $key => $value) { |
|
60 $this->env[(binary) $key] = (binary) $value; |
|
61 } |
|
62 } else { |
|
63 $this->env = null; |
|
64 } |
|
65 $this->stdin = $stdin; |
|
66 $this->timeout = $timeout; |
|
67 $this->options = array_merge(array('suppress_errors' => true, 'binary_pipes' => true, 'bypass_shell' => false), $options); |
|
68 } |
|
69 |
|
70 /** |
|
71 * Runs the process. |
|
72 * |
|
73 * The callback receives the type of output (out or err) and |
|
74 * some bytes from the output in real-time. It allows to have feedback |
|
75 * from the independent process during execution. |
|
76 * |
|
77 * The STDOUT and STDERR are also available after the process is finished |
|
78 * via the getOutput() and getErrorOutput() methods. |
|
79 * |
|
80 * @param Closure|string|array $callback A PHP callback to run whenever there is some |
|
81 * output available on STDOUT or STDERR |
|
82 * |
|
83 * @return integer The exit status code |
|
84 * |
|
85 * @throws \RuntimeException When process can't be launch or is stopped |
|
86 * |
|
87 * @api |
|
88 */ |
|
89 public function run($callback = null) |
|
90 { |
|
91 $this->stdout = ''; |
|
92 $this->stderr = ''; |
|
93 $that = $this; |
|
94 $callback = function ($type, $data) use ($that, $callback) |
|
95 { |
|
96 if ('out' == $type) { |
|
97 $that->addOutput($data); |
|
98 } else { |
|
99 $that->addErrorOutput($data); |
|
100 } |
|
101 |
|
102 if (null !== $callback) { |
|
103 call_user_func($callback, $type, $data); |
|
104 } |
|
105 }; |
|
106 |
|
107 $descriptors = array(array('pipe', 'r'), array('pipe', 'w'), array('pipe', 'w')); |
|
108 |
|
109 $process = proc_open($this->commandline, $descriptors, $pipes, $this->cwd, $this->env, $this->options); |
|
110 |
|
111 if (!is_resource($process)) { |
|
112 throw new \RuntimeException('Unable to launch a new process.'); |
|
113 } |
|
114 |
|
115 foreach ($pipes as $pipe) { |
|
116 stream_set_blocking($pipe, false); |
|
117 } |
|
118 |
|
119 if (null === $this->stdin) { |
|
120 fclose($pipes[0]); |
|
121 $writePipes = null; |
|
122 } else { |
|
123 $writePipes = array($pipes[0]); |
|
124 $stdinLen = strlen($this->stdin); |
|
125 $stdinOffset = 0; |
|
126 } |
|
127 unset($pipes[0]); |
|
128 |
|
129 while ($pipes || $writePipes) { |
|
130 $r = $pipes; |
|
131 $w = $writePipes; |
|
132 $e = null; |
|
133 |
|
134 $n = @stream_select($r, $w, $e, $this->timeout); |
|
135 |
|
136 if (false === $n) { |
|
137 break; |
|
138 } elseif ($n === 0) { |
|
139 proc_terminate($process); |
|
140 |
|
141 throw new \RuntimeException('The process timed out.'); |
|
142 } |
|
143 |
|
144 if ($w) { |
|
145 $written = fwrite($writePipes[0], (binary) substr($this->stdin, $stdinOffset), 8192); |
|
146 if (false !== $written) { |
|
147 $stdinOffset += $written; |
|
148 } |
|
149 if ($stdinOffset >= $stdinLen) { |
|
150 fclose($writePipes[0]); |
|
151 $writePipes = null; |
|
152 } |
|
153 } |
|
154 |
|
155 foreach ($r as $pipe) { |
|
156 $type = array_search($pipe, $pipes); |
|
157 $data = fread($pipe, 8192); |
|
158 if (strlen($data) > 0) { |
|
159 call_user_func($callback, $type == 1 ? 'out' : 'err', $data); |
|
160 } |
|
161 if (false === $data || feof($pipe)) { |
|
162 fclose($pipe); |
|
163 unset($pipes[$type]); |
|
164 } |
|
165 } |
|
166 } |
|
167 |
|
168 $this->status = proc_get_status($process); |
|
169 |
|
170 $time = 0; |
|
171 while (1 == $this->status['running'] && $time < 1000000) { |
|
172 $time += 1000; |
|
173 usleep(1000); |
|
174 $this->status = proc_get_status($process); |
|
175 } |
|
176 |
|
177 $exitcode = proc_close($process); |
|
178 |
|
179 if ($this->status['signaled']) { |
|
180 throw new \RuntimeException(sprintf('The process stopped because of a "%s" signal.', $this->status['stopsig'])); |
|
181 } |
|
182 |
|
183 return $this->exitcode = $this->status['running'] ? $exitcode : $this->status['exitcode']; |
|
184 } |
|
185 |
|
186 /** |
|
187 * Returns the output of the process (STDOUT). |
|
188 * |
|
189 * This only returns the output if you have not supplied a callback |
|
190 * to the run() method. |
|
191 * |
|
192 * @return string The process output |
|
193 * |
|
194 * @api |
|
195 */ |
|
196 public function getOutput() |
|
197 { |
|
198 return $this->stdout; |
|
199 } |
|
200 |
|
201 /** |
|
202 * Returns the error output of the process (STDERR). |
|
203 * |
|
204 * This only returns the error output if you have not supplied a callback |
|
205 * to the run() method. |
|
206 * |
|
207 * @return string The process error output |
|
208 * |
|
209 * @api |
|
210 */ |
|
211 public function getErrorOutput() |
|
212 { |
|
213 return $this->stderr; |
|
214 } |
|
215 |
|
216 /** |
|
217 * Returns the exit code returned by the process. |
|
218 * |
|
219 * @return integer The exit status code |
|
220 * |
|
221 * @api |
|
222 */ |
|
223 public function getExitCode() |
|
224 { |
|
225 return $this->exitcode; |
|
226 } |
|
227 |
|
228 /** |
|
229 * Checks if the process ended successfully. |
|
230 * |
|
231 * @return Boolean true if the process ended successfully, false otherwise |
|
232 * |
|
233 * @api |
|
234 */ |
|
235 public function isSuccessful() |
|
236 { |
|
237 return 0 == $this->exitcode; |
|
238 } |
|
239 |
|
240 /** |
|
241 * Returns true if the child process has been terminated by an uncaught signal. |
|
242 * |
|
243 * It always returns false on Windows. |
|
244 * |
|
245 * @return Boolean |
|
246 * |
|
247 * @api |
|
248 */ |
|
249 public function hasBeenSignaled() |
|
250 { |
|
251 return $this->status['signaled']; |
|
252 } |
|
253 |
|
254 /** |
|
255 * Returns the number of the signal that caused the child process to terminate its execution. |
|
256 * |
|
257 * It is only meaningful if hasBeenSignaled() returns true. |
|
258 * |
|
259 * @return integer |
|
260 * |
|
261 * @api |
|
262 */ |
|
263 public function getTermSignal() |
|
264 { |
|
265 return $this->status['termsig']; |
|
266 } |
|
267 |
|
268 /** |
|
269 * Returns true if the child process has been stopped by a signal. |
|
270 * |
|
271 * It always returns false on Windows. |
|
272 * |
|
273 * @return Boolean |
|
274 * |
|
275 * @api |
|
276 */ |
|
277 public function hasBeenStopped() |
|
278 { |
|
279 return $this->status['stopped']; |
|
280 } |
|
281 |
|
282 /** |
|
283 * Returns the number of the signal that caused the child process to stop its execution |
|
284 * |
|
285 * It is only meaningful if hasBeenStopped() returns true. |
|
286 * |
|
287 * @return integer |
|
288 * |
|
289 * @api |
|
290 */ |
|
291 public function getStopSignal() |
|
292 { |
|
293 return $this->status['stopsig']; |
|
294 } |
|
295 |
|
296 public function addOutput($line) |
|
297 { |
|
298 $this->stdout .= $line; |
|
299 } |
|
300 |
|
301 public function addErrorOutput($line) |
|
302 { |
|
303 $this->stderr .= $line; |
|
304 } |
|
305 |
|
306 public function getCommandLine() |
|
307 { |
|
308 return $this->commandline; |
|
309 } |
|
310 |
|
311 public function setCommandLine($commandline) |
|
312 { |
|
313 $this->commandline = $commandline; |
|
314 } |
|
315 |
|
316 public function getTimeout() |
|
317 { |
|
318 return $this->timeout; |
|
319 } |
|
320 |
|
321 public function setTimeout($timeout) |
|
322 { |
|
323 $this->timeout = $timeout; |
|
324 } |
|
325 |
|
326 public function getWorkingDirectory() |
|
327 { |
|
328 return $this->cwd; |
|
329 } |
|
330 |
|
331 public function setWorkingDirectory($cwd) |
|
332 { |
|
333 $this->cwd = $cwd; |
|
334 } |
|
335 |
|
336 public function getEnv() |
|
337 { |
|
338 return $this->env; |
|
339 } |
|
340 |
|
341 public function setEnv(array $env) |
|
342 { |
|
343 $this->env = $env; |
|
344 } |
|
345 |
|
346 public function getStdin() |
|
347 { |
|
348 return $this->stdin; |
|
349 } |
|
350 |
|
351 public function setStdin($stdin) |
|
352 { |
|
353 $this->stdin = $stdin; |
|
354 } |
|
355 |
|
356 public function getOptions() |
|
357 { |
|
358 return $this->options; |
|
359 } |
|
360 |
|
361 public function setOptions(array $options) |
|
362 { |
|
363 $this->options = $options; |
|
364 } |
|
365 } |