aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil/devil/android/logcat_monitor.py
blob: bec74440b742fd1a806ece29107cc2d7851b5458 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# pylint: disable=unused-argument

import errno
import logging
import os
import re
import shutil
import tempfile
import threading
import time

from devil.android import decorators
from devil.android import device_errors
from devil.android.sdk import adb_wrapper
from devil.utils import reraiser_thread

logger = logging.getLogger(__name__)


class LogcatMonitor(object):

  _RECORD_ITER_TIMEOUT = 0.01
  _RECORD_THREAD_JOIN_WAIT = 5.0
  _WAIT_TIME = 0.2
  THREADTIME_RE_FORMAT = (
      r'(?P<date>\S*) +(?P<time>\S*) +(?P<proc_id>%s) +(?P<thread_id>%s) +'
      r'(?P<log_level>%s) +(?P<component>%s) *: +(?P<message>%s)$')

  def __init__(self,
               adb,
               clear=True,
               filter_specs=None,
               output_file=None,
               transform_func=None,
               check_error=True):
    """Create a LogcatMonitor instance.

    Args:
      adb: An instance of adb_wrapper.AdbWrapper.
      clear: If True, clear the logcat when monitoring starts.
      filter_specs: An optional list of '<tag>[:priority]' strings.
      output_file: File path to save recorded logcat.
      transform_func: An optional unary callable that takes and returns
        a list of lines, possibly transforming them in the process.
      check_error: Check for and raise an exception on nonzero exit codes
        from the underlying logcat command.
    """
    if isinstance(adb, adb_wrapper.AdbWrapper):
      self._adb = adb
    else:
      raise ValueError('Unsupported type passed for argument "device"')
    self._check_error = check_error
    self._clear = clear
    self._filter_specs = filter_specs
    self._output_file = output_file
    self._record_file = None
    self._record_file_lock = threading.Lock()
    self._record_thread = None
    self._stop_recording_event = threading.Event()
    self._transform_func = transform_func

  @property
  def output_file(self):
    return self._output_file

  @decorators.WithTimeoutAndRetriesDefaults(10, 0)
  def WaitFor(self,
              success_regex,
              failure_regex=None,
              timeout=None,
              retries=None):
    """Wait for a matching logcat line or until a timeout occurs.

    This will attempt to match lines in the logcat against both |success_regex|
    and |failure_regex| (if provided). Note that this calls re.search on each
    logcat line, not re.match, so the provided regular expressions don't have
    to match an entire line.

    Args:
      success_regex: The regular expression to search for.
      failure_regex: An optional regular expression that, if hit, causes this
        to stop looking for a match. Can be None.
      timeout: timeout in seconds
      retries: number of retries

    Returns:
      A match object if |success_regex| matches a part of a logcat line, or
      None if |failure_regex| matches a part of a logcat line.
    Raises:
      CommandFailedError on logcat failure (NOT on a |failure_regex| match).
      CommandTimeoutError if no logcat line matching either |success_regex| or
        |failure_regex| is found in |timeout| seconds.
      DeviceUnreachableError if the device becomes unreachable.
      LogcatMonitorCommandError when calling |WaitFor| while not recording
        logcat.
    """
    if self._record_thread is None:
      raise LogcatMonitorCommandError(
          'Must be recording logcat when calling |WaitFor|',
          device_serial=str(self._adb))
    if isinstance(success_regex, basestring):
      success_regex = re.compile(success_regex)
    if isinstance(failure_regex, basestring):
      failure_regex = re.compile(failure_regex)

    logger.debug('Waiting %d seconds for "%s"', timeout, success_regex.pattern)

    # NOTE This will continue looping until:
    #  - success_regex matches a line, in which case the match object is
    #    returned.
    #  - failure_regex matches a line, in which case None is returned
    #  - the timeout is hit, in which case a CommandTimeoutError is raised.
    with open(self._record_file.name, 'r') as f:
      while True:
        line = f.readline()
        if line:
          m = success_regex.search(line)
          if m:
            return m
          if failure_regex and failure_regex.search(line):
            return None
        else:
          time.sleep(self._WAIT_TIME)

  def FindAll(self,
              message_regex,
              proc_id=None,
              thread_id=None,
              log_level=None,
              component=None):
    """Finds all lines in the logcat that match the provided constraints.

    Args:
      message_regex: The regular expression that the <message> section must
        match.
      proc_id: The process ID to match. If None, matches any process ID.
      thread_id: The thread ID to match. If None, matches any thread ID.
      log_level: The log level to match. If None, matches any log level.
      component: The component to match. If None, matches any component.

    Raises:
      LogcatMonitorCommandError when calling |FindAll| before recording logcat.

    Yields:
      A match object for each matching line in the logcat. The match object
      will always contain, in addition to groups defined in |message_regex|,
      the following named groups: 'date', 'time', 'proc_id', 'thread_id',
      'log_level', 'component', and 'message'.
    """
    if self._record_file is None:
      raise LogcatMonitorCommandError(
          'Must have recorded or be recording a logcat to call |FindAll|',
          device_serial=str(self._adb))
    if proc_id is None:
      proc_id = r'\d+'
    if thread_id is None:
      thread_id = r'\d+'
    if log_level is None:
      log_level = r'[VDIWEF]'
    if component is None:
      component = r'[^\s:]+'
    # pylint: disable=protected-access
    threadtime_re = re.compile(
        type(self).THREADTIME_RE_FORMAT % (proc_id, thread_id, log_level,
                                           component, message_regex))

    with open(self._record_file.name, 'r') as f:
      for line in f:
        m = re.match(threadtime_re, line)
        if m:
          yield m

  def _StartRecording(self):
    """Starts recording logcat to file.

    Function spawns a thread that records logcat to file and will not die
    until |StopRecording| is called.
    """

    def record_to_file():
      # Write the log with line buffering so the consumer sees each individual
      # line.
      for data in self._adb.Logcat(
          filter_specs=self._filter_specs,
          logcat_format='threadtime',
          iter_timeout=self._RECORD_ITER_TIMEOUT,
          check_error=self._check_error):
        if self._stop_recording_event.isSet():
          return

        if data is None:
          # Logcat can yield None if the iter_timeout is hit.
          continue

        with self._record_file_lock:
          if self._record_file and not self._record_file.closed:
            if self._transform_func:
              data = '\n'.join(self._transform_func([data]))
            self._record_file.write(data + '\n')

    self._stop_recording_event.clear()
    if not self._record_thread:
      self._record_thread = reraiser_thread.ReraiserThread(record_to_file)
      self._record_thread.start()

  def _StopRecording(self):
    """Finish recording logcat."""
    if self._record_thread:
      self._stop_recording_event.set()
      self._record_thread.join(timeout=self._RECORD_THREAD_JOIN_WAIT)
      self._record_thread.ReraiseIfException()
      self._record_thread = None

  def Start(self):
    """Starts the logcat monitor.

    Clears the logcat if |clear| was set in |__init__|.
    """
    if self._clear:
      self._adb.Logcat(clear=True)
    if not self._record_file:
      self._record_file = tempfile.NamedTemporaryFile(mode='a', bufsize=1)
    self._StartRecording()

  def Stop(self):
    """Stops the logcat monitor.

    Stops recording the logcat. Copies currently recorded logcat to
    |self._output_file|.
    """
    self._StopRecording()
    with self._record_file_lock:
      if self._record_file and self._output_file:
        try:
          os.makedirs(os.path.dirname(self._output_file))
        except OSError as e:
          if e.errno != errno.EEXIST:
            raise
        shutil.copy(self._record_file.name, self._output_file)

  def Close(self):
    """Closes logcat recording file.

    Should be called when finished using the logcat monitor.
    """
    with self._record_file_lock:
      if self._record_file:
        self._record_file.close()
        self._record_file = None

  def close(self):
    """An alias for Close.

    Allows LogcatMonitors to be used with contextlib.closing.
    """
    self.Close()

  def __enter__(self):
    """Starts the logcat monitor."""
    self.Start()
    return self

  def __exit__(self, exc_type, exc_val, exc_tb):
    """Stops the logcat monitor."""
    self.Stop()

  def __del__(self):
    """Closes logcat recording file in case |Close| was never called."""
    with self._record_file_lock:
      if self._record_file:
        logger.warning('Need to call |Close| on the logcat monitor when done!')
        self._record_file.close()

  @property
  def adb(self):
    return self._adb


class LogcatMonitorCommandError(device_errors.CommandFailedError):
  """Exception for errors with logcat monitor commands."""
  pass