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