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