| #!/usr/bin/env python3 |
| # |
| # Copyright 2012 The Chromium Authors |
| # 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. |
| """ |
| # pylint: disable=W0702 |
| |
| import argparse |
| import io |
| 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(r'^\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(r'^logcat_(\S+)_(\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 device_id not 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.items(): |
| logger.debug('%s: %s', device, str(device_files)) |
| device_file_lines = [] |
| for cur_file in device_files: |
| with open(cur_file, encoding='latin1') 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(argv): |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| '--output-path', |
| help='Output file path (if unspecified, prints to stdout)') |
| parser.add_argument('log_dir') |
| args = parser.parse_args(argv) |
| base_dir = args.log_dir |
| |
| log_stringio = io.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) |
| |
| if args.output_path: |
| if not os.path.exists(os.path.dirname(args.output_path)): |
| logger.warning('Output dir %s doesn\'t exist. Creating it.', |
| os.path.dirname(args.output_path)) |
| os.makedirs(os.path.dirname(args.output_path)) |
| output_file = open(args.output_path, 'w') |
| logger.info( |
| 'Dumping logcat to local file %s. If running in a build, ' |
| 'this file will likely will be uploaded to google storage ' |
| 'in a later step. It can be downloaded from there.', args.output_path) |
| else: |
| output_file = sys.stdout |
| |
| 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__': |
| sys.exit(main(sys.argv[1:])) |