• 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(1262303): After Telemetry is supported by python3 we can re-add
90# 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._tool_name = args.tool
112    self._trace_output = None
113    # Must check if arg exist because this class is used by
114    # //third_party/catapult's browser_options.py
115    if hasattr(args, 'trace_output'):
116      self._trace_output = args.trace_output
117    self._trace_all = None
118    if hasattr(args, 'trace_all'):
119      self._trace_all = args.trace_all
120    self._force_main_user = False
121    if hasattr(args, 'force_main_user'):
122      self._force_main_user = args.force_main_user
123    self._use_persistent_shell = args.use_persistent_shell
124    self._disable_test_server = args.disable_test_server
125
126    use_local_devil_tools = False
127    if hasattr(args, 'use_local_devil_tools'):
128      use_local_devil_tools = args.use_local_devil_tools
129
130    devil_chromium.Initialize(output_directory=constants.GetOutDirectory(),
131                              adb_path=args.adb_path,
132                              use_local_devil_tools=use_local_devil_tools)
133
134    # Some things such as Forwarder require ADB to be in the environment path,
135    # while others like Devil's bundletool.py require Java on the path.
136    adb_dir = os.path.dirname(adb_wrapper.AdbWrapper.GetAdbPath())
137    if adb_dir and adb_dir not in os.environ['PATH'].split(os.pathsep):
138      os.environ['PATH'] = os.pathsep.join(
139          [adb_dir, host_paths.JAVA_PATH, os.environ['PATH']])
140
141  #override
142  def SetUp(self):
143    if self.trace_output and self._trace_all:
144      to_include = [r"pylib\..*", r"devil\..*", "__main__"]
145      to_exclude = ["logging"]
146      instrumentation_tracing.start_instrumenting(self.trace_output, to_include,
147                                                  to_exclude)
148    elif self.trace_output:
149      self.EnableTracing()
150
151  # Must be called before accessing |devices|.
152  def SetPreferredAbis(self, abis):
153    assert self._devices is None
154    self._preferred_abis = abis
155
156  def _InitDevices(self):
157    device_arg = []
158    if self._device_serials:
159      device_arg = self._device_serials
160
161    self._devices = device_utils.DeviceUtils.HealthyDevices(
162        self._denylist,
163        retries=5,
164        enable_usb_resets=True,
165        enable_device_files_cache=self._enable_device_cache,
166        default_retries=self._max_tries - 1,
167        device_arg=device_arg,
168        abis=self._preferred_abis,
169        persistent_shell=self._use_persistent_shell)
170
171    if self._logcat_output_file:
172      self._logcat_output_dir = tempfile.mkdtemp()
173
174    @handle_shard_failures_with(on_failure=self.DenylistDevice)
175    def prepare_device(d):
176      d.WaitUntilFullyBooted()
177
178      if self._force_main_user:
179        # Ensure the current user is the main user (the first real human user).
180        main_user = d.GetMainUser()
181        if d.GetCurrentUser() != main_user:
182          logging.info('Switching to the main user with id %s', main_user)
183          d.SwitchUser(main_user)
184        d.target_user = main_user
185      elif d.GetCurrentUser() != SYSTEM_USER_ID:
186        # TODO(b/293175593): Remove this after "force_main_user" works fine.
187        # Use system user to run tasks to avoid "/sdcard "accessing issue
188        # due to multiple-users. For details, see
189        # https://source.android.com/docs/devices/admin/multi-user-testing
190        logging.info('Switching to user with id %s', SYSTEM_USER_ID)
191        d.SwitchUser(SYSTEM_USER_ID)
192
193      if self._enable_device_cache:
194        cache_path = _DeviceCachePath(d)
195        if os.path.exists(cache_path):
196          logging.info('Using device cache: %s', cache_path)
197          with open(cache_path) as f:
198            d.LoadCacheData(f.read())
199          # Delete cached file so that any exceptions cause it to be cleared.
200          os.unlink(cache_path)
201
202      if self._logcat_output_dir:
203        logcat_file = os.path.join(
204            self._logcat_output_dir,
205            '%s_%s' % (d.adb.GetDeviceSerial(),
206                       datetime.datetime.utcnow().strftime('%Y%m%dT%H%M%S')))
207        monitor = logcat_monitor.LogcatMonitor(d.adb,
208                                               clear=True,
209                                               output_file=logcat_file,
210                                               check_error=False)
211        self._logcat_monitors.append(monitor)
212        monitor.Start()
213
214    self.parallel_devices.pMap(prepare_device)
215
216  @property
217  def current_try(self):
218    return self._current_try
219
220  def IncrementCurrentTry(self):
221    self._current_try += 1
222
223  def ResetCurrentTry(self):
224    self._current_try = 0
225
226  @property
227  def denylist(self):
228    return self._denylist
229
230  @property
231  def concurrent_adb(self):
232    return self._concurrent_adb
233
234  @property
235  def devices(self):
236    # Initialize lazily so that host-only tests do not fail when no devices are
237    # attached.
238    if self._devices is None:
239      self._InitDevices()
240    return self._devices
241
242  @property
243  def max_tries(self):
244    return self._max_tries
245
246  @property
247  def parallel_devices(self):
248    return parallelizer.SyncParallelizer(self.devices)
249
250  @property
251  def recover_devices(self):
252    return self._recover_devices
253
254  @property
255  def skip_clear_data(self):
256    return self._skip_clear_data
257
258  @property
259  def tool(self):
260    return self._tool_name
261
262  @property
263  def trace_output(self):
264    return self._trace_output
265
266  @property
267  def disable_test_server(self):
268    return self._disable_test_server
269
270  @property
271  def force_main_user(self):
272    return self._force_main_user
273
274  #override
275  def TearDown(self):
276    if self.trace_output and self._trace_all:
277      instrumentation_tracing.stop_instrumenting()
278    elif self.trace_output:
279      self.DisableTracing()
280
281    # By default, teardown will invoke ADB. When receiving SIGTERM due to a
282    # timeout, there's a high probability that ADB is non-responsive. In these
283    # cases, sending an ADB command will potentially take a long time to time
284    # out. Before this happens, the process will be hard-killed for not
285    # responding to SIGTERM fast enough.
286    if self._received_sigterm:
287      return
288
289    if not self._devices:
290      return
291
292    @handle_shard_failures_with(on_failure=self.DenylistDevice)
293    def tear_down_device(d):
294      # Write the cache even when not using it so that it will be ready the
295      # first time that it is enabled. Writing it every time is also necessary
296      # so that an invalid cache can be flushed just by disabling it for one
297      # run.
298      cache_path = _DeviceCachePath(d)
299      if os.path.exists(os.path.dirname(cache_path)):
300        with open(cache_path, 'w') as f:
301          f.write(d.DumpCacheData())
302          logging.info('Wrote device cache: %s', cache_path)
303      else:
304        logging.warning(
305            'Unable to write device cache as %s directory does not exist',
306            os.path.dirname(cache_path))
307
308    self.parallel_devices.pMap(tear_down_device)
309
310    for m in self._logcat_monitors:
311      try:
312        m.Stop()
313        m.Close()
314        _, temp_path = tempfile.mkstemp()
315        with open(m.output_file, 'r') as infile:
316          with open(temp_path, 'w') as outfile:
317            for line in infile:
318              outfile.write('Device(%s) %s' % (m.adb.GetDeviceSerial(), line))
319        shutil.move(temp_path, m.output_file)
320      except base_error.BaseError:
321        logging.exception('Failed to stop logcat monitor for %s',
322                          m.adb.GetDeviceSerial())
323      except IOError:
324        logging.exception('Failed to locate logcat for device %s',
325                          m.adb.GetDeviceSerial())
326
327    if self._logcat_output_file:
328      file_utils.MergeFiles(
329          self._logcat_output_file,
330          [m.output_file for m in self._logcat_monitors
331           if os.path.exists(m.output_file)])
332      shutil.rmtree(self._logcat_output_dir)
333
334  def DenylistDevice(self, device, reason='local_device_failure'):
335    device_serial = device.adb.GetDeviceSerial()
336    if self._denylist:
337      self._denylist.Extend([device_serial], reason=reason)
338    with self._devices_lock:
339      self._devices = [d for d in self._devices if str(d) != device_serial]
340    logging.error('Device %s denylisted: %s', device_serial, reason)
341    if not self._devices:
342      raise device_errors.NoDevicesError(
343          'All devices were denylisted due to errors')
344
345  @staticmethod
346  def DisableTracing():
347    if not trace_event.trace_is_enabled():
348      logging.warning('Tracing is not running.')
349    else:
350      trace_event.trace_disable()
351
352  def EnableTracing(self):
353    if trace_event.trace_is_enabled():
354      logging.warning('Tracing is already running.')
355    else:
356      trace_event.trace_enable(self._trace_output)
357