ConsoleLogger.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Deploy\Console;
  7. use Psr\Log\AbstractLogger;
  8. use Psr\Log\LogLevel;
  9. use Symfony\Component\Console\Output\OutputInterface;
  10. use Symfony\Component\Console\Output\ConsoleOutputInterface;
  11. use Symfony\Component\Console\Helper\FormatterHelper;
  12. use Magento\Framework\Filesystem;
  13. use Magento\Framework\App\Filesystem\DirectoryList;
  14. use Magento\Framework\Filesystem\Directory\ReadInterface;
  15. /**
  16. * PSR logger implementation for CLI
  17. */
  18. class ConsoleLogger extends AbstractLogger
  19. {
  20. /**
  21. * Type for informational message
  22. */
  23. const INFO = 'info';
  24. /**
  25. * Type for error message
  26. */
  27. const ERROR = 'error';
  28. /**
  29. * Public static files directory read interface
  30. *
  31. * @var ReadInterface
  32. */
  33. private $tmpDir;
  34. /**
  35. * Console output interface
  36. *
  37. * @var OutputInterface
  38. */
  39. private $output;
  40. /**
  41. * Helper for preparing data of specific formats (date, percentage, etc)
  42. *
  43. * @var FormatterHelper
  44. */
  45. private $formatterHelper;
  46. /**
  47. * Maximum progress bar row string length
  48. *
  49. * @var int
  50. */
  51. private $initialMaxBarSize = 0;
  52. /**
  53. * Number of rendered lines
  54. *
  55. * Used for clearing previously rendered progress bars
  56. *
  57. * @var int
  58. */
  59. private $renderedLines = 0;
  60. /**
  61. * Time of previous rendering tick
  62. *
  63. * @var int
  64. */
  65. private $lastTimeRefreshed = 0;
  66. /**
  67. * @var array
  68. */
  69. private $verbosityLevelMap = [
  70. LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL,
  71. LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL,
  72. LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL,
  73. LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL,
  74. LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL,
  75. LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL,
  76. LogLevel::INFO => OutputInterface::VERBOSITY_VERBOSE,
  77. LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG
  78. ];
  79. /**
  80. * @var array
  81. */
  82. private $formatLevelMap = [
  83. LogLevel::EMERGENCY => self::ERROR,
  84. LogLevel::ALERT => self::ERROR,
  85. LogLevel::CRITICAL => self::ERROR,
  86. LogLevel::ERROR => self::ERROR,
  87. LogLevel::WARNING => self::INFO,
  88. LogLevel::NOTICE => self::INFO,
  89. LogLevel::INFO => self::INFO,
  90. LogLevel::DEBUG => self::INFO
  91. ];
  92. /**
  93. * Running deployment processes info
  94. *
  95. * @var array[]
  96. */
  97. private $processes = [];
  98. /**
  99. * @param Filesystem $filesystem
  100. * @param OutputInterface $output
  101. * @param FormatterHelper $formatterHelper
  102. * @param array $verbosityLevelMap
  103. * @param array $formatLevelMap
  104. */
  105. public function __construct(
  106. Filesystem $filesystem,
  107. OutputInterface $output,
  108. FormatterHelper $formatterHelper,
  109. array $verbosityLevelMap = [],
  110. array $formatLevelMap = []
  111. ) {
  112. $this->tmpDir = $filesystem->getDirectoryWrite(DirectoryList::TMP_MATERIALIZATION_DIR);
  113. $this->output = $output;
  114. $this->formatterHelper = $formatterHelper;
  115. $this->verbosityLevelMap = $verbosityLevelMap + $this->verbosityLevelMap;
  116. $this->formatLevelMap = $formatLevelMap + $this->formatLevelMap;
  117. }
  118. /**
  119. * @inheritdoc
  120. */
  121. public function log($level, $message, array $context = [])
  122. {
  123. if (!isset($this->verbosityLevelMap[$level])) {
  124. $level = self::INFO;
  125. }
  126. // Write to the error output if necessary and available
  127. if ($this->formatLevelMap[$level] === self::ERROR && $this->output instanceof ConsoleOutputInterface) {
  128. $output = $this->output->getErrorOutput();
  129. } else {
  130. $output = $this->output;
  131. }
  132. if (isset($context['process'])) {
  133. $this->registerProcess($context);
  134. } else {
  135. $this->refresh($output);
  136. }
  137. if ($output->getVerbosity() >= $this->verbosityLevelMap[$level]) {
  138. $output->writeln(sprintf('<%1$s>%2$s</%1$s>', $this->formatLevelMap[$level], $message));
  139. }
  140. }
  141. /**
  142. * Add deployment process to rendering stack
  143. *
  144. * @param array $context
  145. * @return void
  146. */
  147. private function registerProcess(array $context)
  148. {
  149. $name = isset($context['process']) ? $context['process'] : 'main';
  150. if (!isset($this->processes[$name])) {
  151. $context['start'] = time();
  152. $context['elapsed'] = 0;
  153. $this->processes[$name] = $context;
  154. }
  155. }
  156. /**
  157. * Refresh CLI output
  158. *
  159. * @param OutputInterface $output
  160. * @return void
  161. */
  162. private function refresh(OutputInterface $output)
  163. {
  164. if (!count($this->processes) || (time() - $this->lastTimeRefreshed < 1)) {
  165. return;
  166. }
  167. $this->cleanUp();
  168. $bars = [];
  169. $maxBarSize = 0;
  170. foreach ($this->processes as $name => & $process) {
  171. $this->updateProcessInfo($name, $process);
  172. $bar = $this->renderProgressBar($output, $process);
  173. $maxBarSize = strlen($bar) > $maxBarSize ? strlen($bar) : $maxBarSize;
  174. $bars[] = $bar;
  175. }
  176. if (!$this->initialMaxBarSize) {
  177. $this->initialMaxBarSize = $maxBarSize + 10;
  178. }
  179. if ($bars) {
  180. $this->renderedLines = count($bars);
  181. $bar = '';
  182. foreach ($bars as &$bar) {
  183. if ($this->initialMaxBarSize > strlen($bar)) {
  184. $bar .= str_pad(" ", ($this->initialMaxBarSize - strlen($bar)));
  185. }
  186. }
  187. $bar = trim($bar);
  188. $output->writeln(implode("\n", $bars));
  189. }
  190. }
  191. /**
  192. * Update process information
  193. *
  194. * @param string $deployedPackagePath
  195. * @param array $process
  196. * @return void
  197. */
  198. private function updateProcessInfo($deployedPackagePath, array & $process)
  199. {
  200. $packageDeploymentInfo = $this->getPackageDeploymentInfo($deployedPackagePath . '/info.json');
  201. if ($packageDeploymentInfo) {
  202. $process['done'] = $packageDeploymentInfo['count'];
  203. } else {
  204. $process['done'] = 0;
  205. }
  206. if ($process['done'] > $process['count']) {
  207. $process['count'] = $process['done'];
  208. }
  209. if ($process['done'] !== $process['count']) {
  210. $process['elapsed'] = $this->formatterHelper->formatTime(time() - $process['start']);
  211. }
  212. $process['percent'] = floor(
  213. ($process['count'] ? (float)$process['done'] / $process['count'] : 0) * 100
  214. );
  215. }
  216. /**
  217. * Clear rendered lines
  218. *
  219. * @return void
  220. */
  221. private function cleanUp()
  222. {
  223. $this->lastTimeRefreshed = time();
  224. // Erase previous lines
  225. if ($this->renderedLines > 0) {
  226. for ($i = 0; $i < $this->renderedLines; ++$i) {
  227. $this->output->write("\x1B[1A\x1B[2K", false, OutputInterface::OUTPUT_RAW);
  228. }
  229. }
  230. $this->renderedLines = 0;
  231. }
  232. /**
  233. * Generate progress bar part
  234. *
  235. * @param OutputInterface $output
  236. * @param array $process
  237. * @return string
  238. */
  239. private function renderProgressBar(OutputInterface $output, array $process)
  240. {
  241. $title = "{$process['process']}";
  242. $titlePad = str_pad(' ', (40 - strlen($title)));
  243. $count = "{$process['done']}/{$process['count']}";
  244. $countPad = str_pad(' ', (20 - strlen($count)));
  245. $percent = "{$process['percent']}% ";
  246. $percentPad = str_pad(' ', (7 - strlen($percent)));
  247. return "{$title}{$titlePad}"
  248. . "{$count}{$countPad}"
  249. . "{$this->renderBar($output, $process)} "
  250. . "{$percent}%{$percentPad}"
  251. . "{$process['elapsed']} ";
  252. }
  253. /**
  254. * Generate progress bar row
  255. *
  256. * @param OutputInterface $output
  257. * @param array $process
  258. * @return string
  259. */
  260. private function renderBar(OutputInterface $output, array $process)
  261. {
  262. $completeBars = floor(
  263. $process['count'] > 0 ? ($process['done'] / $process['count']) * 28 : $process['done'] % 28
  264. );
  265. $display = str_repeat('=', $completeBars);
  266. if ($completeBars < 28) {
  267. $emptyBars = 28 - $completeBars
  268. - $this->formatterHelper->strlenWithoutDecoration($output->getFormatter(), '>');
  269. $display .= '>' . str_repeat('-', $emptyBars);
  270. }
  271. return $display;
  272. }
  273. /**
  274. * Retrieve package deployment process information
  275. *
  276. * @param string $relativePath
  277. * @return string|false
  278. */
  279. private function getPackageDeploymentInfo($relativePath)
  280. {
  281. if ($this->tmpDir->isFile($relativePath)) {
  282. $info = $this->tmpDir->readFile($relativePath);
  283. $info = json_decode($info, true);
  284. } else {
  285. $info = [];
  286. }
  287. return $info;
  288. }
  289. }