1# Copyright 2018 the V8 project authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""
6Wrapper around the Android device abstraction from src/build/android.
7"""
8
9import logging
10import os
11import sys
12
13
14BASE_DIR = os.path.normpath(
15    os.path.join(os.path.dirname(__file__), '..', '..', '..'))
16ANDROID_DIR = os.path.join(BASE_DIR, 'build', 'android')
17DEVICE_DIR = '/data/local/tmp/v8/'
18
19
20class TimeoutException(Exception):
21  def __init__(self, timeout):
22    self.timeout = timeout
23
24
25class CommandFailedException(Exception):
26  def __init__(self, status, output):
27    self.status = status
28    self.output = output
29
30
31class _Driver(object):
32  """Helper class to execute shell commands on an Android device."""
33  def __init__(self, device=None):
34    assert os.path.exists(ANDROID_DIR)
35    sys.path.insert(0, ANDROID_DIR)
36
37    # We import the dependencies only on demand, so that this file can be
38    # imported unconditionally.
39    import devil_chromium
40    from devil.android import device_errors  # pylint: disable=import-error
41    from devil.android import device_utils  # pylint: disable=import-error
42    from devil.android.perf import cache_control  # pylint: disable=import-error
43    from devil.android.perf import perf_control  # pylint: disable=import-error
44    from devil.android.sdk import adb_wrapper  # pylint: disable=import-error
45    global cache_control
46    global device_errors
47    global perf_control
48
49    devil_chromium.Initialize()
50
51    if not device:
52      # Detect attached device if not specified.
53      devices = adb_wrapper.AdbWrapper.Devices()
54      assert devices, 'No devices detected'
55      assert len(devices) == 1, 'Multiple devices detected.'
56      device = str(devices[0])
57    self.adb_wrapper = adb_wrapper.AdbWrapper(device)
58    self.device = device_utils.DeviceUtils(self.adb_wrapper)
59
60    # This remembers what we have already pushed to the device.
61    self.pushed = set()
62
63  def tear_down(self):
64    """Clean up files after running all tests."""
65    self.device.RemovePath(DEVICE_DIR, force=True, recursive=True)
66
67  def push_file(self, host_dir, file_name, target_rel='.',
68                skip_if_missing=False):
69    """Push a single file to the device (cached).
70
71    Args:
72      host_dir: Absolute parent directory of the file to push.
73      file_name: Name of the file to push.
74      target_rel: Parent directory of the target location on the device
75          (relative to the device's base dir for testing).
76      skip_if_missing: Keeps silent about missing files when set. Otherwise logs
77          error.
78    """
79    file_on_host = os.path.join(host_dir, file_name)
80
81    # Only push files not yet pushed in one execution.
82    if file_on_host in self.pushed:
83      return
84
85    file_on_device_tmp = os.path.join(DEVICE_DIR, '_tmp_', file_name)
86    file_on_device = os.path.join(DEVICE_DIR, target_rel, file_name)
87    folder_on_device = os.path.dirname(file_on_device)
88
89    # Only attempt to push files that exist.
90    if not os.path.exists(file_on_host):
91      if not skip_if_missing:
92        logging.critical('Missing file on host: %s' % file_on_host)
93      return
94
95    # Work-around for 'text file busy' errors. Push the files to a temporary
96    # location and then copy them with a shell command.
97    output = self.adb_wrapper.Push(file_on_host, file_on_device_tmp)
98    # Success looks like this: '3035 KB/s (12512056 bytes in 4.025s)'.
99    # Errors look like this: 'failed to copy  ... '.
100    if output and not re.search('^[0-9]', output.splitlines()[-1]):
101      logging.critical('PUSH FAILED: ' + output)
102    self.adb_wrapper.Shell('mkdir -p %s' % folder_on_device)
103    self.adb_wrapper.Shell('cp %s %s' % (file_on_device_tmp, file_on_device))
104    self.pushed.add(file_on_host)
105
106  def push_executable(self, shell_dir, target_dir, binary):
107    """Push files required to run a V8 executable.
108
109    Args:
110      shell_dir: Absolute parent directory of the executable on the host.
111      target_dir: Parent directory of the executable on the device (relative to
112          devices' base dir for testing).
113      binary: Name of the binary to push.
114    """
115    self.push_file(shell_dir, binary, target_dir)
116
117    # Push external startup data. Backwards compatible for revisions where
118    # these files didn't exist. Or for bots that don't produce these files.
119    self.push_file(
120        shell_dir,
121        'natives_blob.bin',
122        target_dir,
123        skip_if_missing=True,
124    )
125    self.push_file(
126        shell_dir,
127        'snapshot_blob.bin',
128        target_dir,
129        skip_if_missing=True,
130    )
131    self.push_file(
132        shell_dir,
133        'snapshot_blob_trusted.bin',
134        target_dir,
135        skip_if_missing=True,
136    )
137    self.push_file(
138        shell_dir,
139        'icudtl.dat',
140        target_dir,
141        skip_if_missing=True,
142    )
143
144  def run(self, target_dir, binary, args, rel_path, timeout, env=None,
145          logcat_file=False):
146    """Execute a command on the device's shell.
147
148    Args:
149      target_dir: Parent directory of the executable on the device (relative to
150          devices' base dir for testing).
151      binary: Name of the binary.
152      args: List of arguments to pass to the binary.
153      rel_path: Relative path on device to use as CWD.
154      timeout: Timeout in seconds.
155      env: The environment variables with which the command should be run.
156      logcat_file: File into which to stream adb logcat log.
157    """
158    binary_on_device = os.path.join(DEVICE_DIR, target_dir, binary)
159    cmd = [binary_on_device] + args
160    def run_inner():
161      try:
162        output = self.device.RunShellCommand(
163            cmd,
164            cwd=os.path.join(DEVICE_DIR, rel_path),
165            check_return=True,
166            env=env,
167            timeout=timeout,
168            retries=0,
169        )
170        return '\n'.join(output)
171      except device_errors.AdbCommandFailedError as e:
172        raise CommandFailedException(e.status, e.output)
173      except device_errors.CommandTimeoutError:
174        raise TimeoutException(timeout)
175
176
177    if logcat_file:
178      with self.device.GetLogcatMonitor(output_file=logcat_file) as logmon:
179        result = run_inner()
180      logmon.Close()
181      return result
182    else:
183      return run_inner()
184
185  def drop_ram_caches(self):
186    """Drop ran caches on device."""
187    cache = cache_control.CacheControl(self.device)
188    cache.DropRamCaches()
189
190  def set_high_perf_mode(self):
191    """Set device into high performance mode."""
192    perf = perf_control.PerfControl(self.device)
193    perf.SetHighPerfMode()
194
195  def set_default_perf_mode(self):
196    """Set device into default performance mode."""
197    perf = perf_control.PerfControl(self.device)
198    perf.SetDefaultPerfMode()
199
200
201_ANDROID_DRIVER = None
202def android_driver(device=None):
203  """Singleton access method to the driver class."""
204  global _ANDROID_DRIVER
205  if not _ANDROID_DRIVER:
206    _ANDROID_DRIVER = _Driver(device)
207  return _ANDROID_DRIVER
208