|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Fabrica\Helper; |
|
4
|
|
|
|
|
5
|
|
|
use Exception; |
|
6
|
|
|
use Fabrica\Tools\Logging\BuildLogger; |
|
7
|
|
|
use Symfony\Component\Process\Process; |
|
8
|
|
|
|
|
9
|
|
|
/** |
|
10
|
|
|
* Handles running system commands with variables. |
|
11
|
|
|
*/ |
|
12
|
|
|
class CommandExecutor implements CommandExecutorInterface |
|
13
|
|
|
{ |
|
14
|
|
|
/** |
|
15
|
|
|
* @var BuildLogger |
|
16
|
|
|
*/ |
|
17
|
|
|
protected $logger; |
|
18
|
|
|
|
|
19
|
|
|
/** |
|
20
|
|
|
* @var bool |
|
21
|
|
|
*/ |
|
22
|
|
|
protected $verbose; |
|
23
|
|
|
|
|
24
|
|
|
/** |
|
25
|
|
|
* @var array |
|
26
|
|
|
*/ |
|
27
|
|
|
protected $lastOutput; |
|
28
|
|
|
|
|
29
|
|
|
/** |
|
30
|
|
|
* @var string |
|
31
|
|
|
*/ |
|
32
|
|
|
protected $lastError; |
|
33
|
|
|
|
|
34
|
|
|
/** |
|
35
|
|
|
* @var bool |
|
36
|
|
|
*/ |
|
37
|
|
|
public $logExecOutput = true; |
|
38
|
|
|
|
|
39
|
|
|
/** |
|
40
|
|
|
* The path which findBinary will look in. |
|
41
|
|
|
* |
|
42
|
|
|
* @var string |
|
43
|
|
|
*/ |
|
44
|
|
|
protected $rootDir; |
|
45
|
|
|
|
|
46
|
|
|
/** |
|
47
|
|
|
* Current build path |
|
48
|
|
|
* |
|
49
|
|
|
* @var string |
|
50
|
|
|
*/ |
|
51
|
|
|
protected $buildPath; |
|
52
|
|
|
|
|
53
|
|
|
/** |
|
54
|
|
|
* Commands with no proper exit mechanism |
|
55
|
|
|
* |
|
56
|
|
|
* @var array |
|
57
|
|
|
*/ |
|
58
|
|
|
private static $noExitCommands = [ |
|
59
|
|
|
'codecept', |
|
60
|
|
|
]; |
|
61
|
|
|
|
|
62
|
|
|
/** |
|
63
|
|
|
* Environment variables that should not be inherited |
|
64
|
|
|
* |
|
65
|
|
|
* @var array |
|
66
|
|
|
*/ |
|
67
|
|
|
private static $blacklistEnvVars = [ |
|
68
|
|
|
'PHP_SELF', |
|
69
|
|
|
'SCRIPT_NAME', |
|
70
|
|
|
'SCRIPT_FILENAME', |
|
71
|
|
|
'PATH_TRANSLATED', |
|
72
|
|
|
'DOCUMENT_ROOT', |
|
73
|
|
|
'SHELL_VERBOSITY', |
|
74
|
|
|
]; |
|
75
|
|
|
|
|
76
|
|
|
/** |
|
77
|
|
|
* @param BuildLogger $logger |
|
78
|
|
|
* @param string $rootDir |
|
79
|
|
|
* @param bool $verbose |
|
80
|
|
|
*/ |
|
81
|
|
|
public function __construct(BuildLogger $logger, $rootDir, $verbose = false) |
|
82
|
|
|
{ |
|
83
|
|
|
$this->logger = $logger; |
|
84
|
|
|
$this->verbose = $verbose; |
|
85
|
|
|
$this->lastOutput = []; |
|
86
|
|
|
$this->rootDir = $rootDir; |
|
87
|
|
|
} |
|
88
|
|
|
|
|
89
|
|
|
/** |
|
90
|
|
|
* Executes shell commands. |
|
91
|
|
|
* |
|
92
|
|
|
* @param array $args |
|
93
|
|
|
* |
|
94
|
|
|
* @return bool Indicates success |
|
95
|
|
|
*/ |
|
96
|
|
|
public function executeCommand($args = []) |
|
97
|
|
|
{ |
|
98
|
|
|
$this->lastOutput = []; |
|
99
|
|
|
|
|
100
|
|
|
$this->logger->logDebug('Args: ' . json_encode($args)); |
|
101
|
|
|
|
|
102
|
|
|
$command = call_user_func_array('sprintf', $args); |
|
103
|
|
|
|
|
104
|
|
|
$this->logger->logDebug('Command: ' . $command); |
|
105
|
|
|
|
|
106
|
|
|
$withNoExit = ''; |
|
107
|
|
|
foreach (self::$noExitCommands as $nec) { |
|
108
|
|
|
if (preg_match("/\b{$nec}\b/", $command)) { |
|
109
|
|
|
$withNoExit = $nec; |
|
110
|
|
|
break; |
|
111
|
|
|
} |
|
112
|
|
|
} |
|
113
|
|
|
|
|
114
|
|
|
$process = new Process($command, $this->buildPath); |
|
115
|
|
|
$process->setTimeout(86400); |
|
116
|
|
|
|
|
117
|
|
|
$env = $this->getDefaultEnv(); |
|
118
|
|
|
|
|
119
|
|
|
if (!empty($withNoExit)) { |
|
120
|
|
|
$process->start(null, $env); |
|
121
|
|
|
|
|
122
|
|
|
$this->logger->logDebug("Assuming command '{$withNoExit}' does not exit properly"); |
|
123
|
|
|
do { |
|
124
|
|
|
sleep(15); |
|
125
|
|
|
$response = []; |
|
126
|
|
|
exec("ps auxww | grep '{$withNoExit}' | grep -v grep", $response); |
|
127
|
|
|
$response = array_filter( |
|
128
|
|
|
$response, |
|
129
|
|
|
function ($a) { |
|
130
|
|
|
return strpos($a, $this->buildPath) !== false; |
|
131
|
|
|
} |
|
132
|
|
|
); |
|
133
|
|
|
} while (!empty($response)); |
|
134
|
|
|
$process->stop(); |
|
135
|
|
|
$status = 0; |
|
136
|
|
|
} else { |
|
137
|
|
|
$process->setIdleTimeout(600); |
|
138
|
|
|
$process->start(null, $env); |
|
139
|
|
|
$status = $process->wait(); |
|
140
|
|
|
} |
|
141
|
|
|
|
|
142
|
|
|
$lastOutput = $this->replaceIllegalCharacters($process->getOutput()); |
|
143
|
|
|
$lastError = $this->replaceIllegalCharacters($process->getErrorOutput()); |
|
144
|
|
|
|
|
145
|
|
|
$this->lastOutput = array_filter(explode(PHP_EOL, $lastOutput)); |
|
146
|
|
|
$this->lastError = $lastError; |
|
147
|
|
|
|
|
148
|
|
|
$shouldOutput = ($this->logExecOutput && ($this->verbose || 0 !== $status)); |
|
149
|
|
|
|
|
150
|
|
|
if ($shouldOutput && !empty($this->lastOutput)) { |
|
151
|
|
|
$this->logger->log($this->lastOutput); |
|
152
|
|
|
} |
|
153
|
|
|
|
|
154
|
|
|
if (!empty($this->lastError)) { |
|
155
|
|
|
$this->logger->logFailure($this->lastError); |
|
156
|
|
|
} |
|
157
|
|
|
|
|
158
|
|
|
$isSuccess = false; |
|
159
|
|
|
if (0 === $status) { |
|
160
|
|
|
$isSuccess = true; |
|
161
|
|
|
} |
|
162
|
|
|
|
|
163
|
|
|
$this->logger->logDebug('Execution status: ' . $status); |
|
164
|
|
|
|
|
165
|
|
|
return $isSuccess; |
|
166
|
|
|
} |
|
167
|
|
|
|
|
168
|
|
|
/** |
|
169
|
|
|
* @param string $utf8String |
|
170
|
|
|
* |
|
171
|
|
|
* @return string |
|
172
|
|
|
*/ |
|
173
|
|
|
public function replaceIllegalCharacters($utf8String) |
|
174
|
|
|
{ |
|
175
|
|
|
mb_substitute_character(0xFFFD); // is '�' |
|
176
|
|
|
$legalUtf8String = mb_convert_encoding($utf8String, 'utf8', 'utf8'); |
|
177
|
|
|
$regexp = '/[\x00-\x08\x10\x0B\x0C\x0E-\x19\x7F]' . |
|
178
|
|
|
'|[^\x{0}-\x{ffff}]/u'; // more than 3 byte UTF-8 sequences (unsupported in mysql) |
|
179
|
|
|
|
|
180
|
|
|
return preg_replace($regexp, '�', $legalUtf8String); |
|
181
|
|
|
} |
|
182
|
|
|
|
|
183
|
|
|
/** |
|
184
|
|
|
* Returns the output from the last command run. |
|
185
|
|
|
* |
|
186
|
|
|
* @return string |
|
187
|
|
|
*/ |
|
188
|
|
|
public function getLastOutput() |
|
189
|
|
|
{ |
|
190
|
|
|
return implode(PHP_EOL, $this->lastOutput); |
|
191
|
|
|
} |
|
192
|
|
|
|
|
193
|
|
|
/** |
|
194
|
|
|
* Returns the stderr output from the last command run. |
|
195
|
|
|
* |
|
196
|
|
|
* @return string |
|
197
|
|
|
*/ |
|
198
|
|
|
public function getLastError() |
|
199
|
|
|
{ |
|
200
|
|
|
return $this->lastError; |
|
201
|
|
|
} |
|
202
|
|
|
|
|
203
|
|
|
/** |
|
204
|
|
|
* @param string $binaryPath |
|
205
|
|
|
* @param string $binary |
|
206
|
|
|
* |
|
207
|
|
|
* @return string|false |
|
208
|
|
|
*/ |
|
209
|
|
View Code Duplication |
protected function findBinaryByPath($binaryPath, $binary) |
|
|
|
|
|
|
210
|
|
|
{ |
|
211
|
|
|
if (is_dir($binaryPath) && is_file($binaryPath . '/' . $binary)) { |
|
212
|
|
|
$this->logger->logDebug(sprintf('Found in %s (binary_path): %s', $binaryPath, $binary)); |
|
213
|
|
|
|
|
214
|
|
|
return $binaryPath . '/' . $binary; |
|
215
|
|
|
} |
|
216
|
|
|
|
|
217
|
|
|
return false; |
|
218
|
|
|
} |
|
219
|
|
|
|
|
220
|
|
|
/** |
|
221
|
|
|
* @param string $composerBin |
|
222
|
|
|
* @param string $binary |
|
223
|
|
|
* |
|
224
|
|
|
* @return string|false |
|
225
|
|
|
*/ |
|
226
|
|
View Code Duplication |
protected function findBinaryLocal($composerBin, $binary) |
|
|
|
|
|
|
227
|
|
|
{ |
|
228
|
|
|
if (is_dir($composerBin) && is_file($composerBin . '/' . $binary)) { |
|
229
|
|
|
$this->logger->logDebug(sprintf('Found in %s (local): %s', $composerBin, $binary)); |
|
230
|
|
|
|
|
231
|
|
|
return $composerBin . '/' . $binary; |
|
232
|
|
|
} |
|
233
|
|
|
|
|
234
|
|
|
return false; |
|
235
|
|
|
} |
|
236
|
|
|
|
|
237
|
|
|
/** |
|
238
|
|
|
* @param string $binary |
|
239
|
|
|
* |
|
240
|
|
|
* @return string|false |
|
241
|
|
|
*/ |
|
242
|
|
|
protected function findBinaryGlobal($binary) |
|
243
|
|
|
{ |
|
244
|
|
|
if (is_file($this->rootDir . 'vendor/bin/' . $binary)) { |
|
245
|
|
|
$this->logger->logDebug(sprintf('Found in %s (global): %s', 'vendor/bin', $binary)); |
|
246
|
|
|
|
|
247
|
|
|
return $this->rootDir . 'vendor/bin/' . $binary; |
|
248
|
|
|
} |
|
249
|
|
|
|
|
250
|
|
|
return false; |
|
251
|
|
|
} |
|
252
|
|
|
|
|
253
|
|
|
/** |
|
254
|
|
|
* Uses 'which' to find a system binary by name |
|
255
|
|
|
* |
|
256
|
|
|
* @param string $binary |
|
257
|
|
|
* |
|
258
|
|
|
* @return string|false |
|
259
|
|
|
*/ |
|
260
|
|
|
protected function findBinarySystem($binary) |
|
261
|
|
|
{ |
|
262
|
|
|
$tempBinary = trim(shell_exec('which ' . $binary)); |
|
263
|
|
|
if (is_file($tempBinary)) { |
|
264
|
|
|
$this->logger->logDebug(sprintf('Found in %s (system): %s', '', $binary)); |
|
265
|
|
|
|
|
266
|
|
|
return $tempBinary; |
|
267
|
|
|
} |
|
268
|
|
|
|
|
269
|
|
|
return false; |
|
270
|
|
|
} |
|
271
|
|
|
|
|
272
|
|
|
/** |
|
273
|
|
|
* {@inheritdoc} |
|
274
|
|
|
*/ |
|
275
|
|
|
public function findBinary($binary, $priorityPath = 'local', $binaryPath = '', $binaryName = []) |
|
276
|
|
|
{ |
|
277
|
|
|
$composerBin = $this->getComposerBinDir($this->buildPath); |
|
278
|
|
|
|
|
279
|
|
|
if (is_string($binary)) { |
|
280
|
|
|
$binary = [$binary]; |
|
281
|
|
|
} |
|
282
|
|
|
|
|
283
|
|
|
if ($binaryName) { |
|
|
|
|
|
|
284
|
|
|
array_unshift($binary, ...$binaryName); |
|
285
|
|
|
} |
|
286
|
|
|
|
|
287
|
|
|
foreach ($binary as $bin) { |
|
288
|
|
|
$this->logger->logDebug(sprintf('Looking for binary: %s, priority = %s', $bin, $priorityPath)); |
|
289
|
|
|
|
|
290
|
|
|
if ('binary_path' === $priorityPath) { |
|
291
|
|
|
if ($binaryPath = $this->findBinaryByPath($binaryPath, $bin)) { |
|
|
|
|
|
|
292
|
|
|
return $binaryPath; |
|
293
|
|
|
} |
|
294
|
|
|
|
|
295
|
|
|
if ($binaryLocal = $this->findBinaryLocal($composerBin, $bin)) { |
|
296
|
|
|
return $binaryLocal; |
|
297
|
|
|
} |
|
298
|
|
|
|
|
299
|
|
|
if ($binaryGlobal = $this->findBinaryGlobal($bin)) { |
|
300
|
|
|
return $binaryGlobal; |
|
301
|
|
|
} |
|
302
|
|
|
|
|
303
|
|
|
if ($binarySystem = $this->findBinarySystem($bin)) { |
|
304
|
|
|
return $binarySystem; |
|
305
|
|
|
} |
|
306
|
|
View Code Duplication |
} elseif ('system' === $priorityPath) { |
|
|
|
|
|
|
307
|
|
|
if ($binarySystem = $this->findBinarySystem($bin)) { |
|
308
|
|
|
return $binarySystem; |
|
309
|
|
|
} |
|
310
|
|
|
|
|
311
|
|
|
if ($binaryLocal = $this->findBinaryLocal($composerBin, $bin)) { |
|
312
|
|
|
return $binaryLocal; |
|
313
|
|
|
} |
|
314
|
|
|
|
|
315
|
|
|
if ($binaryGlobal = $this->findBinaryGlobal($bin)) { |
|
316
|
|
|
return $binaryGlobal; |
|
317
|
|
|
} |
|
318
|
|
|
|
|
319
|
|
|
if ($binaryPath = $this->findBinaryByPath($binaryPath, $bin)) { |
|
|
|
|
|
|
320
|
|
|
return $binaryPath; |
|
321
|
|
|
} |
|
322
|
|
|
} elseif ('global' === $priorityPath) { |
|
323
|
|
|
if ($binaryGlobal = $this->findBinaryGlobal($bin)) { |
|
324
|
|
|
return $binaryGlobal; |
|
325
|
|
|
} |
|
326
|
|
|
|
|
327
|
|
|
if ($binaryLocal = $this->findBinaryLocal($composerBin, $bin)) { |
|
328
|
|
|
return $binaryLocal; |
|
329
|
|
|
} |
|
330
|
|
|
|
|
331
|
|
|
if ($binarySystem = $this->findBinarySystem($bin)) { |
|
332
|
|
|
return $binarySystem; |
|
333
|
|
|
} |
|
334
|
|
|
|
|
335
|
|
|
if ($binaryPath = $this->findBinaryByPath($binaryPath, $bin)) { |
|
|
|
|
|
|
336
|
|
|
return $binaryPath; |
|
337
|
|
|
} |
|
338
|
|
View Code Duplication |
} else { |
|
|
|
|
|
|
339
|
|
|
if ($binaryLocal = $this->findBinaryLocal($composerBin, $bin)) { |
|
340
|
|
|
return $binaryLocal; |
|
341
|
|
|
} |
|
342
|
|
|
|
|
343
|
|
|
if ($binaryGlobal = $this->findBinaryGlobal($bin)) { |
|
344
|
|
|
return $binaryGlobal; |
|
345
|
|
|
} |
|
346
|
|
|
|
|
347
|
|
|
if ($binarySystem = $this->findBinarySystem($bin)) { |
|
348
|
|
|
return $binarySystem; |
|
349
|
|
|
} |
|
350
|
|
|
|
|
351
|
|
|
if ($binaryPath = $this->findBinaryByPath($binaryPath, $bin)) { |
|
|
|
|
|
|
352
|
|
|
return $binaryPath; |
|
353
|
|
|
} |
|
354
|
|
|
} |
|
355
|
|
|
} |
|
356
|
|
|
|
|
357
|
|
|
throw new Exception(sprintf('Could not find %s', implode('/', $binary))); |
|
358
|
|
|
} |
|
359
|
|
|
|
|
360
|
|
|
/** |
|
361
|
|
|
* Try to load the composer.json file in the building project |
|
362
|
|
|
* If the bin-dir is configured, return the full path to it |
|
363
|
|
|
* |
|
364
|
|
|
* @param string $path Current build path |
|
365
|
|
|
* |
|
366
|
|
|
* @return string|null |
|
367
|
|
|
*/ |
|
368
|
|
|
public function getComposerBinDir($path) |
|
369
|
|
|
{ |
|
370
|
|
|
if (is_dir($path)) { |
|
371
|
|
|
$composer = $path . '/composer.json'; |
|
372
|
|
|
if (is_file($composer)) { |
|
373
|
|
|
$json = json_decode(file_get_contents($composer)); |
|
374
|
|
|
|
|
375
|
|
|
if (isset($json->config->{"bin-dir"})) { |
|
376
|
|
|
return $path . '/' . $json->config->{"bin-dir"}; |
|
377
|
|
|
} elseif (is_dir($path . '/vendor/bin')) { |
|
378
|
|
|
return $path . '/vendor/bin'; |
|
379
|
|
|
} |
|
380
|
|
|
} |
|
381
|
|
|
} |
|
382
|
|
|
return null; |
|
383
|
|
|
} |
|
384
|
|
|
|
|
385
|
|
|
/** |
|
386
|
|
|
* Set the buildPath property. |
|
387
|
|
|
* |
|
388
|
|
|
* @param string $path |
|
389
|
|
|
*/ |
|
390
|
|
|
public function setBuildPath($path) |
|
391
|
|
|
{ |
|
392
|
|
|
$this->buildPath = $path; |
|
393
|
|
|
} |
|
394
|
|
|
|
|
395
|
|
|
|
|
396
|
|
|
|
|
397
|
|
|
|
|
398
|
|
|
private function getDefaultEnv() |
|
399
|
|
|
{ |
|
400
|
|
|
$env = array(); |
|
401
|
|
|
|
|
402
|
|
|
foreach ($_SERVER as $k => $v) { |
|
403
|
|
|
if (in_array($k, self::$blacklistEnvVars)) { |
|
404
|
|
|
continue; |
|
405
|
|
|
} |
|
406
|
|
|
if (\is_string($v) && false !== $v = getenv($k)) { |
|
407
|
|
|
$env[$k] = $v; |
|
408
|
|
|
} |
|
409
|
|
|
} |
|
410
|
|
|
|
|
411
|
|
View Code Duplication |
foreach ($_ENV as $k => $v) { |
|
|
|
|
|
|
412
|
|
|
if (in_array($k, self::$blacklistEnvVars)) { |
|
413
|
|
|
continue; |
|
414
|
|
|
} |
|
415
|
|
|
if (\is_string($v)) { |
|
416
|
|
|
$env[$k] = $v; |
|
417
|
|
|
} |
|
418
|
|
|
} |
|
419
|
|
|
|
|
420
|
|
|
if (PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 1) { |
|
421
|
|
View Code Duplication |
foreach (getenv() as $k => $v) { |
|
|
|
|
|
|
422
|
|
|
if (in_array($k, self::$blacklistEnvVars)) { |
|
423
|
|
|
continue; |
|
424
|
|
|
} |
|
425
|
|
|
if (\is_string($v)) { |
|
426
|
|
|
$env[$k] = $v; |
|
427
|
|
|
} |
|
428
|
|
|
} |
|
429
|
|
|
} else { |
|
430
|
|
|
$output = []; |
|
431
|
|
|
exec('env', $output); |
|
432
|
|
|
foreach ($output as $o) { |
|
|
|
|
|
|
433
|
|
|
$keyval = explode('=', $o, 2); |
|
434
|
|
|
if (count($keyval) < 2 || empty($keyval[1])) { |
|
435
|
|
|
continue; |
|
436
|
|
|
} |
|
437
|
|
|
if (in_array($keyval[0], self::$blacklistEnvVars)) { |
|
438
|
|
|
continue; |
|
439
|
|
|
} |
|
440
|
|
|
$env[$keyval[0]] = $keyval[1]; |
|
441
|
|
|
} |
|
442
|
|
|
} |
|
443
|
|
|
|
|
444
|
|
|
return $env; |
|
445
|
|
|
} |
|
446
|
|
|
} |
|
447
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.