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