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
|
|
|
* @author Sushil Gupta <[email protected]> |
19
|
|
|
* @license MIT |
20
|
|
|
*/ |
21
|
|
|
class Shell |
22
|
|
|
{ |
23
|
|
|
const STDIN_DESCRIPTOR_KEY = 0; |
24
|
|
|
const STDOUT_DESCRIPTOR_KEY = 1; |
25
|
|
|
const STDERR_DESCRIPTOR_KEY = 2; |
26
|
|
|
|
27
|
|
|
const STATE_READY = 'ready'; |
28
|
|
|
const STATE_STARTED = 'started'; |
29
|
|
|
const STATE_CLOSED = 'closed'; |
30
|
|
|
const STATE_TERMINATED = 'terminated'; |
31
|
|
|
|
32
|
|
|
/** @var bool Whether to wait for the process to finish or return instantly */ |
33
|
|
|
protected $async = false; |
34
|
|
|
|
35
|
|
|
/** @var string Command to be executed */ |
36
|
|
|
protected $command; |
37
|
|
|
|
38
|
|
|
/** @var string Current working directory */ |
39
|
|
|
protected $cwd = null; |
40
|
|
|
|
41
|
|
|
/** @var array Descriptor to be passed for proc_open */ |
42
|
|
|
protected $descriptors; |
43
|
|
|
|
44
|
|
|
/** @var array An array of environment variables */ |
45
|
|
|
protected $env = null; |
46
|
|
|
|
47
|
|
|
/** @var int Exit code of the process once it has been terminated */ |
48
|
|
|
protected $exitCode = null; |
49
|
|
|
|
50
|
|
|
/** @var string Input for stdin */ |
51
|
|
|
protected $input; |
52
|
|
|
|
53
|
|
|
/** @var array Other options to be passed for proc_open */ |
54
|
|
|
protected $otherOptions = []; |
55
|
|
|
|
56
|
|
|
/** @var array Pointers to stdin, stdout & stderr */ |
57
|
|
|
protected $pipes = null; |
58
|
|
|
|
59
|
|
|
/** @var resource The actual process resource returned from proc_open */ |
60
|
|
|
protected $process = null; |
61
|
|
|
|
62
|
|
|
/** @var array Status of the process as returned from proc_get_status */ |
63
|
|
|
protected $processStatus = null; |
64
|
|
|
|
65
|
|
|
/** @var int Process starting time in unix timestamp */ |
66
|
|
|
protected $processStartTime; |
67
|
|
|
|
68
|
|
|
/** @var string Current state of the shell execution */ |
69
|
|
|
protected $state = self::STATE_READY; |
70
|
|
|
|
71
|
|
|
/** @var float Default timeout for the process in seconds with microseconds */ |
72
|
|
|
protected $processTimeoutPeriod = null; |
73
|
|
|
|
74
|
|
|
public function __construct(string $command, string $input = null) |
75
|
|
|
{ |
76
|
|
|
if (!\function_exists('proc_open')) { |
77
|
|
|
throw new RuntimeException('Required proc_open could not be found in your PHP setup'); |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
$this->command = $command; |
81
|
|
|
$this->input = $input; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
protected function getDescriptors() |
85
|
|
|
{ |
86
|
|
|
$out = '\\' === \DIRECTORY_SEPARATOR ? ['file', 'NUL', 'w'] : ['pipe', 'w']; |
87
|
|
|
|
88
|
|
|
return [ |
89
|
|
|
self::STDIN_DESCRIPTOR_KEY => ['pipe', 'r'], |
90
|
|
|
self::STDOUT_DESCRIPTOR_KEY => $out, |
91
|
|
|
self::STDERR_DESCRIPTOR_KEY => $out, |
92
|
|
|
]; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
protected function setInput() |
96
|
|
|
{ |
97
|
|
|
\fwrite($this->pipes[self::STDIN_DESCRIPTOR_KEY], $this->input); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
protected function updateProcessStatus() |
101
|
|
|
{ |
102
|
|
|
if ($this->state !== self::STATE_STARTED) { |
103
|
|
|
return; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
$this->processStatus = \proc_get_status($this->process); |
|
|
|
|
107
|
|
|
|
108
|
|
|
if ($this->processStatus['running'] === false && $this->exitCode === null) { |
109
|
|
|
$this->exitCode = $this->processStatus['exitcode']; |
110
|
|
|
} |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
protected function closePipes() |
114
|
|
|
{ |
115
|
|
|
\fclose($this->pipes[self::STDIN_DESCRIPTOR_KEY]); |
116
|
|
|
\fclose($this->pipes[self::STDOUT_DESCRIPTOR_KEY]); |
117
|
|
|
\fclose($this->pipes[self::STDERR_DESCRIPTOR_KEY]); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
public function wait() |
121
|
|
|
{ |
122
|
|
|
while ($this->isRunning()) { |
123
|
|
|
usleep(5000); |
124
|
|
|
$this->checkTimeout(); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
return $this->exitCode; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
public function checkTimeout() |
131
|
|
|
{ |
132
|
|
|
if ($this->state !== self::STATE_STARTED) { |
133
|
|
|
return; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
if ($this->processTimeoutPeriod === null) { |
137
|
|
|
return; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
$execution_duration = \microtime(true) - $this->processStartTime; |
141
|
|
|
|
142
|
|
|
if ($execution_duration > $this->processTimeoutPeriod) { |
143
|
|
|
$this->kill(); |
144
|
|
|
|
145
|
|
|
throw new RuntimeException('Process timeout occurred, terminated'); |
146
|
|
|
} |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
public function setOptions(string $cwd = null, array $env = null, float $timeout = null, $otherOptions = []) |
150
|
|
|
{ |
151
|
|
|
$this->cwd = $cwd; |
152
|
|
|
$this->env = $env; |
153
|
|
|
$this->processTimeoutPeriod = $timeout; |
154
|
|
|
$this->otherOptions = $otherOptions; |
155
|
|
|
|
156
|
|
|
return $this; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
public function execute($async = false) |
160
|
|
|
{ |
161
|
|
|
if ($this->isRunning()) { |
162
|
|
|
throw new RuntimeException('Process is already running'); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
$this->descriptors = $this->getDescriptors(); |
166
|
|
|
|
167
|
|
|
$this->process = \proc_open($this->command, $this->descriptors, $this->pipes, $this->cwd, $this->env, $this->otherOptions); |
|
|
|
|
168
|
|
|
|
169
|
|
|
if (!\is_resource($this->process)) { |
170
|
|
|
throw new RuntimeException('Bad program could not be started.'); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
$this->state = self::STATE_STARTED; |
174
|
|
|
|
175
|
|
|
$this->setInput(); |
176
|
|
|
$this->updateProcessStatus(); |
177
|
|
|
$this->processStartTime = \microtime(true); |
178
|
|
|
|
179
|
|
|
if ($this->async = $async) { |
180
|
|
|
$this->setOutputStreamNonBlocking(); |
181
|
|
|
} else { |
182
|
|
|
$this->wait(); |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
private function setOutputStreamNonBlocking() |
187
|
|
|
{ |
188
|
|
|
return \stream_set_blocking($this->pipes[self::STDOUT_DESCRIPTOR_KEY], false); |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
public function getState() |
192
|
|
|
{ |
193
|
|
|
return $this->state; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
public function getOutput() |
197
|
|
|
{ |
198
|
|
|
return \stream_get_contents($this->pipes[self::STDOUT_DESCRIPTOR_KEY]); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
public function getErrorOutput() |
202
|
|
|
{ |
203
|
|
|
return \stream_get_contents($this->pipes[self::STDERR_DESCRIPTOR_KEY]); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
public function getExitCode() |
207
|
|
|
{ |
208
|
|
|
$this->updateProcessStatus(); |
209
|
|
|
|
210
|
|
|
return $this->exitCode; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
public function isRunning() |
214
|
|
|
{ |
215
|
|
|
if (self::STATE_STARTED !== $this->state) { |
216
|
|
|
return false; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
$this->updateProcessStatus(); |
220
|
|
|
|
221
|
|
|
return $this->processStatus['running']; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
public function getProcessId() |
225
|
|
|
{ |
226
|
|
|
return $this->isRunning() ? $this->processStatus['pid'] : null; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
public function stop() |
230
|
|
|
{ |
231
|
|
|
$this->closePipes(); |
232
|
|
|
|
233
|
|
|
if (\is_resource($this->process)) { |
234
|
|
|
\proc_close($this->process); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
$this->state = self::STATE_CLOSED; |
238
|
|
|
|
239
|
|
|
$this->exitCode = $this->processStatus['exitcode']; |
240
|
|
|
|
241
|
|
|
return $this->exitCode; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
public function kill() |
245
|
|
|
{ |
246
|
|
|
if (\is_resource($this->process)) { |
247
|
|
|
\proc_terminate($this->process); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
$this->state = self::STATE_TERMINATED; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
public function __destruct() |
254
|
|
|
{ |
255
|
|
|
//if async (run in background) => we don't care if it ever closes |
256
|
|
|
//if not async, waited already till it runs or timeout occurs, in which case kill it |
257
|
|
|
} |
258
|
|
|
} |
259
|
|
|
|
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.