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