diff options
Diffstat (limited to 'vespaclient/src/perl/lib/Yahoo/Vespa/ConsoleOutput.pm')
-rw-r--r-- | vespaclient/src/perl/lib/Yahoo/Vespa/ConsoleOutput.pm | 331 |
1 files changed, 331 insertions, 0 deletions
diff --git a/vespaclient/src/perl/lib/Yahoo/Vespa/ConsoleOutput.pm b/vespaclient/src/perl/lib/Yahoo/Vespa/ConsoleOutput.pm new file mode 100644 index 00000000000..73a0a016592 --- /dev/null +++ b/vespaclient/src/perl/lib/Yahoo/Vespa/ConsoleOutput.pm @@ -0,0 +1,331 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +# +# Output handler +# +# Intentions: +# - Make it easy for unit tests to redirect output. +# - Allow programmers to add all sorts of debug information into tools usable +# for debugging, while hiding it by default for real users. +# - Allow generic functionality that can be reused by all. For instance color +# coding of very important information. +# +# Ideas for improvement: +# - Could possibly detect terminal width and do proper line breaking of long +# lines +# +# A note about colors: +# - This module will detect if terminal supports colors. If not, it will not +# print any. (Color support can be turned off by giving --nocolors argument +# through argument parser, by setting a TERM value that does not support +# colors or programmatically call setUseAnsiColors(0). +# - Currently only red and grey are used in addition to default. These colors +# should work well for both light and dark backgrounds. +# + +package Yahoo::Vespa::ConsoleOutput; + +use strict; +use warnings; +use Yahoo::Vespa::Utils; + +BEGIN { # - Define exports for modul + use base 'Exporter'; + our @EXPORT = qw( + printResult printError printWarning printInfo printDebug printSpam + enableAutomaticLineBreaks + COLOR_RESET COLOR_WARN COLOR_ERR COLOR_ANON + ); + our @EXPORT_OK = qw( + getTerminalWidth getVerbosity usingAnsiColors ansiColorsSupported + setVerbosity + ); +} + +my %TYPES = ( + 'result' => 0, # Output from a tool. Expected when app runs successfully. + 'error' => 1, # Error found, typically aborting the script with a failure. + 'warning' => 2, # An issue that may or may not cause the program to fail. + 'info' => 3, # Useful information to get from the script. + 'debug' => 4, # Debug information useful to debug script or to see + # internals of what is happening. + 'spam' => 5, # Spammy information used when large amounts of details is + # wanted. Typically to debug some failure. +); +my $VERBOSITY; # Current verbosity level +my $ANSI_COLORS_SUPPORTED; # True if terminal supports colors +my $ANSI_COLORS; # True if we want to use colors (and support it) +my %ATTRIBUTE_PREFIX; # Ansi escape prefixes for verbosity levels +my %ATTRIBUTE_POSTFIX; # Ansi escape postfixes for verbosity levels +my %OUTPUT_STREAM; # Where to write different verbosity levels (stdout|stderr) +my $TERMINAL_WIDTH; # With of terminal in columns +my $COLUMN_POSITION; # Current index of cursor in terminal +my $ENABLE_AUTO_LINE_BREAKS; + +use constant COLOR_RESET => "\e[0m"; +use constant COLOR_ERR => "\e[91m"; +use constant COLOR_WARN => "\e[93m"; +use constant COLOR_ANON => "\e[90m"; + +&initialize(*STDOUT, *STDERR); + +return 1; + +########################## Default exported functions ######################## + +sub printResult { # (Output...) + printAtLevel('result', @_); +} +sub printError { # (Output...) + printAtLevel('error', @_); +} +sub printWarning { # (Output...) + printAtLevel('warning', @_); +} +sub printInfo { # (Output...) + printAtLevel('info', @_); +} +sub printDebug { # (Output...) + printAtLevel('debug', @_); +} +sub printSpam { # (Output...) + printAtLevel('spam', @_); +} +sub enableAutomaticLineBreaks { # (Bool) -> (OldValue) + my $oldval = $ENABLE_AUTO_LINE_BREAKS; + $ENABLE_AUTO_LINE_BREAKS = ($_[0] ? 1 : 0); + return $oldval; +} + +######################## Optionally exported functions ####################### + +sub getTerminalWidth { # () -> ColumnCount + # May be undefined if someone prints before initialized + return (defined $TERMINAL_WIDTH ? $TERMINAL_WIDTH : 80); +} +sub getVerbosity { # () -> VerbosityLevel + return $VERBOSITY; +} +sub usingAnsiColors { # () -> Bool + return $ANSI_COLORS; +} +sub ansiColorsSupported { # () -> Bool + return $ANSI_COLORS_SUPPORTED; +} +sub setVerbosity { # (VerbosityLevel) + $VERBOSITY = $_[0]; +} + +################## Functions for unit tests to mock internals ################ + +sub setTerminalWidth { # (ColumnCount) + $TERMINAL_WIDTH = $_[0]; +} +sub setUseAnsiColors { # (Bool) + if ($ANSI_COLORS_SUPPORTED && $_[0]) { + $ANSI_COLORS = 1; + } else { + $ANSI_COLORS = 0; + } +} + +############## Utility functions - Not intended for external use ############# + +sub initialize { # () + my ($stdout, $stderr, $use_colors_by_default) = @_; + if (!defined $VERBOSITY) { + $VERBOSITY = &getDefaultVerbosity(); + } + $COLUMN_POSITION = 0; + $ENABLE_AUTO_LINE_BREAKS = 1; + %ATTRIBUTE_PREFIX = map { $_ => '' } keys %TYPES; + %ATTRIBUTE_POSTFIX = map { $_ => '' } keys %TYPES; + &setAttribute('error', COLOR_ERR, COLOR_RESET); + &setAttribute('warning', COLOR_WARN, COLOR_RESET); + &setAttribute('debug', COLOR_ANON, COLOR_RESET); + &setAttribute('spam', COLOR_ANON, COLOR_RESET); + %OUTPUT_STREAM = map { $_ => $stdout } keys %TYPES; + $OUTPUT_STREAM{'error'} = $stderr; + $OUTPUT_STREAM{'warning'} = $stderr; + if (defined $use_colors_by_default) { + $ANSI_COLORS_SUPPORTED = $use_colors_by_default; + $ANSI_COLORS = $ANSI_COLORS_SUPPORTED; + } else { + &detectTerminalColorSupport(); + } + if (!defined $TERMINAL_WIDTH) { + $TERMINAL_WIDTH = &detectTerminalWidth(); + } +} +sub setAttribute { # (type, prefox, postfix) + my ($type, $prefix, $postfix) = @_; + $ATTRIBUTE_PREFIX{$type} = $prefix; + $ATTRIBUTE_POSTFIX{$type} = $postfix; +} +sub stripAnsiEscapes { # (Line) -> (StrippedLine) + $_[0] =~ s/\e\[[^m]*m//g; + return $_[0]; +} +sub getDefaultVerbosity { # () -> VerbosityLevel + # We can not print at correct verbosity levels before argument parsing has + # completed. We try some simple arg parsing here assuming default options + # used to set verbosity, such that we likely guess correctly, allowing + # correct verbosity from the start. + my $default = 3; + foreach my $arg (@ARGV) { + if ($arg eq '--') { return $default; } + if ($arg =~ /^-([^-]+)/) { + my $optstring = $1; + while ($optstring =~ /^(.)(.*)$/) { + my $char = $1; + $optstring = $2; + if ($char eq 'v') { + ++$default; + } + if ($char eq 's') { + if ($default > 0) { + --$default; + } + } + } + } + } + return $default; +} +sub detectTerminalWidth { #() -> ColumnCount + my $cols = &checkConsoleFeature('cols'); + if (!defined $cols) { + printDebug "Assuming terminal width of 80.\n"; + return 80; + } + if ($cols =~ /^\d+$/ && $cols > 10 && $cols < 500) { + printDebug "Detected terminal width of $cols.\n"; + return $cols; + } else { + printDebug "Unexpected terminal width of '$cols' given. " + . "Assuming size of 80.\n"; + return 80; + } +} +sub detectTerminalColorSupport { # () -> Bool + my $colorcount = &checkConsoleFeature('colors'); + if (!defined $colorcount) { + $ANSI_COLORS_SUPPORTED = 0; + printDebug "Assuming no color support.\n"; + return 0; + } + if ($colorcount =~ /^\d+$/ && $colorcount >= 8) { + $ANSI_COLORS_SUPPORTED = 1; + if (!defined $ANSI_COLORS) { + $ANSI_COLORS = $ANSI_COLORS_SUPPORTED; + } + printDebug "Color support detected.\n"; + return 1; + } +} +sub checkConsoleFeature { # (Feature) -> Bool + my ($feature) = @_; + # Unit tests must mock. Can't depend on TERM being set. + assertNotUnitTest(); + if (!exists $ENV{'TERM'}) { + printDebug "Terminal not set. Unknown.\n"; + return; + } + if (-f '/usr/bin/tput') { + my ($fh, $result); + if (open ($fh, "tput $feature 2>/dev/null |")) { + $result = <$fh>; + close $fh; + } else { + printDebug "Failed to open tput pipe.\n"; + return; + } + if ($? != 0) { + printDebug "Failed tput call to detect feature $feature $!\n"; + return; + } + chomp $result; + #printSpam "Console feature $feature: '$result'\n"; + return $result; + } else { + printDebug "No tput binary. Dont know how to detect feature.\n"; + return; + } +} +sub printAtLevel { # (Level, Output...) + # Prints an array of data that may contain newlines + my $level = shift @_; + exists $TYPES{$level} or confess "Unknown print level '$level'."; + if ($TYPES{$level} > $VERBOSITY) { + return; + } + my $buffer = ''; + my $width = &getTerminalWidth(); + foreach my $printable (@_) { + my @lines = split(/\n/, $printable, -1); + my $current = 0; + for (my $i=0; $i < scalar @lines; ++$i) { + if ($i != 0) { + $buffer .= "\n"; + $COLUMN_POSITION = 0; + } + my $last = ($i + 1 == scalar @lines); + printLineAtLevel($level, $lines[$i], \$buffer, $last); + } + } + my $stream = $OUTPUT_STREAM{$level}; + print $stream $buffer; +} +sub printLineAtLevel { # (Level, Line, Buffer, Last) + # Prints a single line, which might still have to be broken into multiple + # lines + my ($level, $data, $buffer, $last) = @_; + if (!$ANSI_COLORS) { + $data = &stripAnsiEscapes($data); + } + my $width = &getTerminalWidth(); + while (1) { + my $remaining = $width - $COLUMN_POSITION; + if (&prefixLineWithLevel($level)) { + $remaining -= (2 + length $level); + } + if ($ENABLE_AUTO_LINE_BREAKS && $remaining < length $data) { + my $min = int (2 * $width / 3) - $COLUMN_POSITION; + if ($min < 1) { $min = 1; } + if ($data =~ /^(.{$min,$remaining}) (.*?)$/s) { + my ($first, $rest) = ($1, $2); + &printLinePartAtLevel($level, $first, $buffer); + $$buffer .= "\n"; + $data = $rest; + $COLUMN_POSITION = 0; + } else { + last; + } + } else { + last; + } + } + if (!$last || length $data > 0) { + &printLinePartAtLevel($level, $data, $buffer); + } +} +sub printLinePartAtLevel { # ($Level, Line, Buffer) + # Print a single line that should fit on one line + my ($level, $data, $buffer) = @_; + if ($ANSI_COLORS) { + $$buffer .= $ATTRIBUTE_PREFIX{$level}; + } + if (&prefixLineWithLevel($level)) { + $$buffer .= $level . ": "; + $COLUMN_POSITION = (length $level) + 2; + } + $$buffer .= $data; + $COLUMN_POSITION += length $data; + if ($ANSI_COLORS) { + $$buffer .= $ATTRIBUTE_POSTFIX{$level}; + } +} +sub prefixLineWithLevel { # (Level) -> Bool + my ($level) = @_; + return ($TYPES{$level} > 2 && $VERBOSITY >= 4 && $COLUMN_POSITION == 0); +} + |