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