• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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