diff --git a/overviewer.py b/overviewer.py index 6992fdc..dc3a05c 100755 --- a/overviewer.py +++ b/overviewer.py @@ -49,8 +49,6 @@ import time import logging from overviewer_core import util -logging.basicConfig(level=logging.INFO,format="%(asctime)s [%(levelname)s] %(message)s") - this_dir = util.get_program_path() # make sure the c_overviewer extension is available @@ -108,8 +106,34 @@ from overviewer_core import googlemap, rendernode helptext = """ %prog [OPTIONS] """ +def configure_logger(): + "Configures the root logger to our liking" + + outstream = sys.stderr + if platform.system() == 'Windows': + # Our custom output stream processor knows how to deal with select ANSI + # color escape sequences + outstream = util.WindowsOutputStream() + formatter = util.ANSIColorFormatter() + + elif sys.stderr.isatty(): + # terminal logging with ANSI color + formatter = util.ANSIColorFormatter() + + else: + # Let's not assume anything. Just text. + formatter = util.DumbFormatter() + + logger = logging.getLogger() + handler = logging.StreamHandler(outstream) + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging.INFO) def main(): + + configure_logger() + try: cpus = multiprocessing.cpu_count() except NotImplementedError: diff --git a/overviewer_core/util.py b/overviewer_core/util.py index 2e51a55..4506db3 100644 --- a/overviewer_core/util.py +++ b/overviewer_core/util.py @@ -22,6 +22,10 @@ import os import os.path import sys from subprocess import Popen, PIPE +import logging +from cStringIO import StringIO +import ctypes +import platform def get_program_path(): if hasattr(sys, "frozen") or imp.is_frozen("__main__"): @@ -74,3 +78,229 @@ def findGitVersion(): return overviewer_version.VERSION except Exception: return "unknown" + + +# Logging related classes are below + +# Some cool code for colored logging: +# For background, add 40. For foreground, add 30 +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) + +RESET_SEQ = "\033[0m" +COLOR_SEQ = "\033[1;%dm" +BOLD_SEQ = "\033[1m" + +# Windows colors, taken from WinCon.h +FOREGROUND_BLUE = 0x01 +FOREGROUND_GREEN = 0x02 +FOREGROUND_RED = 0x04 +FOREGROUND_BOLD = 0x08 +FOREGROUND_WHITE = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED + +BACKGROUND_BLACK = 0x00 +BACKGROUND_BLUE = 0x10 +BACKGROUND_GREEN = 0x20 +BACKGROUND_RED = 0x40 + +COLORIZE = { + #'INFO': WHITe, + 'DEBUG': CYAN, +} +HIGHLIGHT = { + 'CRITICAL': RED, + 'ERROR': RED, + 'WARNING': YELLOW, +} + + +class WindowsOutputStream(object): + """A file-like object that proxies sys.stderr and interprets simple ANSI + escape codes for color, translating them to the appropriate Windows calls. + + """ + def __init__(self, stream=None): + assert platform.system() == 'Windows' + self.stream = stream or sys.stderr + + # go go gadget ctypes + self.GetStdHandle = ctypes.windll.kernel32.GetStdHandle + self.SetConsoleTextAttribute = ctypes.windll.kernel32.SetConsoleTextAttribute + self.STD_OUTPUT_HANDLE = ctypes.c_int(0xFFFFFFF5) + self.output_handle = self.GetStdHandle(self.STD_OUTPUT_HANDLE) + if self.output_handle == 0xFFFFFFFF: + raise Exception("Something failed in WindowsColorFormatter") + + + # default is white text on a black background + self.currentForeground = FOREGROUND_WHITE + self.currentBackground = BACKGROUND_BLACK + self.currentBold = 0 + + def updateWinColor(self, Fore=None, Back=None, Bold=False): + if Fore != None: self.currentForeground = Fore + if Back != None: self.currentBackground = Back + if Bold: + self.currentBold = FOREGROUND_BOLD + else: + self.currentBold = 0 + + self.SetConsoleTextAttribute(self.output_handle, + ctypes.c_int(self.currentForeground | self.currentBackground | self.currentBold)) + + def write(self, s): + + msg_strm = StringIO(s) + + while (True): + c = msg_strm.read(1) + if c == '': break + if c == '\033': + c1 = msg_strm.read(1) + if c1 != '[': # + sys.stream.write(c + c1) + continue + c2 = msg_strm.read(2) + if c2 == "0m": # RESET_SEQ + self.updateWinColor(Fore=FOREGROUND_WHITE, Back=BACKGROUND_BLACK) + + elif c2 == "1;": + color = "" + while(True): + nc = msg_strm.read(1) + if nc == 'm': break + color += nc + color = int(color) + if (color >= 40): # background + color = color - 40 + if color == BLACK: + self.updateWinColor(Back=BACKGROUND_BLACK) + if color == RED: + self.updateWinColor(Back=BACKGROUND_RED) + elif color == GREEN: + self.updateWinColor(Back=BACKGROUND_GREEN) + elif color == YELLOW: + self.updateWinColor(Back=BACKGROUND_RED | BACKGROUND_GREEN) + elif color == BLUE: + self.updateWinColor(Back=BACKGROUND_BLUE) + elif color == MAGENTA: + self.updateWinColor(Back=BACKGROUND_RED | BACKGROUND_BLUE) + elif color == CYAN: + self.updateWinColor(Back=BACKGROUND_GREEN | BACKGROUND_BLUE) + elif color == WHITE: + self.updateWinColor(Back=BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE) + elif (color >= 30): # foreground + color = color - 30 + if color == BLACK: + self.updateWinColor(Fore=FOREGROUND_BLACK) + if color == RED: + self.updateWinColor(Fore=FOREGROUND_RED) + elif color == GREEN: + self.updateWinColor(Fore=FOREGROUND_GREEN) + elif color == YELLOW: + self.updateWinColor(Fore=FOREGROUND_RED | FOREGROUND_GREEN) + elif color == BLUE: + self.updateWinColor(Fore=FOREGROUND_BLUE) + elif color == MAGENTA: + self.updateWinColor(Fore=FOREGROUND_RED | FOREGROUND_BLUE) + elif color == CYAN: + self.updateWinColor(Fore=FOREGROUND_GREEN | FOREGROUND_BLUE) + elif color == WHITE: + self.updateWinColor(Fore=FOREGROUND_WHITE) + + + + elif c2 == "1m": # BOLD_SEQ + pass + + else: + self.stream.write(c) + + + + def flush(self): + self.stream.flush() + +class HighlightingFormatter(logging.Formatter): + """Base class of our custom formatter + + """ + fmtstr = '%(fileandlineno)-18s:PID(%(pid)s):%(asctime)s ' \ + '%(levelname)-8s %(message)s' + datefmt = "%H:%M:%S" + funcName_len = 15 + + def __init__(self): + logging.Formatter.__init__(self, self.fmtstr, self.datefmt) + + def format(self, record): + """Add a few extra options to the record + + pid + The process ID + + fileandlineno + A combination filename:linenumber string, so it can be justified as + one entry in a format string. + + funcName + The function name truncated/padded to a fixed width characters + + """ + record.pid = os.getpid() + record.fileandlineno = "%s:%s" % (record.filename, record.lineno) + + # Set the max length for the funcName field, and left justify + l = self.funcName_len + record.funcName = ("%-" + str(l) + 's') % record.funcName[:l] + + return self.highlight(record) + + def highlight(self, record): + """This method applies any special effects such as colorization. It + should modify the records in the record object, and should return the + *formatted line*. This probably involves calling + logging.Formatter.format() + + Override this in subclasses + + """ + return logging.Formatter.format(self, record) + +class DumbFormatter(HighlightingFormatter): + """Formatter for dumb terminals that don't support color, or log files. + Prints a bunch of stars before a highlighted line. + + """ + def highlight(self, record): + if record.levelname in HIGHLIGHT: + line = logging.Formatter.format(self, record) + line = "*" * min(79,len(line)) + "\n" + line + return line + else: + return super(DumbFormatter, self).highlight(record) + + +class ANSIColorFormatter(HighlightingFormatter): + """Uses ANSI escape sequences to enable GLORIOUS EXTRA-COLOR! + + """ + def highlight(self, record): + if record.levelname in COLORIZE: + # Colorize just the levelname + # left justify again because the color sequence bumps the length up + # above 8 chars + levelname_color = COLOR_SEQ % (30 + COLORIZE[record.levelname]) + \ + "%-8s" % record.levelname + RESET_SEQ + record.levelname = levelname_color + return logging.Formatter.format(self, record) + + elif record.levelname in HIGHLIGHT: + # Colorize the entire line + line = logging.Formatter.format(self, record) + line = COLOR_SEQ % (40 + HIGHLIGHT[record.levelname]) + line + \ + RESET_SEQ + return line + + else: + # No coloring if it's not to be highlighted or colored + return logging.Formatter.format(self, record)