| Index: build/android/adb_logcat_printer.py
|
| diff --git a/build/android/adb_logcat_printer.py b/build/android/adb_logcat_printer.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..6ea47975e4bbba5510b1c9dddd95bfbaef67c443
|
| --- /dev/null
|
| +++ b/build/android/adb_logcat_printer.py
|
| @@ -0,0 +1,202 @@
|
| +#!/usr/bin/python
|
| +#
|
| +# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +"""Shutdown adb_logcat_monitor and print accumulated logs.
|
| +
|
| +To test, call './adb_logcat_printer.py <base_dir>' where
|
| +<base_dir> contains 'adb logcat -v threadtime' files named as
|
| +logcat_<deviceID>_<sequenceNum>
|
| +
|
| +The script will print the files to out, and will combine multiple
|
| +logcats from a single device if there is overlap.
|
| +
|
| +Additionally, if a <base_dir>/LOGCAT_MONITOR_PID exists, the script
|
| +will attempt to terminate the contained PID by sending a SIGINT and
|
| +monitoring for the deletion of the aforementioned file.
|
| +"""
|
| +
|
| +import cStringIO
|
| +import logging
|
| +import os
|
| +import re
|
| +import signal
|
| +import sys
|
| +import time
|
| +
|
| +
|
| +# Set this to debug for more verbose output
|
| +LOG_LEVEL = logging.INFO
|
| +
|
| +
|
| +def CombineLogFiles(list_of_lists, logger):
|
| + """Splices together multiple logcats from the same device.
|
| +
|
| + Args:
|
| + list_of_lists: list of pairs (filename, list of timestamped lines)
|
| + logger: handler to log events
|
| +
|
| + Returns:
|
| + list of lines with duplicates removed
|
| + """
|
| + cur_device_log = ['']
|
| + for cur_file, cur_file_lines in list_of_lists:
|
| + # Ignore files with just the logcat header
|
| + if len(cur_file_lines) < 2:
|
| + continue
|
| + common_index = 0
|
| + # Skip this step if list just has empty string
|
| + if len(cur_device_log) > 1:
|
| + try:
|
| + line = cur_device_log[-1]
|
| + # Used to make sure we only splice on a timestamped line
|
| + if re.match('^\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3} ', line):
|
| + common_index = cur_file_lines.index(line)
|
| + else:
|
| + logger.warning('splice error - no timestamp in "%s"?', line.strip())
|
| + except ValueError:
|
| + # The last line was valid but wasn't found in the next file
|
| + cur_device_log += ['***** POSSIBLE INCOMPLETE LOGCAT *****']
|
| + logger.info('Unable to splice %s. Incomplete logcat?', cur_file)
|
| +
|
| + cur_device_log += ['*'*30 + ' %s' % cur_file]
|
| + cur_device_log.extend(cur_file_lines[common_index:])
|
| +
|
| + return cur_device_log
|
| +
|
| +
|
| +def FindLogFiles(base_dir):
|
| + """Search a directory for logcat files.
|
| +
|
| + Args:
|
| + base_dir: directory to search
|
| +
|
| + Returns:
|
| + Mapping of device_id to a sorted list of file paths for a given device
|
| + """
|
| + logcat_filter = re.compile('^logcat_(\w+)_(\d+)$')
|
| + # list of tuples (<device_id>, <seq num>, <full file path>)
|
| + filtered_list = []
|
| + for cur_file in os.listdir(base_dir):
|
| + matcher = logcat_filter.match(cur_file)
|
| + if matcher:
|
| + filtered_list += [(matcher.group(1), int(matcher.group(2)),
|
| + os.path.join(base_dir, cur_file))]
|
| + filtered_list.sort()
|
| + file_map = {}
|
| + for device_id, _, cur_file in filtered_list:
|
| + if not device_id in file_map:
|
| + file_map[device_id] = []
|
| +
|
| + file_map[device_id] += [cur_file]
|
| + return file_map
|
| +
|
| +
|
| +def GetDeviceLogs(log_filenames, logger):
|
| + """Read log files, combine and format.
|
| +
|
| + Args:
|
| + log_filenames: mapping of device_id to sorted list of file paths
|
| + logger: logger handle for logging events
|
| +
|
| + Returns:
|
| + list of formatted device logs, one for each device.
|
| + """
|
| + device_logs = []
|
| +
|
| + for device, device_files in log_filenames.iteritems():
|
| + logger.debug('%s: %s', device, str(device_files))
|
| + device_file_lines = []
|
| + for cur_file in device_files:
|
| + with open(cur_file) as f:
|
| + device_file_lines += [(cur_file, f.read().splitlines())]
|
| + combined_lines = CombineLogFiles(device_file_lines, logger)
|
| + # Prepend each line with a short unique ID so it's easy to see
|
| + # when the device changes. We don't use the start of the device
|
| + # ID because it can be the same among devices. Example lines:
|
| + # AB324: foo
|
| + # AB324: blah
|
| + device_logs += [('\n' + device[-5:] + ': ').join(combined_lines)]
|
| + return device_logs
|
| +
|
| +
|
| +def ShutdownLogcatMonitor(base_dir, logger):
|
| + """Attempts to shutdown adb_logcat_monitor and blocks while waiting."""
|
| + try:
|
| + monitor_pid_path = os.path.join(base_dir, 'LOGCAT_MONITOR_PID')
|
| + with open(monitor_pid_path) as f:
|
| + monitor_pid = int(f.readline())
|
| +
|
| + logger.info('Sending SIGTERM to %d', monitor_pid)
|
| + os.kill(monitor_pid, signal.SIGTERM)
|
| + i = 0
|
| + while True:
|
| + time.sleep(.2)
|
| + if not os.path.exists(monitor_pid_path):
|
| + return
|
| + if not os.path.exists('/proc/%d' % monitor_pid):
|
| + logger.warning('Monitor (pid %d) terminated uncleanly?', monitor_pid)
|
| + return
|
| + logger.info('Waiting for logcat process to terminate.')
|
| + i += 1
|
| + if i >= 10:
|
| + logger.warning('Monitor pid did not terminate. Continuing anyway.')
|
| + return
|
| +
|
| + except (ValueError, IOError, OSError):
|
| + logger.exception('Error signaling logcat monitor - continuing')
|
| +
|
| +
|
| +def main(base_dir, output_file):
|
| + log_stringio = cStringIO.StringIO()
|
| + logger = logging.getLogger('LogcatPrinter')
|
| + logger.setLevel(LOG_LEVEL)
|
| + sh = logging.StreamHandler(log_stringio)
|
| + sh.setFormatter(logging.Formatter('%(asctime)-2s %(levelname)-8s'
|
| + ' %(message)s'))
|
| + logger.addHandler(sh)
|
| +
|
| + try:
|
| + # Wait at least 5 seconds after base_dir is created before printing.
|
| + #
|
| + # The idea is that 'adb logcat > file' output consists of 2 phases:
|
| + # 1 Dump all the saved logs to the file
|
| + # 2 Stream log messages as they are generated
|
| + #
|
| + # We want to give enough time for phase 1 to complete. There's no
|
| + # good method to tell how long to wait, but it usually only takes a
|
| + # second. On most bots, this code path won't occur at all, since
|
| + # adb_logcat_monitor.py command will have spawned more than 5 seconds
|
| + # prior to called this shell script.
|
| + try:
|
| + sleep_time = 5 - (time.time() - os.path.getctime(base_dir))
|
| + except OSError:
|
| + sleep_time = 5
|
| + if sleep_time > 0:
|
| + logger.warning('Monitor just started? Sleeping %.1fs', sleep_time)
|
| + time.sleep(sleep_time)
|
| +
|
| + assert os.path.exists(base_dir), '%s does not exist' % base_dir
|
| + ShutdownLogcatMonitor(base_dir, logger)
|
| + separator = '\n' + '*' * 80 + '\n\n'
|
| + for log in GetDeviceLogs(FindLogFiles(base_dir), logger):
|
| + output_file.write(log)
|
| + output_file.write(separator)
|
| + with open(os.path.join(base_dir, 'eventlog')) as f:
|
| + output_file.write('\nLogcat Monitor Event Log\n')
|
| + output_file.write(f.read())
|
| + except:
|
| + logger.exception('Unexpected exception')
|
| +
|
| + logger.info('Done.')
|
| + sh.flush()
|
| + output_file.write('\nLogcat Printer Event Log\n')
|
| + output_file.write(log_stringio.getvalue())
|
| +
|
| +if __name__ == '__main__':
|
| + if len(sys.argv) == 1:
|
| + print 'Usage: %s <base_dir>' % sys.argv[0]
|
| + sys.exit(1)
|
| + sys.exit(main(sys.argv[1], sys.stdout))
|
|
|