1
|
|
|
<?php |
2
|
|
|
// +---------------------------------------------------------------------- |
3
|
|
|
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ] |
4
|
|
|
// +---------------------------------------------------------------------- |
5
|
|
|
// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved. |
6
|
|
|
// +---------------------------------------------------------------------- |
7
|
|
|
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) |
8
|
|
|
// +---------------------------------------------------------------------- |
9
|
|
|
// | Author: yunwuxin <[email protected]> |
10
|
|
|
// +---------------------------------------------------------------------- |
11
|
|
|
|
12
|
|
|
namespace think\console\output\driver; |
13
|
|
|
|
14
|
|
|
use think\console\Output; |
15
|
|
|
use think\console\output\Formatter; |
16
|
|
|
|
17
|
|
|
class Console |
18
|
|
|
{ |
19
|
|
|
|
20
|
|
|
/** @var Resource */ |
21
|
|
|
private $stdout; |
22
|
|
|
|
23
|
|
|
/** @var Formatter */ |
24
|
|
|
private $formatter; |
25
|
|
|
|
26
|
|
|
private $terminalDimensions; |
27
|
|
|
|
28
|
|
|
/** @var Output */ |
29
|
|
|
private $output; |
30
|
|
|
|
31
|
|
|
public function __construct(Output $output) |
32
|
|
|
{ |
33
|
|
|
$this->output = $output; |
34
|
|
|
$this->formatter = new Formatter(); |
35
|
|
|
$this->stdout = $this->openOutputStream(); |
36
|
|
|
$decorated = $this->hasColorSupport($this->stdout); |
37
|
|
|
$this->formatter->setDecorated($decorated); |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
public function setDecorated($decorated) |
41
|
|
|
{ |
42
|
|
|
$this->formatter->setDecorated($decorated); |
43
|
|
|
} |
44
|
|
|
|
45
|
|
|
public function write($messages, bool $newline = false, int $type = 0, $stream = null) |
46
|
|
|
{ |
47
|
|
|
if (Output::VERBOSITY_QUIET === $this->output->getVerbosity()) { |
48
|
|
|
return; |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
$messages = (array) $messages; |
52
|
|
|
|
53
|
|
|
foreach ($messages as $message) { |
54
|
|
|
switch ($type) { |
55
|
|
|
case Output::OUTPUT_NORMAL: |
56
|
|
|
$message = $this->formatter->format($message); |
57
|
|
|
break; |
58
|
|
|
case Output::OUTPUT_RAW: |
59
|
|
|
break; |
60
|
|
|
case Output::OUTPUT_PLAIN: |
61
|
|
|
$message = strip_tags($this->formatter->format($message)); |
62
|
|
|
break; |
63
|
|
|
default: |
64
|
|
|
throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type)); |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
$this->doWrite($message, $newline, $stream); |
68
|
|
|
} |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
public function renderException(\Throwable $e) |
72
|
|
|
{ |
73
|
|
|
$stderr = $this->openErrorStream(); |
74
|
|
|
$decorated = $this->hasColorSupport($stderr); |
75
|
|
|
$this->formatter->setDecorated($decorated); |
76
|
|
|
|
77
|
|
|
do { |
78
|
|
|
$title = sprintf(' [%s] ', get_class($e)); |
79
|
|
|
|
80
|
|
|
$len = $this->stringWidth($title); |
81
|
|
|
|
82
|
|
|
$width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX; |
83
|
|
|
|
84
|
|
|
if (defined('HHVM_VERSION') && $width > 1 << 31) { |
85
|
|
|
$width = 1 << 31; |
86
|
|
|
} |
87
|
|
|
$lines = []; |
88
|
|
|
foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) { |
89
|
|
|
foreach ($this->splitStringByWidth($line, $width - 4) as $line) { |
90
|
|
|
|
91
|
|
|
$lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $line)) + 4; |
92
|
|
|
$lines[] = [$line, $lineLength]; |
93
|
|
|
|
94
|
|
|
$len = max($lineLength, $len); |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
$messages = ['', '']; |
99
|
|
|
$messages[] = $emptyLine = sprintf('<error>%s</error>', str_repeat(' ', $len)); |
100
|
|
|
$messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title)))); |
101
|
|
|
foreach ($lines as $line) { |
102
|
|
|
$messages[] = sprintf('<error> %s %s</error>', $line[0], str_repeat(' ', $len - $line[1])); |
103
|
|
|
} |
104
|
|
|
$messages[] = $emptyLine; |
105
|
|
|
$messages[] = ''; |
106
|
|
|
$messages[] = ''; |
107
|
|
|
|
108
|
|
|
$this->write($messages, true, Output::OUTPUT_NORMAL, $stderr); |
109
|
|
|
|
110
|
|
|
if (Output::VERBOSITY_VERBOSE <= $this->output->getVerbosity()) { |
111
|
|
|
$this->write('<comment>Exception trace:</comment>', true, Output::OUTPUT_NORMAL, $stderr); |
112
|
|
|
|
113
|
|
|
// exception related properties |
114
|
|
|
$trace = $e->getTrace(); |
115
|
|
|
array_unshift($trace, [ |
116
|
|
|
'function' => '', |
117
|
|
|
'file' => $e->getFile() !== null ? $e->getFile() : 'n/a', |
118
|
|
|
'line' => $e->getLine() !== null ? $e->getLine() : 'n/a', |
119
|
|
|
'args' => [], |
120
|
|
|
]); |
121
|
|
|
|
122
|
|
|
for ($i = 0, $count = count($trace); $i < $count; ++$i) { |
123
|
|
|
$class = isset($trace[$i]['class']) ? $trace[$i]['class'] : ''; |
124
|
|
|
$type = isset($trace[$i]['type']) ? $trace[$i]['type'] : ''; |
125
|
|
|
$function = $trace[$i]['function']; |
126
|
|
|
$file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a'; |
127
|
|
|
$line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a'; |
128
|
|
|
|
129
|
|
|
$this->write(sprintf(' %s%s%s() at <info>%s:%s</info>', $class, $type, $function, $file, $line), true, Output::OUTPUT_NORMAL, $stderr); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
$this->write('', true, Output::OUTPUT_NORMAL, $stderr); |
133
|
|
|
$this->write('', true, Output::OUTPUT_NORMAL, $stderr); |
134
|
|
|
} |
135
|
|
|
} while ($e = $e->getPrevious()); |
136
|
|
|
|
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* 获取终端宽度 |
141
|
|
|
* @return int|null |
142
|
|
|
*/ |
143
|
|
|
protected function getTerminalWidth() |
144
|
|
|
{ |
145
|
|
|
$dimensions = $this->getTerminalDimensions(); |
146
|
|
|
|
147
|
|
|
return $dimensions[0]; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* 获取终端高度 |
152
|
|
|
* @return int|null |
153
|
|
|
*/ |
154
|
|
|
protected function getTerminalHeight() |
155
|
|
|
{ |
156
|
|
|
$dimensions = $this->getTerminalDimensions(); |
157
|
|
|
|
158
|
|
|
return $dimensions[1]; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* 获取当前终端的尺寸 |
163
|
|
|
* @return array |
164
|
|
|
*/ |
165
|
|
|
public function getTerminalDimensions(): array |
166
|
|
|
{ |
167
|
|
|
if ($this->terminalDimensions) { |
168
|
|
|
return $this->terminalDimensions; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
if ('\\' === DIRECTORY_SEPARATOR) { |
172
|
|
|
if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) { |
173
|
|
|
return [(int) $matches[1], (int) $matches[2]]; |
174
|
|
|
} |
175
|
|
|
if (preg_match('/^(\d+)x(\d+)$/', $this->getMode(), $matches)) { |
176
|
|
|
return [(int) $matches[1], (int) $matches[2]]; |
177
|
|
|
} |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
if ($sttyString = $this->getSttyColumns()) { |
181
|
|
View Code Duplication |
if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { |
|
|
|
|
182
|
|
|
return [(int) $matches[2], (int) $matches[1]]; |
183
|
|
|
} |
184
|
|
View Code Duplication |
if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { |
|
|
|
|
185
|
|
|
return [(int) $matches[2], (int) $matches[1]]; |
186
|
|
|
} |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
return [null, null]; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* 获取stty列数 |
194
|
|
|
* @return string |
195
|
|
|
*/ |
196
|
|
|
private function getSttyColumns() |
197
|
|
|
{ |
198
|
|
|
if (!function_exists('proc_open')) { |
199
|
|
|
return; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
$descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; |
203
|
|
|
$process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); |
204
|
|
|
if (is_resource($process)) { |
205
|
|
|
$info = stream_get_contents($pipes[1]); |
206
|
|
|
fclose($pipes[1]); |
207
|
|
|
fclose($pipes[2]); |
208
|
|
|
proc_close($process); |
209
|
|
|
|
210
|
|
|
return $info; |
211
|
|
|
} |
212
|
|
|
return; |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* 获取终端模式 |
217
|
|
|
* @return string <width>x<height> 或 null |
218
|
|
|
*/ |
219
|
|
|
private function getMode() |
220
|
|
|
{ |
221
|
|
|
if (!function_exists('proc_open')) { |
222
|
|
|
return; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
$descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; |
226
|
|
|
$process = proc_open('mode CON', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); |
227
|
|
|
if (is_resource($process)) { |
228
|
|
|
$info = stream_get_contents($pipes[1]); |
229
|
|
|
fclose($pipes[1]); |
230
|
|
|
fclose($pipes[2]); |
231
|
|
|
proc_close($process); |
232
|
|
|
|
233
|
|
|
if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { |
234
|
|
|
return $matches[2] . 'x' . $matches[1]; |
235
|
|
|
} |
236
|
|
|
} |
237
|
|
|
return; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
private function stringWidth(string $string): int |
241
|
|
|
{ |
242
|
|
|
if (!function_exists('mb_strwidth')) { |
243
|
|
|
return strlen($string); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
if (false === $encoding = mb_detect_encoding($string)) { |
247
|
|
|
return strlen($string); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
return mb_strwidth($string, $encoding); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
private function splitStringByWidth(string $string, int $width): array |
254
|
|
|
{ |
255
|
|
|
if (!function_exists('mb_strwidth')) { |
256
|
|
|
return str_split($string, $width); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
if (false === $encoding = mb_detect_encoding($string)) { |
260
|
|
|
return str_split($string, $width); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
$utf8String = mb_convert_encoding($string, 'utf8', $encoding); |
264
|
|
|
$lines = []; |
265
|
|
|
$line = ''; |
266
|
|
|
foreach (preg_split('//u', $utf8String) as $char) { |
267
|
|
|
if (mb_strwidth($line . $char, 'utf8') <= $width) { |
268
|
|
|
$line .= $char; |
269
|
|
|
continue; |
270
|
|
|
} |
271
|
|
|
$lines[] = str_pad($line, $width); |
272
|
|
|
$line = $char; |
273
|
|
|
} |
274
|
|
|
if (strlen($line)) { |
275
|
|
|
$lines[] = count($lines) ? str_pad($line, $width) : $line; |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
mb_convert_variables($encoding, 'utf8', $lines); |
279
|
|
|
|
280
|
|
|
return $lines; |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
private function isRunningOS400(): bool |
284
|
|
|
{ |
285
|
|
|
$checks = [ |
286
|
|
|
function_exists('php_uname') ? php_uname('s') : '', |
287
|
|
|
getenv('OSTYPE'), |
288
|
|
|
PHP_OS, |
289
|
|
|
]; |
290
|
|
|
return false !== stripos(implode(';', $checks), 'OS400'); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* 当前环境是否支持写入控制台输出到stdout. |
295
|
|
|
* |
296
|
|
|
* @return bool |
297
|
|
|
*/ |
298
|
|
|
protected function hasStdoutSupport(): bool |
299
|
|
|
{ |
300
|
|
|
return false === $this->isRunningOS400(); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* 当前环境是否支持写入控制台输出到stderr. |
305
|
|
|
* |
306
|
|
|
* @return bool |
307
|
|
|
*/ |
308
|
|
|
protected function hasStderrSupport(): bool |
309
|
|
|
{ |
310
|
|
|
return false === $this->isRunningOS400(); |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* @return resource |
315
|
|
|
*/ |
316
|
|
|
private function openOutputStream() |
317
|
|
|
{ |
318
|
|
|
if (!$this->hasStdoutSupport()) { |
319
|
|
|
return fopen('php://output', 'w'); |
320
|
|
|
} |
321
|
|
|
return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w'); |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* @return resource |
326
|
|
|
*/ |
327
|
|
|
private function openErrorStream() |
328
|
|
|
{ |
329
|
|
|
return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w'); |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* 将消息写入到输出。 |
334
|
|
|
* @param string $message 消息 |
335
|
|
|
* @param bool $newline 是否另起一行 |
336
|
|
|
* @param null $stream |
337
|
|
|
*/ |
338
|
|
|
protected function doWrite($message, $newline, $stream = null) |
339
|
|
|
{ |
340
|
|
|
if (null === $stream) { |
341
|
|
|
$stream = $this->stdout; |
342
|
|
|
} |
343
|
|
|
if (false === @fwrite($stream, $message . ($newline ? PHP_EOL : ''))) { |
344
|
|
|
throw new \RuntimeException('Unable to write output.'); |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
fflush($stream); |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* 是否支持着色 |
352
|
|
|
* @param $stream |
353
|
|
|
* @return bool |
354
|
|
|
*/ |
355
|
|
|
protected function hasColorSupport($stream): bool |
356
|
|
|
{ |
357
|
|
|
if (DIRECTORY_SEPARATOR === '\\') { |
358
|
|
|
return |
359
|
|
|
'10.0.10586' === PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD |
360
|
|
|
|| false !== getenv('ANSICON') |
361
|
|
|
|| 'ON' === getenv('ConEmuANSI') |
362
|
|
|
|| 'xterm' === getenv('TERM'); |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
return function_exists('posix_isatty') && @posix_isatty($stream); |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
} |
369
|
|
|
|
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.