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