• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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