• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2014 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5
6import datetime
7import functools
8import logging
9import os
10import shutil
11import tempfile
12import threading
13
14import devil_chromium
15from devil import base_error
16from devil.android import device_denylist
17from devil.android import device_errors
18from devil.android import device_utils
19from devil.android import logcat_monitor
20from devil.android.sdk import adb_wrapper
21from devil.utils import file_utils
22from devil.utils import parallelizer
23from pylib import constants
24from pylib.constants import host_paths
25from pylib.base import environment
26from pylib.utils import instrumentation_tracing
27from py_trace_event import trace_event
28
29
30LOGCAT_FILTERS = [
31  'chromium:v',
32  'cr_*:v',
33  'DEBUG:I',
34  'StrictMode:D',
35]
36
37SYSTEM_USER_ID = 0
38
39
40def _DeviceCachePath(device):
41  file_name = 'device_cache_%s.json' % device.adb.GetDeviceSerial()
42  return os.path.join(constants.GetOutDirectory(), file_name)
43
44
45def handle_shard_failures(f):
46  """A decorator that handles device failures for per-device functions.
47
48  Args:
49    f: the function being decorated. The function must take at least one
50      argument, and that argument must be the device.
51  """
52  return handle_shard_failures_with(None)(f)
53
54
55# TODO(jbudorick): Refactor this to work as a decorator or context manager.
56def handle_shard_failures_with(on_failure):
57  """A decorator that handles device failures for per-device functions.
58
59  This calls on_failure in the event of a failure.
60
61  Args:
62    f: the function being decorated. The function must take at least one
63      argument, and that argument must be the device.
64    on_failure: A binary function to call on failure.
65  """
66  def decorator(f):
67    @functools.wraps(f)
68    def wrapper(dev, *args, **kwargs):
69      try:
70        return f(dev, *args, **kwargs)
71      except device_errors.CommandTimeoutError:
72        logging.exception('Shard timed out: %s(%s)', f.__name__, str(dev))
73      except device_errors.DeviceUnreachableError:
74        logging.exception('Shard died: %s(%s)', f.__name__, str(dev))
75      except base_error.BaseError:
76        logging.exception('Shard failed: %s(%s)', f.__name__, str(dev))
77      except SystemExit:
78        logging.exception('Shard killed: %s(%s)', f.__name__, str(dev))
79        raise
80      if on_failure:
81        on_failure(dev, f.__name__)
82      return None
83
84    return wrapper
85
86  return decorator
87
88
89# TODO(crbug.com/40799394): After Telemetry is supported by python3 we can
90# re-add super without arguments in this script.
91# pylint: disable=super-with-arguments
92class LocalDeviceEnvironment(environment.Environment):
93
94  def __init__(self, args, output_manager, _error_func):
95    super(LocalDeviceEnvironment, self).__init__(output_manager)
96    self._current_try = 0
97    self._denylist = (device_denylist.Denylist(args.denylist_file)
98                      if args.denylist_file else None)
99    self._device_serials = args.test_devices
100    self._devices_lock = threading.Lock()
101    self._devices = None
102    self._concurrent_adb = args.enable_concurrent_adb
103    self._enable_device_cache = args.enable_device_cache
104    self._logcat_monitors = []
105    self._logcat_output_dir = args.logcat_output_dir
106    self._logcat_output_file = args.logcat_output_file
107    self._max_tries = 1 + args.num_retries
108    self._preferred_abis = None
109    self._recover_devices = args.recover_devices
110    self._skip_clear_data = args.skip_clear_data
111    self._trace_output = None
112    # Must check if arg exist because this class is used by
113    # //third_party/catapult's browser_options.py
114    if hasattr(args, 'trace_output'):
115      self._trace_output = args.trace_output
116    self._trace_all = None
117    if hasattr(args, 'trace_all'):
118      self._trace_all = args.trace_all
119    self._force_main_user = False
120    if hasattr(args, 'force_main_user'):
121      self._force_main_user = args.force_main_user
122    self._use_persistent_shell = args.use_persistent_shell
123    self._disable_test_server = args.disable_test_server
124
125    use_local_devil_tools = False
126    if hasattr(args, 'use_local_devil_tools'):
127      use_local_devil_tools = args.use_local_devil_tools
128
129    devil_chromium.Initialize(output_directory=constants.GetOutDirectory(),
130                              adb_path=args.adb_path,
131                              use_local_devil_tools=use_local_devil_tools)
132
133    # Some things such as Forwarder require ADB to be in the environment path,
134    # while others like Devil's bundletool.py require Java on the path.
135    adb_dir = os.path.dirname(adb_wrapper.AdbWrapper.GetAdbPath())
136    if adb_dir and adb_dir not in os.environ['PATH'].split(os.pathsep):
137      os.environ['PATH'] = os.pathsep.join(
138          [adb_dir, host_paths.JAVA_PATH, os.environ['PATH']])
139
140  #override
141  def SetUp(self):
142    if self.trace_output and self._trace_all:
143      to_include = [r"pylib\..*", r"devil\..*", "__main__"]
144      to_exclude = ["logging"]
145      instrumentation_tracing.start_instrumenting(self.trace_output, to_include,
146                                                  to_exclude)
147    elif self.trace_output:
148      self.EnableTracing()
149
150  # Must be called before accessing |devices|.
151  def SetPreferredAbis(self, abis):
152    assert self._devices is None
153    self._preferred_abis = abis
154
155  def _InitDevices(self):
156    device_arg = []
157    if self._device_serials:
158      device_arg = self._device_serials
159
160    self._devices = device_utils.DeviceUtils.HealthyDevices(
161        self._denylist,
162        retries=5,
163        enable_usb_resets=True,
164        enable_device_files_cache=self._enable_device_cache,
165        default_retries=self._max_tries - 1,
166        device_arg=device_arg,
167        abis=self._preferred_abis,
168        persistent_shell=self._use_persistent_shell)
169
170    if self._logcat_output_file:
171      self._logcat_output_dir = tempfile.mkdtemp()
172
173    @handle_shard_failures_with(on_failure=self.DenylistDevice)
174    def prepare_device(d):
175      d.WaitUntilFullyBooted()
176
177      if self._force_main_user:
178        # Ensure the current user is the main user (the first real human user).
179        main_user = d.GetMainUser()
180        if d.GetCurrentUser() != main_user:
181          logging.info('Switching to the main user with id %s', main_user)
182          d.SwitchUser(main_user)
183        d.target_user = main_user
184      elif d.GetCurrentUser() != SYSTEM_USER_ID:
185        # TODO(b/293175593): Remove this after "force_main_user" works fine.
186        # Use system user to run tasks to avoid "/sdcard "accessing issue
187        # due to multiple-users. For details, see
188        # https://source.android.com/docs/devices/admin/multi-user-testing
189        logging.info('Switching to user with id %s', SYSTEM_USER_ID)
190        d.SwitchUser(SYSTEM_USER_ID)
191
192      if self._enable_device_cache:
193        cache_path = _DeviceCachePath(d)
194        if os.path.exists(cache_path):
195          logging.info('Using device cache: %s', cache_path)
196          with open(cache_path) as f:
197            d.LoadCacheData(f.read())
198          # Delete cached file so that any exceptions cause it to be cleared.
199          os.unlink(cache_path)
200
201      if self._logcat_output_dir:
202        logcat_file = os.path.join(
203            self._logcat_output_dir,
204            '%s_%s' % (d.adb.GetDeviceSerial(),
205                       datetime.datetime.utcnow().strftime('%Y%m%dT%H%M%S')))
206        monitor = logcat_monitor.LogcatMonitor(d.adb,
207                                               clear=True,
208                                               output_file=logcat_file,
209                                               check_error=False)
210        self._logcat_monitors.append(monitor)
211        monitor.Start()
212
213    self.parallel_devices.pMap(prepare_device)
214
215  @property
216  def current_try(self):
217    return self._current_try
218
219  def IncrementCurrentTry(self):
220    self._current_try += 1
221
222  def ResetCurrentTry(self):
223    self._current_try = 0
224
225  @property
226  def denylist(self):
227    return self._denylist
228
229  @property
230  def concurrent_adb(self):
231    return self._concurrent_adb
232
233  @property
234  def devices(self):
235    # Initialize lazily so that host-only tests do not fail when no devices are
236    # attached.
237    if self._devices is None:
238      self._InitDevices()
239    return self._devices
240
241  @property
242  def max_tries(self):
243    return self._max_tries
244
245  @property
246  def parallel_devices(self):
247    return parallelizer.SyncParallelizer(self.devices)
248
249  @property
250  def recover_devices(self):
251    return self._recover_devices
252
253  @property
254  def skip_clear_data(self):
255    return self._skip_clear_data
256
257  @property
258  def trace_output(self):
259    return self._trace_output
260
261  @property
262  def disable_test_server(self):
263    return self._disable_test_server
264
265  @property
266  def force_main_user(self):
267    return self._force_main_user
268
269  #override
270  def TearDown(self):
271    if self.trace_output and self._trace_all:
272      instrumentation_tracing.stop_instrumenting()
273    elif self.trace_output:
274      self.DisableTracing()
275
276    # By default, teardown will invoke ADB. When receiving SIGTERM due to a
277    # timeout, there's a high probability that ADB is non-responsive. In these
278    # cases, sending an ADB command will potentially take a long time to time
279    # out. Before this happens, the process will be hard-killed for not
280    # responding to SIGTERM fast enough.
281    if self._received_sigterm:
282      return
283
284    if not self._devices:
285      return
286
287    @handle_shard_failures_with(on_failure=self.DenylistDevice)
288    def tear_down_device(d):
289      # Write the cache even when not using it so that it will be ready the
290      # first time that it is enabled. Writing it every time is also necessary
291      # so that an invalid cache can be flushed just by disabling it for one
292      # run.
293      cache_path = _DeviceCachePath(d)
294      if os.path.exists(os.path.dirname(cache_path)):
295        with open(cache_path, 'w') as f:
296          f.write(d.DumpCacheData())
297          logging.info('Wrote device cache: %s', cache_path)
298      else:
299        logging.warning(
300            'Unable to write device cache as %s directory does not exist',
301            os.path.dirname(cache_path))
302
303    self.parallel_devices.pMap(tear_down_device)
304
305    for m in self._logcat_monitors:
306      try:
307        m.Stop()
308        m.Close()
309        _, temp_path = tempfile.mkstemp()
310        with open(m.output_file, 'r') as infile:
311          with open(temp_path, 'w') as outfile:
312            for line in infile:
313              outfile.write('Device(%s) %s' % (m.adb.GetDeviceSerial(), line))
314        shutil.move(temp_path, m.output_file)
315      except base_error.BaseError:
316        logging.exception('Failed to stop logcat monitor for %s',
317                          m.adb.GetDeviceSerial())
318      except IOError:
319        logging.exception('Failed to locate logcat for device %s',
320                          m.adb.GetDeviceSerial())
321
322    if self._logcat_output_file:
323      file_utils.MergeFiles(
324          self._logcat_output_file,
325          [m.output_file for m in self._logcat_monitors
326           if os.path.exists(m.output_file)])
327      shutil.rmtree(self._logcat_output_dir)
328
329  def DenylistDevice(self, device, reason='local_device_failure'):
330    device_serial = device.adb.GetDeviceSerial()
331    if self._denylist:
332      self._denylist.Extend([device_serial], reason=reason)
333    with self._devices_lock:
334      self._devices = [d for d in self._devices if str(d) != device_serial]
335    logging.error('Device %s denylisted: %s', device_serial, reason)
336    if not self._devices:
337      raise device_errors.NoDevicesError(
338          'All devices were denylisted due to errors')
339
340  @staticmethod
341  def DisableTracing():
342    if not trace_event.trace_is_enabled():
343      logging.warning('Tracing is not running.')
344    else:
345      trace_event.trace_disable()
346
347  def EnableTracing(self):
348    if trace_event.trace_is_enabled():
349      logging.warning('Tracing is already running.')
350    else:
351      trace_event.trace_enable(self._trace_output)
352