• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2013 The Chromium 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
5import logging
6import os
7import pipes
8import re
9import subprocess
10import sys
11import time
12
13from telemetry.core import exceptions
14from telemetry.core import forwarders
15from telemetry.core import util
16from telemetry.core.backends import adb_commands
17from telemetry.core.backends import browser_backend
18from telemetry.core.backends.chrome import chrome_browser_backend
19from telemetry.core.forwarders import android_forwarder
20
21util.AddDirToPythonPath(util.GetChromiumSrcDir(), 'build', 'android')
22from pylib.device import device_errors  # pylint: disable=F0401
23from pylib.device import intent  # pylint: disable=F0401
24
25
26class AndroidBrowserBackendSettings(object):
27
28  def __init__(self, activity, cmdline_file, package, pseudo_exec_name,
29               supports_tab_control, relax_ssl_check=False):
30    self.activity = activity
31    self._cmdline_file = cmdline_file
32    self.package = package
33    self.pseudo_exec_name = pseudo_exec_name
34    self.supports_tab_control = supports_tab_control
35    self.relax_ssl_check = relax_ssl_check
36
37  def GetCommandLineFile(self, is_user_debug_build):  # pylint: disable=W0613
38    return self._cmdline_file
39
40  def GetDevtoolsRemotePort(self, adb):
41    raise NotImplementedError()
42
43  def RemoveProfile(self, adb):
44    files = adb.device().RunShellCommand(
45        'ls "%s"' % self.profile_dir, as_root=True)
46    # Don't delete lib, since it is created by the installer.
47    paths = ['"%s/%s"' % (self.profile_dir, f) for f in files if f != 'lib']
48    adb.device().RunShellCommand('rm -r %s' % ' '.join(paths), as_root=True)
49
50  def PushProfile(self, _new_profile_dir, _adb):
51    logging.critical('Profiles cannot be overriden with current configuration')
52    sys.exit(1)
53
54  @property
55  def profile_dir(self):
56    return '/data/data/%s/' % self.package
57
58
59class ChromeBackendSettings(AndroidBrowserBackendSettings):
60  # Stores a default Preferences file, re-used to speed up "--page-repeat".
61  _default_preferences_file = None
62
63  def GetCommandLineFile(self, is_user_debug_build):
64    if is_user_debug_build:
65      return '/data/local/tmp/chrome-command-line'
66    else:
67      return '/data/local/chrome-command-line'
68
69  def __init__(self, package):
70    super(ChromeBackendSettings, self).__init__(
71        activity='com.google.android.apps.chrome.Main',
72        cmdline_file=None,
73        package=package,
74        pseudo_exec_name='chrome',
75        supports_tab_control=True)
76
77  def GetDevtoolsRemotePort(self, adb):
78    return 'localabstract:chrome_devtools_remote'
79
80  def PushProfile(self, new_profile_dir, adb):
81    # Pushing the profile is slow, so we don't want to do it every time.
82    # Avoid this by pushing to a safe location using PushChangedFiles, and
83    # then copying into the correct location on each test run.
84
85    (profile_parent, profile_base) = os.path.split(new_profile_dir)
86    # If the path ends with a '/' python split will return an empty string for
87    # the base name; so we now need to get the base name from the directory.
88    if not profile_base:
89      profile_base = os.path.basename(profile_parent)
90
91    saved_profile_location = '/sdcard/profile/%s' % profile_base
92    adb.device().PushChangedFiles(new_profile_dir, saved_profile_location)
93
94    adb.device().old_interface.EfficientDeviceDirectoryCopy(
95        saved_profile_location, self.profile_dir)
96    dumpsys = adb.device().RunShellCommand(
97        'dumpsys package %s' % self.package)
98    id_line = next(line for line in dumpsys if 'userId=' in line)
99    uid = re.search('\d+', id_line).group()
100    files = adb.device().RunShellCommand(
101        'ls "%s"' % self.profile_dir, as_root=True)
102    files.remove('lib')
103    paths = ['%s/%s' % (self.profile_dir, f) for f in files]
104    for path in paths:
105      extended_path = '%s %s/* %s/*/* %s/*/*/*' % (path, path, path, path)
106      adb.device().RunShellCommand(
107          'chown %s.%s %s' % (uid, uid, extended_path))
108
109class ContentShellBackendSettings(AndroidBrowserBackendSettings):
110  def __init__(self, package):
111    super(ContentShellBackendSettings, self).__init__(
112        activity='org.chromium.content_shell_apk.ContentShellActivity',
113        cmdline_file='/data/local/tmp/content-shell-command-line',
114        package=package,
115        pseudo_exec_name='content_shell',
116        supports_tab_control=False)
117
118  def GetDevtoolsRemotePort(self, adb):
119    return 'localabstract:content_shell_devtools_remote'
120
121
122class ChromeShellBackendSettings(AndroidBrowserBackendSettings):
123  def __init__(self, package):
124    super(ChromeShellBackendSettings, self).__init__(
125          activity='org.chromium.chrome.shell.ChromeShellActivity',
126          cmdline_file='/data/local/tmp/chrome-shell-command-line',
127          package=package,
128          pseudo_exec_name='chrome_shell',
129          supports_tab_control=False)
130
131  def GetDevtoolsRemotePort(self, adb):
132    return 'localabstract:chrome_shell_devtools_remote'
133
134class WebviewBackendSettings(AndroidBrowserBackendSettings):
135  def __init__(self, package,
136               activity='org.chromium.telemetry_shell.TelemetryActivity'):
137    super(WebviewBackendSettings, self).__init__(
138        activity=activity,
139        cmdline_file='/data/local/tmp/webview-command-line',
140        package=package,
141        pseudo_exec_name='webview',
142        supports_tab_control=False)
143
144  def GetDevtoolsRemotePort(self, adb):
145    # The DevTools socket name for WebView depends on the activity PID's.
146    retries = 0
147    timeout = 1
148    pid = None
149    while True:
150      pids = adb.ExtractPid(self.package)
151      if (len(pids) > 0):
152        pid = pids[-1]
153        break
154      time.sleep(timeout)
155      retries += 1
156      timeout *= 2
157      if retries == 4:
158        logging.critical('android_browser_backend: Timeout while waiting for '
159                         'activity %s:%s to come up',
160                         self.package,
161                         self.activity)
162        raise exceptions.BrowserGoneException(self.browser,
163                                              'Timeout waiting for PID.')
164    return 'localabstract:webview_devtools_remote_%s' % str(pid)
165
166class WebviewShellBackendSettings(WebviewBackendSettings):
167  def __init__(self, package):
168    super(WebviewShellBackendSettings, self).__init__(
169        activity='org.chromium.android_webview.shell.AwShellActivity',
170        package=package)
171
172class AndroidBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
173  """The backend for controlling a browser instance running on Android."""
174  def __init__(self, browser_options, backend_settings, use_rndis_forwarder,
175               output_profile_path, extensions_to_load, target_arch,
176               android_platform_backend):
177    super(AndroidBrowserBackend, self).__init__(
178        supports_tab_control=backend_settings.supports_tab_control,
179        supports_extensions=False, browser_options=browser_options,
180        output_profile_path=output_profile_path,
181        extensions_to_load=extensions_to_load)
182    if len(extensions_to_load) > 0:
183      raise browser_backend.ExtensionsNotSupportedException(
184          'Android browser does not support extensions.')
185
186    # Initialize fields so that an explosion during init doesn't break in Close.
187    self._android_platform_backend = android_platform_backend
188    self._backend_settings = backend_settings
189    self._saved_cmdline = ''
190    self._target_arch = target_arch
191    self._saved_sslflag = ''
192
193    # TODO(tonyg): This is flaky because it doesn't reserve the port that it
194    # allocates. Need to fix this.
195    self._port = adb_commands.AllocateTestServerPort()
196
197    # Disables android.net SSL certificate check.  This is necessary for
198    # applications using the android.net stack to work with proxy HTTPS server
199    # created by telemetry
200    if self._backend_settings.relax_ssl_check:
201      self._saved_sslflag = self._adb.device().GetProp('socket.relaxsslcheck')
202      self._adb.device().SetProp('socket.relaxsslcheck', 'yes')
203
204    # Kill old browser.
205    self._KillBrowser()
206
207    if self._adb.device().old_interface.CanAccessProtectedFileContents():
208      if self.browser_options.profile_dir:
209        self._backend_settings.PushProfile(self.browser_options.profile_dir,
210                                           self._adb)
211      elif not self.browser_options.dont_override_profile:
212        self._backend_settings.RemoveProfile(self._adb)
213
214    self._forwarder_factory = android_forwarder.AndroidForwarderFactory(
215        self._adb, use_rndis_forwarder)
216
217    if self.browser_options.netsim or use_rndis_forwarder:
218      assert use_rndis_forwarder, 'Netsim requires RNDIS forwarding.'
219      self.wpr_port_pairs = forwarders.PortPairs(
220          http=forwarders.PortPair(0, 80),
221          https=forwarders.PortPair(0, 443),
222          dns=forwarders.PortPair(0, 53))
223
224    # Set the debug app if needed.
225    if self._adb.IsUserBuild():
226      logging.debug('User build device, setting debug app')
227      self._adb.device().RunShellCommand(
228          'am set-debug-app --persistent %s' % self._backend_settings.package)
229
230  @property
231  def _adb(self):
232    return self._android_platform_backend.adb
233
234  def _KillBrowser(self):
235    # We use KillAll rather than ForceStop for efficiency reasons.
236    try:
237      self._adb.device().KillAll(self._backend_settings.package, retries=0)
238    except device_errors.CommandFailedError:
239      pass
240
241  def _SetUpCommandLine(self):
242    def QuoteIfNeeded(arg):
243      # Properly escape "key=valueA valueB" to "key='valueA valueB'"
244      # Values without spaces, or that seem to be quoted are left untouched.
245      # This is required so CommandLine.java can parse valueB correctly rather
246      # than as a separate switch.
247      params = arg.split('=', 1)
248      if len(params) != 2:
249        return arg
250      key, values = params
251      if ' ' not in values:
252        return arg
253      if values[0] in '"\'' and values[-1] == values[0]:
254        return arg
255      return '%s=%s' % (key, pipes.quote(values))
256    args = [self._backend_settings.pseudo_exec_name]
257    args.extend(self.GetBrowserStartupArgs())
258    content = ' '.join(QuoteIfNeeded(arg) for arg in args)
259    cmdline_file = self._backend_settings.GetCommandLineFile(
260        self._adb.IsUserBuild())
261    as_root = self._adb.device().old_interface.CanAccessProtectedFileContents()
262
263    try:
264      # Save the current command line to restore later, except if it appears to
265      # be a  Telemetry created one. This is to prevent a common bug where
266      # --host-resolver-rules borks people's browsers if something goes wrong
267      # with Telemetry.
268      self._saved_cmdline = ''.join(self._adb.device().ReadFile(cmdline_file))
269      if '--host-resolver-rules' in self._saved_cmdline:
270        self._saved_cmdline = ''
271      self._adb.device().WriteTextFile(cmdline_file, content, as_root=as_root)
272    except device_errors.CommandFailedError:
273      logging.critical('Cannot set Chrome command line. '
274                       'Fix this by flashing to a userdebug build.')
275      sys.exit(1)
276
277  def _RestoreCommandLine(self):
278    cmdline_file = self._backend_settings.GetCommandLineFile(
279        self._adb.IsUserBuild())
280    as_root = self._adb.device().old_interface.CanAccessProtectedFileContents()
281    self._adb.device().WriteTextFile(cmdline_file, self._saved_cmdline,
282                                     as_root=as_root)
283
284  def Start(self):
285    self._SetUpCommandLine()
286
287    self._adb.device().RunShellCommand('logcat -c')
288    if self.browser_options.startup_url:
289      url = self.browser_options.startup_url
290    elif self.browser_options.profile_dir:
291      url = None
292    else:
293      # If we have no existing tabs start with a blank page since default
294      # startup with the NTP can lead to race conditions with Telemetry
295      url = 'about:blank'
296    # Dismiss any error dialogs. Limit the number in case we have an error loop
297    # or we are failing to dismiss.
298    for _ in xrange(10):
299      if not self._adb.device().old_interface.DismissCrashDialogIfNeeded():
300        break
301    self._adb.device().StartActivity(
302        intent.Intent(package=self._backend_settings.package,
303                      activity=self._backend_settings.activity,
304                      action=None, data=url, category=None),
305        blocking=True)
306
307    self._adb.Forward('tcp:%d' % self._port,
308                      self._backend_settings.GetDevtoolsRemotePort(self._adb))
309
310    try:
311      self._WaitForBrowserToComeUp()
312    except exceptions.BrowserGoneException:
313      logging.critical('Failed to connect to browser.')
314      if not self._adb.device().old_interface.CanAccessProtectedFileContents():
315        logging.critical(
316          'Resolve this by either: '
317          '(1) Flashing to a userdebug build OR '
318          '(2) Manually enabling web debugging in Chrome at '
319          'Settings > Developer tools > Enable USB Web debugging.')
320      sys.exit(1)
321    except:
322      import traceback
323      traceback.print_exc()
324      self.Close()
325      raise
326    finally:
327      self._RestoreCommandLine()
328
329  def GetBrowserStartupArgs(self):
330    args = super(AndroidBrowserBackend, self).GetBrowserStartupArgs()
331    if self.forwarder_factory.does_forwarder_override_dns:
332      args = [arg for arg in args
333              if not arg.startswith('--host-resolver-rules')]
334    args.append('--enable-remote-debugging')
335    args.append('--disable-fre')
336    args.append('--disable-external-intent-requests')
337    return args
338
339  @property
340  def forwarder_factory(self):
341    return self._forwarder_factory
342
343  @property
344  def adb(self):
345    return self._adb
346
347  @property
348  def pid(self):
349    pids = self._adb.ExtractPid(self._backend_settings.package)
350    if not pids:
351      raise exceptions.BrowserGoneException(self.browser)
352    return int(pids[0])
353
354  @property
355  def browser_directory(self):
356    return None
357
358  @property
359  def profile_directory(self):
360    return self._backend_settings.profile_dir
361
362  @property
363  def package(self):
364    return self._backend_settings.package
365
366  @property
367  def activity(self):
368    return self._backend_settings.activity
369
370  def __del__(self):
371    self.Close()
372
373  def Close(self):
374    super(AndroidBrowserBackend, self).Close()
375    self._KillBrowser()
376
377    # Restore android.net SSL check
378    if self._backend_settings.relax_ssl_check:
379      self._adb.device().SetProp('socket.relaxsslcheck', self._saved_sslflag)
380
381    if self._output_profile_path:
382      logging.info("Pulling profile directory from device: '%s'->'%s'.",
383                   self._backend_settings.profile_dir,
384                   self._output_profile_path)
385      # To minimize bandwidth it might be good to look at whether all the data
386      # pulled down is really needed e.g. .pak files.
387      if not os.path.exists(self._output_profile_path):
388        os.makedirs(self._output_profile_pathame)
389      files = self.adb.device().RunShellCommand(
390          'ls "%s"' % self._backend_settings.profile_dir)
391      for f in files:
392        # Don't pull lib, since it is created by the installer.
393        if f != 'lib':
394          source = '%s%s' % (self._backend_settings.profile_dir, f)
395          dest = os.path.join(self._output_profile_path, f)
396          # self._adb.Pull(source, dest) doesn't work because its timeout
397          # is fixed in android's adb_interface at 60 seconds, which may
398          # be too short to pull the cache.
399          cmd = 'pull %s %s' % (source, dest)
400          self._adb.device().old_interface.Adb().SendCommand(
401              cmd, timeout_time=240)
402
403  def IsBrowserRunning(self):
404    pids = self._adb.ExtractPid(self._backend_settings.package)
405    return len(pids) != 0
406
407  def GetRemotePort(self, local_port):
408    return local_port
409
410  def GetStandardOutput(self):
411    return '\n'.join(self._adb.device().RunShellCommand('logcat -d -t 500'))
412
413  def GetStackTrace(self):
414    def Decorate(title, content):
415      return title + '\n' + content + '\n' + '*' * 80 + '\n'
416    # Get the last lines of logcat (large enough to contain stacktrace)
417    logcat = self.GetStandardOutput()
418    ret = Decorate('Logcat', logcat)
419    stack = os.path.join(util.GetChromiumSrcDir(), 'third_party',
420                         'android_platform', 'development', 'scripts', 'stack')
421    # Try to symbolize logcat.
422    if os.path.exists(stack):
423      cmd = [stack]
424      if self._target_arch:
425        cmd.append('--arch=%s' % self._target_arch)
426      p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
427      ret += Decorate('Stack from Logcat', p.communicate(input=logcat)[0])
428
429    # Try to get tombstones.
430    tombstones = os.path.join(util.GetChromiumSrcDir(), 'build', 'android',
431                              'tombstones.py')
432    if os.path.exists(tombstones):
433      ret += Decorate('Tombstones',
434                      subprocess.Popen([tombstones, '-w', '--device',
435                                        self._adb.device_serial()],
436                                       stdout=subprocess.PIPE).communicate()[0])
437    return ret
438
439  def AddReplayServerOptions(self, extra_wpr_args):
440    if not self.forwarder_factory.does_forwarder_override_dns:
441      extra_wpr_args.append('--no-dns_forwarding')
442    if self.browser_options.netsim:
443      extra_wpr_args.append('--net=%s' % self.browser_options.netsim)
444