logcat_monitor.py revision ee838d1c4002134ff5af32da272140586c4d31de
1# Copyright 2015 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5# pylint: disable=unused-argument
6
7import errno
8import logging
9import os
10import re
11import shutil
12import tempfile
13import threading
14import time
15
16from devil.android import decorators
17from devil.android import device_errors
18from devil.android.sdk import adb_wrapper
19from devil.utils import reraiser_thread
20
21logger = logging.getLogger(__name__)
22
23
24class LogcatMonitor(object):
25
26  _RECORD_THREAD_JOIN_WAIT = 2.0
27  _WAIT_TIME = 0.2
28  _THREADTIME_RE_FORMAT = (
29      r'(?P<date>\S*) +(?P<time>\S*) +(?P<proc_id>%s) +(?P<thread_id>%s) +'
30      r'(?P<log_level>%s) +(?P<component>%s) *: +(?P<message>%s)$')
31
32  def __init__(self, adb, clear=True, filter_specs=None, output_file=None):
33    """Create a LogcatMonitor instance.
34
35    Args:
36      adb: An instance of adb_wrapper.AdbWrapper.
37      clear: If True, clear the logcat when monitoring starts.
38      filter_specs: An optional list of '<tag>[:priority]' strings.
39      output_file: File path to save recorded logcat.
40    """
41    if isinstance(adb, adb_wrapper.AdbWrapper):
42      self._adb = adb
43    else:
44      raise ValueError('Unsupported type passed for argument "device"')
45    self._clear = clear
46    self._filter_specs = filter_specs
47    self._output_file = output_file
48    self._record_file = None
49    self._record_file_lock = threading.Lock()
50    self._record_thread = None
51    self._stop_recording_event = threading.Event()
52
53  @property
54  def output_file(self):
55    return self._output_file
56
57  @decorators.WithTimeoutAndRetriesDefaults(10, 0)
58  def WaitFor(self, success_regex, failure_regex=None, timeout=None,
59              retries=None):
60    """Wait for a matching logcat line or until a timeout occurs.
61
62    This will attempt to match lines in the logcat against both |success_regex|
63    and |failure_regex| (if provided). Note that this calls re.search on each
64    logcat line, not re.match, so the provided regular expressions don't have
65    to match an entire line.
66
67    Args:
68      success_regex: The regular expression to search for.
69      failure_regex: An optional regular expression that, if hit, causes this
70        to stop looking for a match. Can be None.
71      timeout: timeout in seconds
72      retries: number of retries
73
74    Returns:
75      A match object if |success_regex| matches a part of a logcat line, or
76      None if |failure_regex| matches a part of a logcat line.
77    Raises:
78      CommandFailedError on logcat failure (NOT on a |failure_regex| match).
79      CommandTimeoutError if no logcat line matching either |success_regex| or
80        |failure_regex| is found in |timeout| seconds.
81      DeviceUnreachableError if the device becomes unreachable.
82      LogcatMonitorCommandError when calling |WaitFor| while not recording
83        logcat.
84    """
85    if self._record_thread is None:
86      raise LogcatMonitorCommandError(
87          'Must be recording logcat when calling |WaitFor|',
88          device_serial=str(self._adb))
89    if isinstance(success_regex, basestring):
90      success_regex = re.compile(success_regex)
91    if isinstance(failure_regex, basestring):
92      failure_regex = re.compile(failure_regex)
93
94    logger.debug('Waiting %d seconds for "%s"', timeout, success_regex.pattern)
95
96    # NOTE This will continue looping until:
97    #  - success_regex matches a line, in which case the match object is
98    #    returned.
99    #  - failure_regex matches a line, in which case None is returned
100    #  - the timeout is hit, in which case a CommandTimeoutError is raised.
101    with open(self._record_file.name, 'r') as f:
102      while True:
103        line = f.readline()
104        if line:
105          m = success_regex.search(line)
106          if m:
107            return m
108          if failure_regex and failure_regex.search(line):
109            return None
110        else:
111          time.sleep(self._WAIT_TIME)
112
113  def FindAll(self, message_regex, proc_id=None, thread_id=None, log_level=None,
114              component=None):
115    """Finds all lines in the logcat that match the provided constraints.
116
117    Args:
118      message_regex: The regular expression that the <message> section must
119        match.
120      proc_id: The process ID to match. If None, matches any process ID.
121      thread_id: The thread ID to match. If None, matches any thread ID.
122      log_level: The log level to match. If None, matches any log level.
123      component: The component to match. If None, matches any component.
124
125    Raises:
126      LogcatMonitorCommandError when calling |FindAll| before recording logcat.
127
128    Yields:
129      A match object for each matching line in the logcat. The match object
130      will always contain, in addition to groups defined in |message_regex|,
131      the following named groups: 'date', 'time', 'proc_id', 'thread_id',
132      'log_level', 'component', and 'message'.
133    """
134    if self._record_file is None:
135      raise LogcatMonitorCommandError(
136          'Must have recorded or be recording a logcat to call |FindAll|',
137          device_serial=str(self._adb))
138    if proc_id is None:
139      proc_id = r'\d+'
140    if thread_id is None:
141      thread_id = r'\d+'
142    if log_level is None:
143      log_level = r'[VDIWEF]'
144    if component is None:
145      component = r'[^\s:]+'
146    # pylint: disable=protected-access
147    threadtime_re = re.compile(
148        type(self)._THREADTIME_RE_FORMAT % (
149            proc_id, thread_id, log_level, component, message_regex))
150
151    with open(self._record_file.name, 'r') as f:
152      for line in f:
153        m = re.match(threadtime_re, line)
154        if m:
155          yield m
156
157  def _StartRecording(self):
158    """Starts recording logcat to file.
159
160    Function spawns a thread that records logcat to file and will not die
161    until |StopRecording| is called.
162    """
163    def record_to_file():
164      # Write the log with line buffering so the consumer sees each individual
165      # line.
166      for data in self._adb.Logcat(filter_specs=self._filter_specs,
167                                   logcat_format='threadtime'):
168        with self._record_file_lock:
169          if self._stop_recording_event.isSet():
170            return
171          if self._record_file and not self._record_file.closed:
172            self._record_file.write(data + '\n')
173
174    self._stop_recording_event.clear()
175    if not self._record_thread:
176      self._record_thread = reraiser_thread.ReraiserThread(record_to_file)
177      self._record_thread.start()
178
179  def _StopRecording(self):
180    """Finish recording logcat."""
181    if self._record_thread:
182      self._stop_recording_event.set()
183      self._record_thread.join(timeout=self._RECORD_THREAD_JOIN_WAIT)
184      self._record_thread.ReraiseIfException()
185      self._record_thread = None
186
187  def Start(self):
188    """Starts the logcat monitor.
189
190    Clears the logcat if |clear| was set in |__init__|.
191    """
192    if self._clear:
193      self._adb.Logcat(clear=True)
194    if not self._record_file:
195      self._record_file = tempfile.NamedTemporaryFile(mode='a', bufsize=1)
196    self._StartRecording()
197
198  def Stop(self):
199    """Stops the logcat monitor.
200
201    Stops recording the logcat. Copies currently recorded logcat to
202    |self._output_file|.
203    """
204    self._StopRecording()
205    with self._record_file_lock:
206      if self._record_file and self._output_file:
207        try:
208          os.makedirs(os.path.dirname(self._output_file))
209        except OSError as e:
210          if e.errno != errno.EEXIST:
211            raise
212        shutil.copy(self._record_file.name, self._output_file)
213
214  def Close(self):
215    """Closes logcat recording file.
216
217    Should be called when finished using the logcat monitor.
218    """
219    with self._record_file_lock:
220      if self._record_file:
221        self._record_file.close()
222        self._record_file = None
223
224  def __enter__(self):
225    """Starts the logcat monitor."""
226    self.Start()
227    return self
228
229  def __exit__(self, exc_type, exc_val, exc_tb):
230    """Stops the logcat monitor."""
231    self.Stop()
232
233  def __del__(self):
234    """Closes logcat recording file in case |Close| was never called."""
235    with self._record_file_lock:
236      if self._record_file:
237        logger.warning(
238            'Need to call |Close| on the logcat monitor when done!')
239        self._record_file.close()
240
241  @property
242  def adb(self):
243    return self._adb
244
245
246class LogcatMonitorCommandError(device_errors.CommandFailedError):
247  """Exception for errors with logcat monitor commands."""
248  pass
249