# Copyright 2018 the V8 project authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """ Wrapper around the Android device abstraction from src/build/android. """ import logging import os import sys import re BASE_DIR = os.path.normpath( os.path.join(os.path.dirname(__file__), '..', '..', '..')) ANDROID_DIR = os.path.join(BASE_DIR, 'build', 'android') DEVICE_DIR = '/data/local/tmp/v8/' class TimeoutException(Exception): def __init__(self, timeout, output=None): self.timeout = timeout self.output = output class CommandFailedException(Exception): def __init__(self, status, output): self.status = status self.output = output class _Driver(object): """Helper class to execute shell commands on an Android device.""" def __init__(self, device=None): assert os.path.exists(ANDROID_DIR) sys.path.insert(0, ANDROID_DIR) # We import the dependencies only on demand, so that this file can be # imported unconditionally. import devil_chromium from devil.android import device_errors # pylint: disable=import-error from devil.android import device_utils # pylint: disable=import-error from devil.android.perf import cache_control # pylint: disable=import-error from devil.android.perf import perf_control # pylint: disable=import-error global cache_control global device_errors global perf_control devil_chromium.Initialize() # Find specified device or a single attached device if none was specified. # In case none or multiple devices are attached, this raises an exception. self.device = device_utils.DeviceUtils.HealthyDevices( retries=5, enable_usb_resets=True, device_arg=device)[0] # This remembers what we have already pushed to the device. self.pushed = set() def tear_down(self): """Clean up files after running all tests.""" self.device.RemovePath(DEVICE_DIR, force=True, recursive=True) def push_file(self, host_dir, file_name, target_rel='.', skip_if_missing=False): """Push a single file to the device (cached). Args: host_dir: Absolute parent directory of the file to push. file_name: Name of the file to push. target_rel: Parent directory of the target location on the device (relative to the device's base dir for testing). skip_if_missing: Keeps silent about missing files when set. Otherwise logs error. """ # TODO(sergiyb): Implement this method using self.device.PushChangedFiles to # avoid accessing low-level self.device.adb. file_on_host = os.path.join(host_dir, file_name) # Only push files not yet pushed in one execution. if file_on_host in self.pushed: return file_on_device_tmp = os.path.join(DEVICE_DIR, '_tmp_', file_name) file_on_device = os.path.join(DEVICE_DIR, target_rel, file_name) folder_on_device = os.path.dirname(file_on_device) # Only attempt to push files that exist. if not os.path.exists(file_on_host): if not skip_if_missing: logging.critical('Missing file on host: %s' % file_on_host) return # Work-around for 'text file busy' errors. Push the files to a temporary # location and then copy them with a shell command. output = self.device.adb.Push(file_on_host, file_on_device_tmp) # Success looks like this: '3035 KB/s (12512056 bytes in 4.025s)'. # Errors look like this: 'failed to copy ... '. if output and not re.search('^[0-9]', output.splitlines()[-1]): logging.critical('PUSH FAILED: ' + output) self.device.adb.Shell('mkdir -p %s' % folder_on_device) self.device.adb.Shell('cp %s %s' % (file_on_device_tmp, file_on_device)) self.pushed.add(file_on_host) def push_executable(self, shell_dir, target_dir, binary): """Push files required to run a V8 executable. Args: shell_dir: Absolute parent directory of the executable on the host. target_dir: Parent directory of the executable on the device (relative to devices' base dir for testing). binary: Name of the binary to push. """ self.push_file(shell_dir, binary, target_dir) # Push external startup data. Backwards compatible for revisions where # these files didn't exist. Or for bots that don't produce these files. self.push_file( shell_dir, 'natives_blob.bin', target_dir, skip_if_missing=True, ) self.push_file( shell_dir, 'snapshot_blob.bin', target_dir, skip_if_missing=True, ) self.push_file( shell_dir, 'snapshot_blob_trusted.bin', target_dir, skip_if_missing=True, ) self.push_file( shell_dir, 'icudtl.dat', target_dir, skip_if_missing=True, ) def run(self, target_dir, binary, args, rel_path, timeout, env=None, logcat_file=False): """Execute a command on the device's shell. Args: target_dir: Parent directory of the executable on the device (relative to devices' base dir for testing). binary: Name of the binary. args: List of arguments to pass to the binary. rel_path: Relative path on device to use as CWD. timeout: Timeout in seconds. env: The environment variables with which the command should be run. logcat_file: File into which to stream adb logcat log. """ binary_on_device = os.path.join(DEVICE_DIR, target_dir, binary) cmd = [binary_on_device] + args def run_inner(): try: output = self.device.RunShellCommand( cmd, cwd=os.path.join(DEVICE_DIR, rel_path), check_return=True, env=env, timeout=timeout, retries=0, ) return '\n'.join(output) except device_errors.AdbCommandFailedError as e: raise CommandFailedException(e.status, e.output) except device_errors.CommandTimeoutError as e: raise TimeoutException(timeout, e.output) if logcat_file: with self.device.GetLogcatMonitor(output_file=logcat_file) as logmon: result = run_inner() logmon.Close() return result else: return run_inner() def drop_ram_caches(self): """Drop ran caches on device.""" cache = cache_control.CacheControl(self.device) cache.DropRamCaches() def set_high_perf_mode(self): """Set device into high performance mode.""" perf = perf_control.PerfControl(self.device) perf.SetHighPerfMode() def set_default_perf_mode(self): """Set device into default performance mode.""" perf = perf_control.PerfControl(self.device) perf.SetDefaultPerfMode() _ANDROID_DRIVER = None def android_driver(device=None): """Singleton access method to the driver class.""" global _ANDROID_DRIVER if not _ANDROID_DRIVER: _ANDROID_DRIVER = _Driver(device) return _ANDROID_DRIVER