Home | History | Annotate | Download | only in android
      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 
      9 Usage: adb_logcat_monitor.py <base_dir> [<adb_binary_path>]
     10 
     11 This script will repeatedly poll adb for new devices and save logcats
     12 inside the <base_dir> directory, which it attempts to create.  The
     13 script will run until killed by an external signal.  To test, run the
     14 script in a shell and <Ctrl>-C it after a while.  It should be
     15 resilient across phone disconnects and reconnects and start the logcat
     16 early enough to not miss anything.
     17 """
     18 
     19 import logging
     20 import os
     21 import re
     22 import shutil
     23 import signal
     24 import subprocess
     25 import sys
     26 import time
     27 
     28 # Map from device_id -> (process, logcat_num)
     29 devices = {}
     30 
     31 
     32 class TimeoutException(Exception):
     33   """Exception used to signal a timeout."""
     34   pass
     35 
     36 
     37 class SigtermError(Exception):
     38   """Exception used to catch a sigterm."""
     39   pass
     40 
     41 
     42 def 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 
     67 def 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 
     97 def 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           subprocess.call([adb_cmd, '-s', device_id, 'logcat', '-c'])
    130           devices[device_id] = (None, 0)
    131 
    132       for device in devices:
    133         # This will spawn logcat watchers for any device ever detected
    134         StartLogcatIfNecessary(device, adb_cmd, base_dir)
    135 
    136       time.sleep(5)
    137   except SigtermError:
    138     logging.info('Received SIGTERM, shutting down')
    139   except:
    140     logging.exception('Unexpected exception in main.')
    141   finally:
    142     for process, _ in devices.itervalues():
    143       if process:
    144         try:
    145           process.terminate()
    146         except OSError:
    147           pass
    148     os.remove(pid_file_path)
    149 
    150 
    151 if __name__ == '__main__':
    152   if 2 <= len(sys.argv) <= 3:
    153     print 'adb_logcat_monitor: Initializing'
    154     sys.exit(main(*sys.argv[1:3]))
    155 
    156   print 'Usage: %s <base_dir> [<adb_binary_path>]' % sys.argv[0]
    157