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