|  | #!/usr/bin/env python | 
|  | # Copyright 2017 The Fuchsia Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  | """flog is a developer friendly log listener with automatic crash symbol decoder. | 
|  |  | 
|  | The crash is archived to /tmp/fuchsia-crash or a specified directory. | 
|  | Configurable keywords or lines with them can be color coded. | 
|  | """ | 
|  |  | 
|  | import argparse | 
|  | import os | 
|  | import re | 
|  | import subprocess | 
|  | import sys | 
|  | from time import gmtime | 
|  | from time import strftime | 
|  |  | 
|  | CASE_SENSITIVE = False | 
|  | BEGINS = [] | 
|  | ENDS = [] | 
|  | SUPPRESSES = [] | 
|  | CRASH_DIR = '/tmp/fuchsia_crash' | 
|  |  | 
|  | # TODO(porce): Support regular expression | 
|  | # TODO(porce): Support compatibility with REGEX_ regex expressions | 
|  | COLOR_LINES = {'error': 'red'} | 
|  | COLOR_WORDS = { | 
|  | 'warn': 'white-on-red', | 
|  | 'error': 'white-on-red', | 
|  | 'fail': 'white-on-red', | 
|  | 'exception': 'white-on-red', | 
|  | 'address': 'green', | 
|  | 'did not add device in bind': 'green', | 
|  | } | 
|  |  | 
|  | RESET = '\033[1;0m' | 
|  |  | 
|  | COLORS = { | 
|  | 'WHITE-ON-RED': '\033[41;37m', | 
|  | 'BLACK': '\033[30;1m', | 
|  | 'RED': '\033[31;1m', | 
|  | 'GREEN': '\033[32;1m', | 
|  | 'YELLOW': '\033[33;1m', | 
|  | 'BLUE': '\033[34;1m', | 
|  | 'MAGENTA': '\033[35;1m', | 
|  | 'CYAN': '\033[36m;1m', | 
|  | } | 
|  |  | 
|  | FIRST_LOG_AFTER_BOOTUP = '[00000.000] 00000.00000> bootdata:' | 
|  |  | 
|  | # REGEX_* are constructed when the command line arguments are parsed. | 
|  | REGEX_BEGINS = '' | 
|  | REGEX_ENDS = '' | 
|  | REGEX_SUPPRESS = '' | 
|  | REGEX_COLOR_LINES = '' | 
|  | REGEX_COLOR_WORDS = '' | 
|  |  | 
|  |  | 
|  | def static_vars(**kwargs): | 
|  |  | 
|  | def decorate(func): | 
|  | for k in kwargs: | 
|  | setattr(func, k, kwargs[k]) | 
|  | return func | 
|  |  | 
|  | return decorate | 
|  |  | 
|  |  | 
|  | def now_str(): | 
|  | return strftime('%Y%m%d_%H%M%S', gmtime()) | 
|  |  | 
|  |  | 
|  | def get_log_listener(args): | 
|  | # return 'cat /tmp/z' # Unit test | 
|  | listener = 'fx log' | 
|  | return '{} {}'.format(listener, ' '.join(x for x in args)) | 
|  |  | 
|  |  | 
|  | @static_vars(crash_dump=[]) | 
|  | @static_vars(is_crashing=False) | 
|  | def monitor_crash(line): | 
|  | if '<==' in line and 'exception' in line: | 
|  | monitor_crash.is_crashing = True | 
|  |  | 
|  | if monitor_crash.is_crashing is True: | 
|  | monitor_crash.crash_dump.append(line) | 
|  |  | 
|  | if ': end' in line and 'bt#' in line: | 
|  | decode_backtrace(monitor_crash.crash_dump, CRASH_DIR) | 
|  | monitor_crash.crash_dump = [] | 
|  | monitor_crash.is_crashing = False | 
|  |  | 
|  |  | 
|  | def color(string, color_name): | 
|  | if color_name.upper() not in COLORS: | 
|  | return string | 
|  |  | 
|  | ends_in_newline = string.endswith('\n') | 
|  |  | 
|  | if ends_in_newline: | 
|  | string = string[:-1] | 
|  |  | 
|  | result = '{}{}{}'.format(COLORS[color_name.upper()], string, RESET) | 
|  |  | 
|  | if ends_in_newline: | 
|  | result += '\n' | 
|  |  | 
|  | return result | 
|  |  | 
|  |  | 
|  | def anymatch(test_string, regexes): | 
|  | global CASE_SENSITIVE | 
|  | if CASE_SENSITIVE: | 
|  | ans = re.search(regexes, test_string) | 
|  | else: | 
|  | ans = re.search(regexes, test_string, flags=re.IGNORECASE) | 
|  |  | 
|  | return ans.group(0) if ans else None | 
|  |  | 
|  |  | 
|  | @static_vars(is_in_session=False) | 
|  | def is_suppressed(line): | 
|  | """Test if the log may be printed or not. | 
|  |  | 
|  | Args: | 
|  | line (str): A log line. | 
|  |  | 
|  | Returns: | 
|  | True if the log line should be suppressed. False otherwise. | 
|  | """ | 
|  | if line.startswith(FIRST_LOG_AFTER_BOOTUP): | 
|  | # A reboot occurs. Reset to default. | 
|  | is_suppressed.is_in_session = False | 
|  |  | 
|  | if not BEGINS or anymatch(line, REGEX_BEGINS): | 
|  | if not is_suppressed.is_in_session: | 
|  | print '\n' * 3 | 
|  | is_suppressed.is_in_session = True | 
|  |  | 
|  | if anymatch(line, REGEX_ENDS): | 
|  | is_suppressed.is_in_session = False | 
|  |  | 
|  | if not is_suppressed.is_in_session: | 
|  | return True | 
|  |  | 
|  | if anymatch(line, REGEX_SUPPRESSES): | 
|  | return True | 
|  |  | 
|  | return False | 
|  |  | 
|  |  | 
|  | def colorize(line_incoming): | 
|  | """Color-code the log line. | 
|  |  | 
|  | Args: | 
|  | line_incoming: log line. | 
|  |  | 
|  | Returns: | 
|  | color-coded log line | 
|  | """ | 
|  |  | 
|  | line = line_incoming | 
|  |  | 
|  | k = anymatch(line, REGEX_COLOR_LINES) | 
|  | if k: | 
|  | v = COLOR_LINES.get(k.lower(), '') | 
|  | line = color(line, v) | 
|  | return line | 
|  |  | 
|  | if anymatch(line, REGEX_COLOR_WORDS): | 
|  | # TODO(porce): Maybe there are less costly ways. | 
|  | for k, v in COLOR_WORDS.iteritems(): | 
|  | replacement = r'%s\1%s' % (COLORS[v.upper()], RESET) | 
|  | search = '(?i)(' + '|'.join(map(re.escape, [k])) + ')' | 
|  | line = re.sub(search, replacement, line) | 
|  |  | 
|  | return line | 
|  |  | 
|  |  | 
|  | def print_log(line): | 
|  | if is_suppressed(line): | 
|  | return | 
|  |  | 
|  | print colorize(line), | 
|  |  | 
|  |  | 
|  | def hijack_stdout(cmd): | 
|  | proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) | 
|  | return iter(proc.stdout.readline, '') | 
|  |  | 
|  |  | 
|  | def get_decode_cmd(): | 
|  | decode_cmd = 'fx symbolize' | 
|  | return decode_cmd | 
|  |  | 
|  |  | 
|  | def print_bt(line): | 
|  | """Print backtrace with some colors. | 
|  |  | 
|  | Args: | 
|  | line (str): Backtrace line. | 
|  | """ | 
|  | tokens = line.split(' at ') | 
|  |  | 
|  | methods = tokens[:-1] | 
|  | methods.append('') | 
|  | methods_text = ' at '.join(x for x in methods) | 
|  |  | 
|  | path = tokens[-1] | 
|  | path_tokens = path.split('/') | 
|  | paths = path_tokens[:-1] | 
|  | paths.append('') | 
|  |  | 
|  | path_text = '/'.join(x for x in paths) | 
|  | path_colored = path_text + color(path_tokens[-1], 'red') | 
|  |  | 
|  | print methods_text, path_colored, | 
|  |  | 
|  |  | 
|  | def decode_backtrace(crash_dump, dst_dir): | 
|  | """A wrapper to fsymbolize. | 
|  |  | 
|  | Args: | 
|  | crash_dump (array): array of log lines. | 
|  | dst_dir    (str): directory to archive to. | 
|  | """ | 
|  | os.system('mkdir -p {}'.format(dst_dir)) | 
|  | crash_file_path = '{}/{}.crash'.format(dst_dir, now_str()) | 
|  | tmp_file = '{}/tmp.dump'.format(dst_dir) | 
|  |  | 
|  | f = open(tmp_file, 'w') | 
|  | for line in crash_dump: | 
|  | f.write(line) | 
|  | f.close() | 
|  |  | 
|  | cmd = '{} < {} > {}'.format(get_decode_cmd(), tmp_file, crash_file_path) | 
|  | os.system(cmd) | 
|  |  | 
|  | print '\n\n' | 
|  |  | 
|  | is_start = False | 
|  | with open(crash_file_path, 'r') as f: | 
|  | for line in f: | 
|  | if 'start of symbolized stack:' in line: | 
|  | is_start = True | 
|  |  | 
|  | if is_start: | 
|  | print_bt(line) | 
|  | print '\n\n' | 
|  |  | 
|  |  | 
|  | def parse_color_map(string): | 
|  | """Converted comma separated text into a color code map. | 
|  |  | 
|  | Args: | 
|  | string (str): a text line | 
|  |  | 
|  | Returns: | 
|  | Dictionary whose key is a text pattern and value is the color name. | 
|  | """ | 
|  | m = {} | 
|  | items = string.split(',') | 
|  | for item in items: | 
|  | sep = ':' | 
|  | if sep not in item: | 
|  | continue | 
|  |  | 
|  | idx = item.rfind(sep) | 
|  | text = item[:idx] | 
|  | color_name = item[idx + 1:] | 
|  |  | 
|  | if text.__len__() == 0: | 
|  | continue | 
|  |  | 
|  | m[text] = color_name | 
|  |  | 
|  | return m | 
|  |  | 
|  |  | 
|  | def proc_cmdline(): | 
|  | """Argument parser. | 
|  |  | 
|  | Returns: | 
|  | args. | 
|  | """ | 
|  | example_commands = """ | 
|  |  | 
|  | Pro tip: Use comma separated texts for multiple matches | 
|  |  | 
|  | Example: | 
|  | $ flog --begin \'my module starts,rare event\' | 
|  | --end \'my module ends\' | 
|  | --suppress \'verbose,chatty\' | 
|  | --lines \'error msg:red,warn:blue\' | 
|  | --words \'register 0x00:green,exit:yellow\' | 
|  |  | 
|  | """ | 
|  |  | 
|  | p = argparse.ArgumentParser( | 
|  | description='A friendly Fuchsia log listener', | 
|  | epilog=example_commands, | 
|  | formatter_class=argparse.RawDescriptionHelpFormatter) | 
|  |  | 
|  | p.add_argument('--begin', type=str, help='trigger texts to start logging') | 
|  | p.add_argument('--end', type=str, help='trigger texts to end logging') | 
|  |  | 
|  | p.add_argument( | 
|  | '--case', type=bool, help='match case-sensitively', default=False) | 
|  |  | 
|  | p.add_argument('--suppress', type=str, help='text to suppress the line') | 
|  | p.add_argument('--lines', type=str, help='colorize the line. {text:color}') | 
|  | p.add_argument('--words', type=str, help='colorize the word. {text:color}') | 
|  | p.add_argument('--crashdir', type=str, help='directory to store crash files.') | 
|  | p.add_argument( | 
|  | 'remainders', | 
|  | nargs=argparse.REMAINDER, | 
|  | help='arguments passed to loglistener') | 
|  |  | 
|  | args = p.parse_args() | 
|  |  | 
|  | global CASE_SENSITIVE | 
|  | global BEGINS | 
|  | global ENDS | 
|  | global SUPPRESSES | 
|  | global COLOR_LINES | 
|  | global COLOR_WORDS | 
|  | global CRASH_DIR | 
|  |  | 
|  | CASE_SENSITIVE = args.case | 
|  | if args.begin: | 
|  | BEGINS.extend(args.begin.split(',')) | 
|  | if args.end: | 
|  | ENDS.extend(args.end.split(',')) | 
|  | if args.suppress: | 
|  | SUPPRESSES.extend(args.suppress.split(',')) | 
|  | if args.lines: | 
|  | COLOR_LINES.update(parse_color_map(args.lines)) | 
|  | if args.words: | 
|  | COLOR_WORDS.update(parse_color_map(args.words)) | 
|  | if args.crashdir: | 
|  | CRASH_DIR = args.crashdir | 
|  |  | 
|  | global REGEX_BEGINS | 
|  | global REGEX_ENDS | 
|  | global REGEX_SUPPRESSES | 
|  | global REGEX_COLOR_LINES | 
|  | global REGEX_COLOR_WORDS | 
|  |  | 
|  | # TODO(porce): Support regex input | 
|  | REGEX_BEGINS = '|'.join(BEGINS) | 
|  | REGEX_ENDS = '|'.join(ENDS) | 
|  | REGEX_SUPPRESSES = '|'.join(SUPPRESSES) | 
|  | REGEX_COLOR_LINES = '|'.join(k for k, v in COLOR_LINES.iteritems()) | 
|  | REGEX_COLOR_WORDS = '|'.join(k for k, v in COLOR_WORDS.iteritems()) | 
|  |  | 
|  | return args | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | args = proc_cmdline() | 
|  | cmd = get_log_listener(args.remainders) | 
|  | for line in hijack_stdout(cmd): | 
|  | print_log(line) | 
|  | monitor_crash(line) | 
|  |  | 
|  |  | 
|  | main() |