| # Copyright 2018 Google Inc. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| import io |
| import logging |
| import os |
| import time |
| |
| from mobly import logger as mobly_logger |
| from mobly import utils |
| from mobly.controllers.android_device_lib import adb |
| from mobly.controllers.android_device_lib import errors |
| from mobly.controllers.android_device_lib.services import base_service |
| |
| CREATE_LOGCAT_FILE_TIMEOUT_SEC = 5 |
| |
| |
| class Error(errors.ServiceError): |
| """Root error type for logcat service.""" |
| SERVICE_TYPE = 'Logcat' |
| |
| |
| class Config(object): |
| """Config object for logcat service. |
| |
| Attributes: |
| clear_log: bool, clears the logcat before collection if True. |
| logcat_params: string, extra params to be added to logcat command. |
| output_file_path: string, the path on the host to write the log file |
| to, including the actual filename. The service will automatically |
| generate one if not specified. |
| """ |
| |
| def __init__(self, logcat_params=None, clear_log=True, output_file_path=None): |
| self.clear_log = clear_log |
| self.logcat_params = logcat_params if logcat_params else '' |
| self.output_file_path = output_file_path |
| |
| |
| class Logcat(base_service.BaseService): |
| """Android logcat service for Mobly's AndroidDevice controller. |
| |
| Attributes: |
| adb_logcat_file_path: string, path to the file that the service writes |
| adb logcat to by default. |
| """ |
| OUTPUT_FILE_TYPE = 'logcat' |
| |
| def __init__(self, android_device, configs=None): |
| super(Logcat, self).__init__(android_device, configs) |
| self._ad = android_device |
| self._adb_logcat_process = None |
| self._adb_logcat_file_obj = None |
| self.adb_logcat_file_path = None |
| # Logcat service uses a single config obj, using singular internal |
| # name: `_config`. |
| self._config = configs if configs else Config() |
| |
| def _enable_logpersist(self): |
| """Attempts to enable logpersist daemon to persist logs.""" |
| # Logpersist is only allowed on rootable devices because of excessive |
| # reads/writes for persisting logs. |
| if not self._ad.is_rootable: |
| return |
| |
| logpersist_warning = ('%s encountered an error enabling persistent' |
| ' logs, logs may not get saved.') |
| # Android L and older versions do not have logpersist installed, |
| # so check that the logpersist scripts exists before trying to use |
| # them. |
| if not self._ad.adb.has_shell_command('logpersist.start'): |
| logging.warning(logpersist_warning, self) |
| return |
| |
| try: |
| # Disable adb log spam filter for rootable devices. Have to stop |
| # and clear settings first because 'start' doesn't support --clear |
| # option before Android N. |
| self._ad.adb.shell('logpersist.stop --clear') |
| self._ad.adb.shell('logpersist.start') |
| except adb.AdbError: |
| logging.warning(logpersist_warning, self) |
| |
| def _is_timestamp_in_range(self, target, begin_time, end_time): |
| low = mobly_logger.logline_timestamp_comparator(begin_time, target) <= 0 |
| high = mobly_logger.logline_timestamp_comparator(end_time, target) >= 0 |
| return low and high |
| |
| def create_output_excerpts(self, test_info): |
| """Convenient method for creating excerpts of adb logcat. |
| |
| This copies logcat lines from self.adb_logcat_file_path to an excerpt |
| file, starting from the location where the previous excerpt ended. |
| |
| Call this method at the end of: `setup_class`, `teardown_test`, and |
| `teardown_class`. |
| |
| Args: |
| test_info: `self.current_test_info` in a Mobly test. |
| |
| Returns: |
| List of strings, the absolute paths to excerpt files. |
| """ |
| dest_path = test_info.output_path |
| utils.create_dir(dest_path) |
| filename = self._ad.generate_filename(self.OUTPUT_FILE_TYPE, test_info, |
| 'txt') |
| excerpt_file_path = os.path.join(dest_path, filename) |
| with io.open(excerpt_file_path, 'w', encoding='utf-8', |
| errors='replace') as out: |
| # Devices may accidentally go offline during test, |
| # check not None before readline(). |
| while self._adb_logcat_file_obj: |
| line = self._adb_logcat_file_obj.readline() |
| if not line: |
| break |
| out.write(line) |
| self._ad.log.debug('logcat excerpt created at: %s', excerpt_file_path) |
| return [excerpt_file_path] |
| |
| @property |
| def is_alive(self): |
| return True if self._adb_logcat_process else False |
| |
| def clear_adb_log(self): |
| """Clears cached adb content.""" |
| try: |
| self._ad.adb.logcat('-c') |
| except adb.AdbError as e: |
| # On Android O, the clear command fails due to a known bug. |
| # Catching this so we don't crash from this Android issue. |
| if b'failed to clear' in e.stderr: |
| self._ad.log.warning('Encountered known Android error to clear logcat.') |
| else: |
| raise |
| |
| def _assert_not_running(self): |
| """Asserts the logcat service is not running. |
| |
| Raises: |
| Error, if the logcat service is running. |
| """ |
| if self.is_alive: |
| raise Error( |
| self._ad, |
| 'Logcat thread is already running, cannot start another one.') |
| |
| def update_config(self, new_config): |
| """Updates the configuration for the service. |
| |
| The service needs to be stopped before updating, and explicitly started |
| after the update. |
| |
| This will reset the service. Previous output files may be orphaned if |
| output path is changed. |
| |
| Args: |
| new_config: Config, the new config to use. |
| """ |
| self._assert_not_running() |
| self._ad.log.info('[LogcatService] Changing config from %s to %s', |
| self._config, new_config) |
| self._config = new_config |
| |
| def _open_logcat_file(self): |
| """Create a file object that points to the beginning of the logcat file. |
| Wait for the logcat file to be created by the subprocess if it doesn't |
| exist. |
| """ |
| if not self._adb_logcat_file_obj: |
| start_time = time.time() |
| while not os.path.exists(self.adb_logcat_file_path): |
| if time.time() > start_time + CREATE_LOGCAT_FILE_TIMEOUT_SEC: |
| raise Error(self._ad, |
| 'Timeout while waiting for logcat file to be created.') |
| time.sleep(1) |
| self._adb_logcat_file_obj = io.open(self.adb_logcat_file_path, |
| 'r', |
| encoding='utf-8', |
| errors='replace') |
| self._adb_logcat_file_obj.seek(0, os.SEEK_END) |
| |
| def _close_logcat_file(self): |
| """Closes and resets the logcat file object, if it exists.""" |
| if self._adb_logcat_file_obj: |
| self._adb_logcat_file_obj.close() |
| self._adb_logcat_file_obj = None |
| |
| def start(self): |
| """Starts a standing adb logcat collection. |
| |
| The collection runs in a separate subprocess and saves logs in a file. |
| """ |
| self._assert_not_running() |
| if self._config.clear_log: |
| self.clear_adb_log() |
| self._start() |
| self._open_logcat_file() |
| |
| def _start(self): |
| """The actual logic of starting logcat.""" |
| self._enable_logpersist() |
| if self._config.output_file_path: |
| self._close_logcat_file() |
| self.adb_logcat_file_path = self._config.output_file_path |
| if not self.adb_logcat_file_path: |
| f_name = self._ad.generate_filename(self.OUTPUT_FILE_TYPE, |
| extension_name='txt') |
| logcat_file_path = os.path.join(self._ad.log_path, f_name) |
| self.adb_logcat_file_path = logcat_file_path |
| utils.create_dir(os.path.dirname(self.adb_logcat_file_path)) |
| # In debugging mode of IntelijIDEA, "patch_args" remove |
| # double quotes in args if starting and ending with it. |
| # Add spaces at beginning and at last to fix this issue. |
| cmd = ' "%s" -s %s logcat -v threadtime -T 1 %s >> "%s" ' % ( |
| adb.ADB, self._ad.serial, self._config.logcat_params, |
| self.adb_logcat_file_path) |
| process = utils.start_standing_subprocess(cmd, shell=True) |
| self._adb_logcat_process = process |
| |
| def stop(self): |
| """Stops the adb logcat service.""" |
| self._close_logcat_file() |
| self._stop() |
| |
| def _stop(self): |
| """Stops the background process for logcat.""" |
| if not self._adb_logcat_process: |
| return |
| try: |
| utils.stop_standing_subprocess(self._adb_logcat_process) |
| except: |
| self._ad.log.exception('Failed to stop adb logcat.') |
| self._adb_logcat_process = None |
| |
| def pause(self): |
| """Pauses logcat. |
| |
| Note: the service is unable to collect the logs when paused, if more |
| logs are generated on the device than the device's log buffer can hold, |
| some logs would be lost. |
| """ |
| self._stop() |
| |
| def resume(self): |
| """Resumes a paused logcat service.""" |
| self._assert_not_running() |
| # Not clearing the log regardless of the config when resuming. |
| # Otherwise the logs during the paused time will be lost. |
| self._start() |