#!/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 [] This script will repeatedly poll adb for new devices and save logcats inside the 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 -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('^(\\S+)\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(_signum, _unused_frame): raise TimeoutException() signal.signal(signal.SIGALRM, TimeoutHandler) # Handle SIGTERMs to ensure clean shutdown def SigtermHandler(_signum, _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: subprocess.call([adb_cmd, '-s', device_id, 'logcat', '-c']) 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: # pylint: disable=bare-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 []' % sys.argv[0]