| #!/usr/bin/env 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. | |
| """Saves logcats from all connected devices. | |
| Usage: adb_logcat_monitor.py <base_dir> [<adb_binary_path>] | |
| This script will repeatedly poll adb for new devices and save logcats | |
| inside the <base_dir> directory, which it attempts to create. The | |
| script will run until killed by an external signal. To test, run the | |
| script in a shell and <Ctrl>-C it after a while. It should be | |
| resilient across phone disconnects and reconnects and start the logcat | |
| early enough to not miss anything. | |
| """ | |
| import logging | |
| import os | |
| import re | |
| import shutil | |
| import signal | |
| import subprocess | |
| import sys | |
| import time | |
| # Map from device_id -> (process, logcat_num) | |
| devices = {} | |
| class TimeoutException(Exception): | |
| """Exception used to signal a timeout.""" | |
| pass | |
| class SigtermError(Exception): | |
| """Exception used to catch a sigterm.""" | |
| pass | |
| def StartLogcatIfNecessary(device_id, adb_cmd, base_dir): | |
| """Spawns a adb logcat process if one is not currently running.""" | |
| process, logcat_num = devices[device_id] | |
| if process: | |
| if process.poll() is None: | |
| # Logcat process is still happily running | |
| return | |
| else: | |
| logging.info('Logcat for device %s has died', device_id) | |
| error_filter = re.compile('- waiting for device -') | |
| for line in process.stderr: | |
| if not error_filter.match(line): | |
| logging.error(device_id + ': ' + line) | |
| logging.info('Starting logcat %d for device %s', logcat_num, | |
| device_id) | |
| logcat_filename = 'logcat_%s_%03d' % (device_id, logcat_num) | |
| logcat_file = open(os.path.join(base_dir, logcat_filename), 'w') | |
| process = subprocess.Popen([adb_cmd, '-s', device_id, | |
| 'logcat', '-v', 'threadtime'], | |
| stdout=logcat_file, | |
| stderr=subprocess.PIPE) | |
| devices[device_id] = (process, logcat_num + 1) | |
| def GetAttachedDevices(adb_cmd): | |
| """Gets the device list from adb. | |
| We use an alarm in this function to avoid deadlocking from an external | |
| dependency. | |
| Args: | |
| adb_cmd: binary to run adb | |
| Returns: | |
| list of devices or an empty list on timeout | |
| """ | |
| signal.alarm(2) | |
| try: | |
| out, err = subprocess.Popen([adb_cmd, 'devices'], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE).communicate() | |
| if err: | |
| logging.warning('adb device error %s', err.strip()) | |
| return re.findall('^(\w+)\tdevice$', out, re.MULTILINE) | |
| except TimeoutException: | |
| logging.warning('"adb devices" command timed out') | |
| return [] | |
| except (IOError, OSError): | |
| logging.exception('Exception from "adb devices"') | |
| return [] | |
| finally: | |
| signal.alarm(0) | |
| def main(base_dir, adb_cmd='adb'): | |
| """Monitor adb forever. Expects a SIGINT (Ctrl-C) to kill.""" | |
| # We create the directory to ensure 'run once' semantics | |
| if os.path.exists(base_dir): | |
| print 'adb_logcat_monitor: %s already exists? Cleaning' % base_dir | |
| shutil.rmtree(base_dir, ignore_errors=True) | |
| os.makedirs(base_dir) | |
| logging.basicConfig(filename=os.path.join(base_dir, 'eventlog'), | |
| level=logging.INFO, | |
| format='%(asctime)-2s %(levelname)-8s %(message)s') | |
| # Set up the alarm for calling 'adb devices'. This is to ensure | |
| # our script doesn't get stuck waiting for a process response | |
| def TimeoutHandler(_, unused_frame): | |
| raise TimeoutException() | |
| signal.signal(signal.SIGALRM, TimeoutHandler) | |
| # Handle SIGTERMs to ensure clean shutdown | |
| def SigtermHandler(_, unused_frame): | |
| raise SigtermError() | |
| signal.signal(signal.SIGTERM, SigtermHandler) | |
| logging.info('Started with pid %d', os.getpid()) | |
| pid_file_path = os.path.join(base_dir, 'LOGCAT_MONITOR_PID') | |
| try: | |
| with open(pid_file_path, 'w') as f: | |
| f.write(str(os.getpid())) | |
| while True: | |
| for device_id in GetAttachedDevices(adb_cmd): | |
| if not device_id in devices: | |
| devices[device_id] = (None, 0) | |
| for device in devices: | |
| # This will spawn logcat watchers for any device ever detected | |
| StartLogcatIfNecessary(device, adb_cmd, base_dir) | |
| time.sleep(5) | |
| except SigtermError: | |
| logging.info('Received SIGTERM, shutting down') | |
| except: | |
| logging.exception('Unexpected exception in main.') | |
| finally: | |
| for process, _ in devices.itervalues(): | |
| if process: | |
| try: | |
| process.terminate() | |
| except OSError: | |
| pass | |
| os.remove(pid_file_path) | |
| if __name__ == '__main__': | |
| if 2 <= len(sys.argv) <= 3: | |
| print 'adb_logcat_monitor: Initializing' | |
| sys.exit(main(*sys.argv[1:3])) | |
| print 'Usage: %s <base_dir> [<adb_binary_path>]' % sys.argv[0] |