1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the PHP-CLI package. |
5
|
|
|
* |
6
|
|
|
* (c) Jitendra Adhikari <[email protected]> |
7
|
|
|
* <https://github.com/adhocore> |
8
|
|
|
* |
9
|
|
|
* Licensed under MIT license. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Ahc\Cli\Helper; |
13
|
|
|
|
14
|
|
|
use Ahc\Cli\Exception\RuntimeException; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* A thin proc_open wrapper to execute shell commands. |
18
|
|
|
* |
19
|
|
|
* With some inspirations from symfony/process. |
20
|
|
|
* |
21
|
|
|
* @author Sushil Gupta <[email protected]> |
22
|
|
|
* @license MIT |
23
|
|
|
* |
24
|
|
|
* @link https://github.com/adhocore/cli |
25
|
|
|
*/ |
26
|
|
|
class Shell |
27
|
|
|
{ |
28
|
|
|
const STDIN_DESCRIPTOR_KEY = 0; |
29
|
|
|
const STDOUT_DESCRIPTOR_KEY = 1; |
30
|
|
|
const STDERR_DESCRIPTOR_KEY = 2; |
31
|
|
|
|
32
|
|
|
const STATE_READY = 'ready'; |
33
|
|
|
const STATE_STARTED = 'started'; |
34
|
|
|
const STATE_CLOSED = 'closed'; |
35
|
|
|
const STATE_TERMINATED = 'terminated'; |
36
|
|
|
|
37
|
|
|
/** @var bool Whether to wait for the process to finish or return instantly */ |
38
|
|
|
protected $async = false; |
39
|
|
|
|
40
|
|
|
/** @var string Command to be executed */ |
41
|
|
|
protected $command; |
42
|
|
|
|
43
|
|
|
/** @var string Current working directory */ |
44
|
|
|
protected $cwd = null; |
45
|
|
|
|
46
|
|
|
/** @var array Descriptor to be passed for proc_open */ |
47
|
|
|
protected $descriptors; |
48
|
|
|
|
49
|
|
|
/** @var array An array of environment variables */ |
50
|
|
|
protected $env = null; |
51
|
|
|
|
52
|
|
|
/** @var int Exit code of the process once it has been terminated */ |
53
|
|
|
protected $exitCode = null; |
54
|
|
|
|
55
|
|
|
/** @var string Input for stdin */ |
56
|
|
|
protected $input; |
57
|
|
|
|
58
|
|
|
/** @var array Other options to be passed for proc_open */ |
59
|
|
|
protected $otherOptions = []; |
60
|
|
|
|
61
|
|
|
/** @var array Pointers to stdin, stdout & stderr */ |
62
|
|
|
protected $pipes = null; |
63
|
|
|
|
64
|
|
|
/** @var resource The actual process resource returned from proc_open */ |
65
|
|
|
protected $process = null; |
66
|
|
|
|
67
|
|
|
/** @var int Process starting time in unix timestamp */ |
68
|
|
|
protected $processStartTime; |
69
|
|
|
|
70
|
|
|
/** @var array Array of status of process as returned from proc_get_status */ |
71
|
|
|
protected $processStatus = null; |
72
|
|
|
|
73
|
|
|
/** @var float Default timeout for the process in seconds with microseconds */ |
74
|
|
|
protected $processTimeout = null; |
75
|
|
|
|
76
|
|
|
/** @var string Current state of the shell execution, value from us, NOT proc_get_status */ |
77
|
|
|
protected $state = self::STATE_READY; |
78
|
|
|
|
79
|
|
|
public function __construct(string $command, string $input = null) |
80
|
|
|
{ |
81
|
|
|
// @codeCoverageIgnoreStart |
82
|
|
|
if (!\function_exists('proc_open')) { |
83
|
|
|
throw new RuntimeException('Required proc_open could not be found in your PHP setup.'); |
84
|
|
|
} |
85
|
|
|
// @codeCoverageIgnoreEnd |
86
|
|
|
|
87
|
|
|
$this->command = $command; |
88
|
|
|
$this->input = $input; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
protected function getDescriptors(): array |
92
|
|
|
{ |
93
|
|
|
$out = $this->isWindows() ? ['file', 'NUL', 'w'] : ['pipe', 'w']; |
94
|
|
|
|
95
|
|
|
return [ |
96
|
|
|
self::STDIN_DESCRIPTOR_KEY => ['pipe', 'r'], |
97
|
|
|
self::STDOUT_DESCRIPTOR_KEY => $out, |
98
|
|
|
self::STDERR_DESCRIPTOR_KEY => $out, |
99
|
|
|
]; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
protected function isWindows() |
103
|
|
|
{ |
104
|
|
|
return '\\' === \DIRECTORY_SEPARATOR; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
protected function setInput() |
108
|
|
|
{ |
109
|
|
|
\fwrite($this->pipes[self::STDIN_DESCRIPTOR_KEY], $this->input); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
protected function updateProcessStatus() |
113
|
|
|
{ |
114
|
|
|
if ($this->state !== self::STATE_STARTED) { |
115
|
|
|
return; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
$this->processStatus = \proc_get_status($this->process); |
|
|
|
|
119
|
|
|
|
120
|
|
|
if ($this->processStatus['running'] === false && $this->exitCode === null) { |
121
|
|
|
$this->exitCode = $this->processStatus['exitcode']; |
122
|
|
|
} |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
protected function closePipes() |
126
|
|
|
{ |
127
|
|
|
\fclose($this->pipes[self::STDIN_DESCRIPTOR_KEY]); |
128
|
|
|
\fclose($this->pipes[self::STDOUT_DESCRIPTOR_KEY]); |
129
|
|
|
\fclose($this->pipes[self::STDERR_DESCRIPTOR_KEY]); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
protected function wait() |
133
|
|
|
{ |
134
|
|
|
while ($this->isRunning()) { |
135
|
|
|
usleep(5000); |
136
|
|
|
$this->checkTimeout(); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
return $this->exitCode; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
protected function checkTimeout() |
143
|
|
|
{ |
144
|
|
|
if ($this->processTimeout === null) { |
145
|
|
|
return; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
$executionDuration = \microtime(true) - $this->processStartTime; |
149
|
|
|
|
150
|
|
|
|
151
|
|
|
if ($executionDuration > $this->processTimeout) { |
152
|
|
|
$this->kill(); |
153
|
|
|
|
154
|
|
|
throw new RuntimeException('Timeout occurred, process terminated.'); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
// @codeCoverageIgnoreStart |
158
|
|
|
|
159
|
|
|
// @codeCoverageIgnoreEnd |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
public function setOptions(string $cwd = null, array $env = null, float $timeout = null, array $otherOptions = []): self |
163
|
|
|
{ |
164
|
|
|
$this->cwd = $cwd; |
165
|
|
|
$this->env = $env; |
166
|
|
|
$this->processTimeout = $timeout; |
167
|
|
|
$this->otherOptions = $otherOptions; |
168
|
|
|
|
169
|
|
|
return $this; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
public function execute(bool $async = false): self |
173
|
|
|
{ |
174
|
|
|
if ($this->isRunning()) { |
175
|
|
|
throw new RuntimeException('Process is already running.'); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
$this->descriptors = $this->getDescriptors(); |
179
|
|
|
$this->processStartTime = \microtime(true); |
180
|
|
|
|
181
|
|
|
$this->process = \proc_open($this->command, $this->descriptors, $this->pipes, $this->cwd, $this->env, $this->otherOptions); |
|
|
|
|
182
|
|
|
$this->setInput(); |
183
|
|
|
|
184
|
|
|
// @codeCoverageIgnoreStart |
185
|
|
|
if (!\is_resource($this->process)) { |
186
|
|
|
throw new RuntimeException('Bad program could not be started.'); |
187
|
|
|
} |
188
|
|
|
// @codeCoverageIgnoreEnd |
189
|
|
|
|
190
|
|
|
$this->state = self::STATE_STARTED; |
191
|
|
|
|
192
|
|
|
$this->updateProcessStatus(); |
193
|
|
|
|
194
|
|
|
if ($this->async = $async) { |
195
|
|
|
$this->setOutputStreamNonBlocking(); |
196
|
|
|
} else { |
197
|
|
|
$this->wait(); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
return $this; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
private function setOutputStreamNonBlocking(): bool |
204
|
|
|
{ |
205
|
|
|
return \stream_set_blocking($this->pipes[self::STDOUT_DESCRIPTOR_KEY], false); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
public function getState(): string |
209
|
|
|
{ |
210
|
|
|
return $this->state; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
public function getOutput(): string |
214
|
|
|
{ |
215
|
|
|
return \stream_get_contents($this->pipes[self::STDOUT_DESCRIPTOR_KEY]); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
public function getErrorOutput(): string |
219
|
|
|
{ |
220
|
|
|
return \stream_get_contents($this->pipes[self::STDERR_DESCRIPTOR_KEY]); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
public function getExitCode() |
224
|
|
|
{ |
225
|
|
|
$this->updateProcessStatus(); |
226
|
|
|
|
227
|
|
|
return $this->exitCode; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
public function isRunning(): bool |
231
|
|
|
{ |
232
|
|
|
if (self::STATE_STARTED !== $this->state) { |
233
|
|
|
return false; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
$this->updateProcessStatus(); |
237
|
|
|
|
238
|
|
|
return $this->processStatus['running']; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
public function getProcessId() |
242
|
|
|
{ |
243
|
|
|
return $this->isRunning() ? $this->processStatus['pid'] : null; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
public function stop() |
247
|
|
|
{ |
248
|
|
|
$this->closePipes(); |
249
|
|
|
|
250
|
|
|
if (\is_resource($this->process)) { |
251
|
|
|
\proc_close($this->process); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
$this->state = self::STATE_CLOSED; |
255
|
|
|
|
256
|
|
|
$this->exitCode = $this->processStatus['exitcode']; |
257
|
|
|
|
258
|
|
|
return $this->exitCode; |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
public function kill() |
262
|
|
|
{ |
263
|
|
|
if (\is_resource($this->process)) { |
264
|
|
|
\proc_terminate($this->process); |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
$this->state = self::STATE_TERMINATED; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
public function __destruct() |
271
|
|
|
{ |
272
|
|
|
//If async (run in background) => we don't care if it ever closes |
273
|
|
|
//Otherwise, waited already till it ends itself - or - timeout occurs, in which case, kill it |
274
|
|
|
} |
275
|
|
|
} |
276
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.