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