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