1# Copyright 2018 Google Inc. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14import io 15import logging 16import os 17import time 18 19from mobly import logger as mobly_logger 20from mobly import utils 21from mobly.controllers.android_device_lib import adb 22from mobly.controllers.android_device_lib import errors 23from mobly.controllers.android_device_lib.services import base_service 24 25CREATE_LOGCAT_FILE_TIMEOUT_SEC = 5 26 27 28class Error(errors.ServiceError): 29 """Root error type for logcat service.""" 30 SERVICE_TYPE = 'Logcat' 31 32 33class Config: 34 """Config object for logcat service. 35 36 Attributes: 37 clear_log: bool, clears the logcat before collection if True. 38 logcat_params: string, extra params to be added to logcat command. 39 output_file_path: string, the path on the host to write the log file 40 to, including the actual filename. The service will automatically 41 generate one if not specified. 42 """ 43 44 def __init__(self, logcat_params=None, clear_log=True, output_file_path=None): 45 self.clear_log = clear_log 46 self.logcat_params = logcat_params if logcat_params else '' 47 self.output_file_path = output_file_path 48 49 50class Logcat(base_service.BaseService): 51 """Android logcat service for Mobly's AndroidDevice controller. 52 53 Attributes: 54 adb_logcat_file_path: string, path to the file that the service writes 55 adb logcat to by default. 56 """ 57 OUTPUT_FILE_TYPE = 'logcat' 58 59 def __init__(self, android_device, configs=None): 60 super().__init__(android_device, configs) 61 self._ad = android_device 62 self._adb_logcat_process = None 63 self._adb_logcat_file_obj = None 64 self.adb_logcat_file_path = None 65 # Logcat service uses a single config obj, using singular internal 66 # name: `_config`. 67 self._config = configs if configs else Config() 68 69 def _enable_logpersist(self): 70 """Attempts to enable logpersist daemon to persist logs.""" 71 # Logpersist is only allowed on rootable devices because of excessive 72 # reads/writes for persisting logs. 73 if not self._ad.is_rootable: 74 return 75 76 logpersist_warning = ('%s encountered an error enabling persistent' 77 ' logs, logs may not get saved.') 78 # Android L and older versions do not have logpersist installed, 79 # so check that the logpersist scripts exists before trying to use 80 # them. 81 if not self._ad.adb.has_shell_command('logpersist.start'): 82 logging.warning(logpersist_warning, self) 83 return 84 85 try: 86 # Disable adb log spam filter for rootable devices. Have to stop 87 # and clear settings first because 'start' doesn't support --clear 88 # option before Android N. 89 self._ad.adb.shell('logpersist.stop --clear') 90 self._ad.adb.shell('logpersist.start') 91 except adb.AdbError: 92 logging.warning(logpersist_warning, self) 93 94 def _is_timestamp_in_range(self, target, begin_time, end_time): 95 low = mobly_logger.logline_timestamp_comparator(begin_time, target) <= 0 96 high = mobly_logger.logline_timestamp_comparator(end_time, target) >= 0 97 return low and high 98 99 def create_output_excerpts(self, test_info): 100 """Convenient method for creating excerpts of adb logcat. 101 102 This copies logcat lines from self.adb_logcat_file_path to an excerpt 103 file, starting from the location where the previous excerpt ended. 104 105 Call this method at the end of: `setup_class`, `teardown_test`, and 106 `teardown_class`. 107 108 Args: 109 test_info: `self.current_test_info` in a Mobly test. 110 111 Returns: 112 List of strings, the absolute paths to excerpt files. 113 """ 114 dest_path = test_info.output_path 115 utils.create_dir(dest_path) 116 filename = self._ad.generate_filename(self.OUTPUT_FILE_TYPE, test_info, 117 'txt') 118 excerpt_file_path = os.path.join(dest_path, filename) 119 with io.open(excerpt_file_path, 'w', encoding='utf-8', 120 errors='replace') as out: 121 # Devices may accidentally go offline during test, 122 # check not None before readline(). 123 while self._adb_logcat_file_obj: 124 line = self._adb_logcat_file_obj.readline() 125 if not line: 126 break 127 out.write(line) 128 self._ad.log.debug('logcat excerpt created at: %s', excerpt_file_path) 129 return [excerpt_file_path] 130 131 @property 132 def is_alive(self): 133 return True if self._adb_logcat_process else False 134 135 def clear_adb_log(self): 136 """Clears cached adb content.""" 137 try: 138 self._ad.adb.logcat('-c') 139 except adb.AdbError as e: 140 # On Android O, the clear command fails due to a known bug. 141 # Catching this so we don't crash from this Android issue. 142 if b'failed to clear' in e.stderr: 143 self._ad.log.warning('Encountered known Android error to clear logcat.') 144 else: 145 raise 146 147 def _assert_not_running(self): 148 """Asserts the logcat service is not running. 149 150 Raises: 151 Error, if the logcat service is running. 152 """ 153 if self.is_alive: 154 raise Error( 155 self._ad, 156 'Logcat thread is already running, cannot start another one.') 157 158 def update_config(self, new_config): 159 """Updates the configuration for the service. 160 161 The service needs to be stopped before updating, and explicitly started 162 after the update. 163 164 This will reset the service. Previous output files may be orphaned if 165 output path is changed. 166 167 Args: 168 new_config: Config, the new config to use. 169 """ 170 self._assert_not_running() 171 self._ad.log.info('[LogcatService] Changing config from %s to %s', 172 self._config, new_config) 173 self._config = new_config 174 175 def _open_logcat_file(self): 176 """Create a file object that points to the beginning of the logcat file. 177 Wait for the logcat file to be created by the subprocess if it doesn't 178 exist. 179 """ 180 if not self._adb_logcat_file_obj: 181 deadline = time.perf_counter() + CREATE_LOGCAT_FILE_TIMEOUT_SEC 182 while not os.path.exists(self.adb_logcat_file_path): 183 if time.perf_counter() > deadline: 184 raise Error(self._ad, 185 'Timeout while waiting for logcat file to be created.') 186 time.sleep(1) 187 self._adb_logcat_file_obj = io.open(self.adb_logcat_file_path, 188 'r', 189 encoding='utf-8', 190 errors='replace') 191 self._adb_logcat_file_obj.seek(0, os.SEEK_END) 192 193 def _close_logcat_file(self): 194 """Closes and resets the logcat file object, if it exists.""" 195 if self._adb_logcat_file_obj: 196 self._adb_logcat_file_obj.close() 197 self._adb_logcat_file_obj = None 198 199 def start(self): 200 """Starts a standing adb logcat collection. 201 202 The collection runs in a separate subprocess and saves logs in a file. 203 """ 204 self._assert_not_running() 205 if self._config.clear_log: 206 self.clear_adb_log() 207 self._start() 208 self._open_logcat_file() 209 210 def _start(self): 211 """The actual logic of starting logcat.""" 212 self._enable_logpersist() 213 if self._config.output_file_path: 214 self._close_logcat_file() 215 self.adb_logcat_file_path = self._config.output_file_path 216 if not self.adb_logcat_file_path: 217 f_name = self._ad.generate_filename(self.OUTPUT_FILE_TYPE, 218 extension_name='txt') 219 logcat_file_path = os.path.join(self._ad.log_path, f_name) 220 self.adb_logcat_file_path = logcat_file_path 221 utils.create_dir(os.path.dirname(self.adb_logcat_file_path)) 222 # In debugging mode of IntelijIDEA, "patch_args" remove 223 # double quotes in args if starting and ending with it. 224 # Add spaces at beginning and at last to fix this issue. 225 cmd = ' "%s" -s %s logcat -v threadtime -T 1 %s >> "%s" ' % ( 226 adb.ADB, self._ad.serial, self._config.logcat_params, 227 self.adb_logcat_file_path) 228 process = utils.start_standing_subprocess(cmd, shell=True) 229 self._adb_logcat_process = process 230 231 def stop(self): 232 """Stops the adb logcat service.""" 233 self._close_logcat_file() 234 self._stop() 235 236 def _stop(self): 237 """Stops the background process for logcat.""" 238 if not self._adb_logcat_process: 239 return 240 try: 241 utils.stop_standing_subprocess(self._adb_logcat_process) 242 except Exception: 243 self._ad.log.exception('Failed to stop adb logcat.') 244 self._adb_logcat_process = None 245 246 def pause(self): 247 """Pauses logcat. 248 249 Note: the service is unable to collect the logs when paused, if more 250 logs are generated on the device than the device's log buffer can hold, 251 some logs would be lost. 252 """ 253 self._stop() 254 255 def resume(self): 256 """Resumes a paused logcat service.""" 257 self._assert_not_running() 258 # Not clearing the log regardless of the config when resuming. 259 # Otherwise the logs during the paused time will be lost. 260 self._start() 261