From 117424cf0e15ac0bae2105c2b6d0f74b8e7efb35 Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Thu, 24 Feb 2022 12:58:39 +0100 Subject: [PATCH] wip --- app/Client/Client.php | 11 +- app/Client/Support/ConsoleSectionOutput.php | 138 ++++++++++++++++++++ app/Commands/ShareCommand.php | 6 +- app/Logger/CliRequestLogger.php | 122 ++++++++++++----- 4 files changed, 238 insertions(+), 39 deletions(-) create mode 100644 app/Client/Support/ConsoleSectionOutput.php diff --git a/app/Client/Client.php b/app/Client/Client.php index c2a850e..a029cc2 100644 --- a/app/Client/Client.php +++ b/app/Client/Client.php @@ -118,15 +118,18 @@ class Client $connection->on('authenticated', function ($data) use ($deferred, $sharedUrl) { $httpProtocol = $this->configuration->port() === 443 ? 'https' : 'http'; + + $httpPort = $httpProtocol === 'https' ? '' : ":{$this->configuration->port()}"; + $host = $data->server_host ?? $this->configuration->host(); $this->configuration->setServerHost($host); $this->logger->info($data->message); - $this->logger->info("Local-URL:\t\t{$sharedUrl}"); - $this->logger->info("Dashboard-URL:\t\thttp://127.0.0.1:".config()->get('expose.dashboard_port')); - $this->logger->info("Expose-URL:\t\thttp://{$data->subdomain}.{$host}:{$this->configuration->port()}"); - $this->logger->info("Expose-URL:\t\thttps://{$data->subdomain}.{$host}"); + $this->logger->info("Local-URL:\t\t{$sharedUrl}"); + $this->logger->info("Dashboard-URL:\t\thttp://127.0.0.1:".config()->get('expose.dashboard_port').""); + $this->logger->info("Expose-URL:\t\thttp://{$data->subdomain}.{$host}{$httpPort}"); + $this->logger->info("Expose-URL:\t\thttps://{$data->subdomain}.{$host}"); $this->logger->line(''); static::$subdomains[] = "{$httpProtocol}://{$data->subdomain}.{$host}"; diff --git a/app/Client/Support/ConsoleSectionOutput.php b/app/Client/Support/ConsoleSectionOutput.php new file mode 100644 index 0000000..16373bd --- /dev/null +++ b/app/Client/Support/ConsoleSectionOutput.php @@ -0,0 +1,138 @@ + + * @author Gabriel Ostrolucký + */ +class ConsoleSectionOutput extends StreamOutput +{ + private $content = []; + private $lines = 0; + private $sections; + private $terminal; + + /** + * @param resource $stream + * @param \Symfony\Component\Console\Output\ConsoleSectionOutput[] $sections + */ + public function __construct($stream, array &$sections, int $verbosity, bool $decorated, OutputFormatterInterface $formatter) + { + parent::__construct($stream, $verbosity, $decorated, $formatter); + array_unshift($sections, $this); + $this->sections = &$sections; + $this->terminal = new Terminal(); + } + + /** + * Clears previous output for this section. + * + * @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared + */ + public function clear(int $lines = null) + { + if (empty($this->content) || !$this->isDecorated()) { + return; + } + + if ($lines) { + array_splice($this->content, -($lines * 2)); // Multiply lines by 2 to cater for each new line added between content + } else { + $lines = $this->lines; + $this->content = []; + } + + $this->lines -= $lines; + + parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false); + } + + /** + * Overwrites the previous output with a new message. + * + * @param array|string $message + */ + public function overwrite($message) + { + $this->clear(); + $this->writeln($message); + } + + public function getContent(): string + { + return implode('', $this->content); + } + + /** + * @internal + */ + public function addContent(string $input) + { + foreach (explode(\PHP_EOL, $input) as $lineContent) { + $this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1; + $this->content[] = $lineContent; + $this->content[] = \PHP_EOL; + } + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $message, bool $newline) + { + if (!$this->isDecorated()) { + parent::doWrite($message, $newline); + + return; + } + + $erasedContent = $this->popStreamContentUntilCurrentSection(); + + $this->addContent($message); + + parent::doWrite($message, true); + parent::doWrite($erasedContent, false); + } + + /** + * At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits + * current section. Then it erases content it crawled through. Optionally, it erases part of current section too. + */ + private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string + { + $numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection; + $erasedContent = []; + + foreach ($this->sections as $section) { + if ($section === $this) { + break; + } + + $numberOfLinesToClear += $section->lines; + $erasedContent[] = $section->getContent(); + } + + if ($numberOfLinesToClear > 0) { + // move cursor up n lines + parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false); + // erase to end of screen + parent::doWrite("\x1b[0J", false); + } + + return implode('', array_reverse($erasedContent)); + } + + private function getDisplayLength(string $text): int + { + $cleanedText = Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text)); + $cleanedText = preg_replace('/]8;;(.*)]8;;/m', '', $cleanedText); + + return Helper::width($cleanedText); + } +} diff --git a/app/Commands/ShareCommand.php b/app/Commands/ShareCommand.php index 3ff49c5..87241b5 100644 --- a/app/Commands/ShareCommand.php +++ b/app/Commands/ShareCommand.php @@ -5,6 +5,7 @@ namespace App\Commands; use App\Client\Factory; use Illuminate\Support\Str; use React\EventLoop\LoopInterface; +use Symfony\Component\Console\Output\OutputInterface; class ShareCommand extends ServerAwareCommand { @@ -15,6 +16,7 @@ class ShareCommand extends ServerAwareCommand public function handle() { $auth = $this->option('auth') ?? config('expose.auth_token', ''); + $this->info('Using auth token: '.$auth, OutputInterface::VERBOSITY_DEBUG); if (strstr($this->argument('host'), 'host.docker.internal')) { config(['expose.dns' => true]); @@ -36,12 +38,12 @@ class ShareCommand extends ServerAwareCommand if (! is_null($this->option('subdomain'))) { $subdomains = explode(',', $this->option('subdomain')); - $this->info('Trying to use custom domain: '.$subdomains[0]); + $this->info('Trying to use custom domain: '.$subdomains[0].PHP_EOL, OutputInterface::VERBOSITY_VERBOSE); } else { $host = Str::beforeLast($this->argument('host'), '.'); $host = Str::beforeLast($host, ':'); $subdomains = [Str::slug($host)]; - $this->info('Trying to use custom domain: '.$subdomains[0].PHP_EOL); + $this->info('Trying to use custom domain: '.$subdomains[0].PHP_EOL, OutputInterface::VERBOSITY_VERBOSE); } (new Factory()) diff --git a/app/Logger/CliRequestLogger.php b/app/Logger/CliRequestLogger.php index d18634e..95a3bec 100644 --- a/app/Logger/CliRequestLogger.php +++ b/app/Logger/CliRequestLogger.php @@ -2,32 +2,60 @@ namespace App\Logger; +use App\Client\Support\ConsoleSectionOutput; use Illuminate\Support\Collection; -use Symfony\Component\Console\Helper\Table; -use Symfony\Component\Console\Helper\TableSeparator; -use Symfony\Component\Console\Helper\TableStyle; use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Terminal; class CliRequestLogger extends Logger { - /** @var Table */ - protected $table; - /** @var Collection */ protected $requests; - /** @var \Symfony\Component\Console\Output\ConsoleSectionOutput */ protected $section; + protected $verbColors = [ + 'GET' => 'blue', + 'HEAD' => '#6C7280', + 'OPTIONS' => '#6C7280', + 'POST' => 'yellow', + 'PUT' => 'yellow', + 'PATCH' => 'yellow', + 'DELETE' => 'red', + ]; + + protected $consoleSectionOutputs = []; + + /** + * The current terminal width. + * + * @var int|null + */ + protected $terminalWidth; + + /** + * Computes the terminal width. + * + * @return int + */ + protected function getTerminalWidth() + { + if ($this->terminalWidth == null) { + $this->terminalWidth = (new Terminal)->getWidth(); + + $this->terminalWidth = $this->terminalWidth >= 30 + ? $this->terminalWidth + : 30; + } + + return $this->terminalWidth; + } + public function __construct(ConsoleOutputInterface $consoleOutput) { parent::__construct($consoleOutput); - $this->section = $this->output->section(); - - $this->table = new Table($this->section); - $this->table->setStyle($this->getTableStyle()); - $this->table->setHeaders(['Method', 'URI', 'Response', 'Time', 'Duration']); + $this->section = new ConsoleSectionOutput($this->output->getStream(), $this->consoleSectionOutputs, $this->output->getVerbosity(), $this->output->isDecorated(), $this->output->getFormatter()); $this->requests = new Collection(); } @@ -40,15 +68,6 @@ class CliRequestLogger extends Logger return $this->output; } - protected function getTableStyle() - { - return (new TableStyle()) - ->setHorizontalBorderChars('─') - ->setVerticalBorderChars('│') - ->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├') - ; - } - protected function getRequestColor(?LoggedRequest $request) { $statusCode = optional($request->getResponse())->getStatusCode(); @@ -78,21 +97,58 @@ class CliRequestLogger extends Logger } $this->requests = $this->requests->slice(0, config('expose.max_logged_requests', 10)); - $this->section->clear(); + $terminalWidth = $this->getTerminalWidth(); - $this->table->setRows($this->requests->map(function (LoggedRequest $loggedRequest) use ($dashboardUrl) { + $requests = $this->requests->map(function (LoggedRequest $loggedRequest) use ($dashboardUrl) { return [ - $loggedRequest->getRequest()->getMethod(), - $loggedRequest->getRequest()->getUri(), - 'id().';fg='.$this->getRequestColor($loggedRequest).';options=bold>'. - optional($loggedRequest->getResponse())->getStatusCode().' '.optional($loggedRequest->getResponse())->getReasonPhrase() - .'' - , - $loggedRequest->getStartTime()->isToday() ? $loggedRequest->getStartTime()->toTimeString() : $loggedRequest->getStartTime()->toDateTimeString(), - $loggedRequest->getDuration().'ms', + 'method' => $loggedRequest->getRequest()->getMethod(), + 'url' => $loggedRequest->getRequest()->getUri(), + 'duration' => $loggedRequest->getDuration(), + 'time' => $loggedRequest->getStartTime()->isToday() ? $loggedRequest->getStartTime()->toTimeString() : $loggedRequest->getStartTime()->toDateTimeString(), + 'color' => $this->getRequestColor($loggedRequest), + 'status' => optional($loggedRequest->getResponse())->getStatusCode(), + 'dashboardUrl' => $dashboardUrl.'/#'.$loggedRequest->id(), ]; - })->toArray()); + }); - $this->table->render(); + $maxMethod = mb_strlen($requests->max('method')); + $maxDuration = mb_strlen($requests->max('duration')); + + $output = $requests->map(function ($loggedRequest) use ($terminalWidth, $maxMethod, $maxDuration) { + $method = $loggedRequest['method']; + $spaces = str_repeat(' ', max($maxMethod + 2 - mb_strlen($method), 0)); + $url = $loggedRequest['url']; + $duration = $loggedRequest['duration']; + $time = $loggedRequest['time']; + $durationSpaces = str_repeat(' ', max($maxDuration + 2 - mb_strlen($duration), 0)); + $color = $loggedRequest['color']; + $status = $loggedRequest['status']; + $dashboardUrl = $loggedRequest['dashboardUrl']; + + $dots = str_repeat('.', max($terminalWidth - strlen($method.$spaces.$url.$time.$durationSpaces.$duration) - 16, 0)); + + if (empty($dots)) { + $url = substr($url, 0, $terminalWidth - strlen($method.$spaces.$time.$durationSpaces.$duration) - 15 - 3).'...'; + } else { + $dots .= ' '; + } + + return sprintf( + ' %s %s%s %s %s%s%s%s ms', + $color, + $status, + $this->verbColors[$method] ?? 'default', + $method, + $spaces, + $dashboardUrl, + $url, + $dots, + $time, + $durationSpaces, + $duration, + ); + }); + + $this->section->overwrite($output); } }