1#!/usr/bin/env python 2# 3# Copyright (c) 2012 The Chromium Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Saves logcats from all connected devices. 8 9Usage: adb_logcat_monitor.py <base_dir> [<adb_binary_path>] 10 11This script will repeatedly poll adb for new devices and save logcats 12inside the <base_dir> directory, which it attempts to create. The 13script will run until killed by an external signal. To test, run the 14script in a shell and <Ctrl>-C it after a while. It should be 15resilient across phone disconnects and reconnects and start the logcat 16early enough to not miss anything. 17""" 18 19import logging 20import os 21import re 22import shutil 23import signal 24import subprocess 25import sys 26import time 27 28# Map from device_id -> (process, logcat_num) 29devices = {} 30 31 32class TimeoutException(Exception): 33 """Exception used to signal a timeout.""" 34 pass 35 36 37class SigtermError(Exception): 38 """Exception used to catch a sigterm.""" 39 pass 40 41 42def StartLogcatIfNecessary(device_id, adb_cmd, base_dir): 43 """Spawns a adb logcat process if one is not currently running.""" 44 process, logcat_num = devices[device_id] 45 if process: 46 if process.poll() is None: 47 # Logcat process is still happily running 48 return 49 else: 50 logging.info('Logcat for device %s has died', device_id) 51 error_filter = re.compile('- waiting for device -') 52 for line in process.stderr: 53 if not error_filter.match(line): 54 logging.error(device_id + ': ' + line) 55 56 logging.info('Starting logcat %d for device %s', logcat_num, 57 device_id) 58 logcat_filename = 'logcat_%s_%03d' % (device_id, logcat_num) 59 logcat_file = open(os.path.join(base_dir, logcat_filename), 'w') 60 process = subprocess.Popen([adb_cmd, '-s', device_id, 61 'logcat', '-v', 'threadtime'], 62 stdout=logcat_file, 63 stderr=subprocess.PIPE) 64 devices[device_id] = (process, logcat_num + 1) 65 66 67def GetAttachedDevices(adb_cmd): 68 """Gets the device list from adb. 69 70 We use an alarm in this function to avoid deadlocking from an external 71 dependency. 72 73 Args: 74 adb_cmd: binary to run adb 75 76 Returns: 77 list of devices or an empty list on timeout 78 """ 79 signal.alarm(2) 80 try: 81 out, err = subprocess.Popen([adb_cmd, 'devices'], 82 stdout=subprocess.PIPE, 83 stderr=subprocess.PIPE).communicate() 84 if err: 85 logging.warning('adb device error %s', err.strip()) 86 return re.findall('^(\w+)\tdevice$', out, re.MULTILINE) 87 except TimeoutException: 88 logging.warning('"adb devices" command timed out') 89 return [] 90 except (IOError, OSError): 91 logging.exception('Exception from "adb devices"') 92 return [] 93 finally: 94 signal.alarm(0) 95 96 97def main(base_dir, adb_cmd='adb'): 98 """Monitor adb forever. Expects a SIGINT (Ctrl-C) to kill.""" 99 # We create the directory to ensure 'run once' semantics 100 if os.path.exists(base_dir): 101 print 'adb_logcat_monitor: %s already exists? Cleaning' % base_dir 102 shutil.rmtree(base_dir, ignore_errors=True) 103 104 os.makedirs(base_dir) 105 logging.basicConfig(filename=os.path.join(base_dir, 'eventlog'), 106 level=logging.INFO, 107 format='%(asctime)-2s %(levelname)-8s %(message)s') 108 109 # Set up the alarm for calling 'adb devices'. This is to ensure 110 # our script doesn't get stuck waiting for a process response 111 def TimeoutHandler(_, unused_frame): 112 raise TimeoutException() 113 signal.signal(signal.SIGALRM, TimeoutHandler) 114 115 # Handle SIGTERMs to ensure clean shutdown 116 def SigtermHandler(_, unused_frame): 117 raise SigtermError() 118 signal.signal(signal.SIGTERM, SigtermHandler) 119 120 logging.info('Started with pid %d', os.getpid()) 121 pid_file_path = os.path.join(base_dir, 'LOGCAT_MONITOR_PID') 122 123 try: 124 with open(pid_file_path, 'w') as f: 125 f.write(str(os.getpid())) 126 while True: 127 for device_id in GetAttachedDevices(adb_cmd): 128 if not device_id in devices: 129 devices[device_id] = (None, 0) 130 131 for device in devices: 132 # This will spawn logcat watchers for any device ever detected 133 StartLogcatIfNecessary(device, adb_cmd, base_dir) 134 135 time.sleep(5) 136 except SigtermError: 137 logging.info('Received SIGTERM, shutting down') 138 except: 139 logging.exception('Unexpected exception in main.') 140 finally: 141 for process, _ in devices.itervalues(): 142 if process: 143 try: 144 process.terminate() 145 except OSError: 146 pass 147 os.remove(pid_file_path) 148 149 150if __name__ == '__main__': 151 if 2 <= len(sys.argv) <= 3: 152 print 'adb_logcat_monitor: Initializing' 153 sys.exit(main(*sys.argv[1:3])) 154 155 print 'Usage: %s <base_dir> [<adb_binary_path>]' % sys.argv[0] 156