blob: f286d50a825eb7303f0dde387cbfe051983dd56e [file] [log] [blame]
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import os
import re
import subprocess
import sys
import time
from telemetry.core import exceptions
from telemetry.core import util
from telemetry.core.backends import adb_commands
from telemetry.core.backends import android_rndis
from telemetry.core.backends import browser_backend
from telemetry.core.backends.chrome import chrome_browser_backend
class AndroidBrowserBackendSettings(object):
def __init__(self, adb, activity, cmdline_file, package, pseudo_exec_name,
supports_tab_control):
self.adb = adb
self.activity = activity
self.cmdline_file = cmdline_file
self.package = package
self.pseudo_exec_name = pseudo_exec_name
self.supports_tab_control = supports_tab_control
def GetDevtoolsRemotePort(self):
raise NotImplementedError()
def RemoveProfile(self):
files = self.adb.RunShellCommandWithSU('ls "%s"' % self.profile_dir)
# Don't delete lib, since it is created by the installer.
paths = ['"%s/%s"' % (self.profile_dir, f) for f in files if f != 'lib']
self.adb.RunShellCommandWithSU('rm -r %s' % ' '.join(paths))
def PushProfile(self, _):
logging.critical('Profiles cannot be overriden with current configuration')
sys.exit(1)
@property
def is_content_shell(self):
return False
@property
def profile_dir(self):
return '/data/data/%s/' % self.package
class ChromeBackendSettings(AndroidBrowserBackendSettings):
# Stores a default Preferences file, re-used to speed up "--page-repeat".
_default_preferences_file = None
@staticmethod
def _GetCommandLineFile(adb):
if adb.IsUserBuild():
return '/data/local/tmp/chrome-command-line'
else:
return '/data/local/chrome-command-line'
def __init__(self, adb, package):
super(ChromeBackendSettings, self).__init__(
adb=adb,
activity='com.google.android.apps.chrome.Main',
cmdline_file=ChromeBackendSettings._GetCommandLineFile(adb),
package=package,
pseudo_exec_name='chrome',
supports_tab_control=True)
def GetDevtoolsRemotePort(self):
return 'localabstract:chrome_devtools_remote'
def PushProfile(self, new_profile_dir):
# Clear the old profile first, since copying won't delete files.
self.RemoveProfile()
# Pushing the profile is slow, so we don't want to do it every time.
# Avoid this by pushing to a safe location using PushIfNeeded, and
# then copying into the correct location on each test run.
(profile_parent, profile_base) = os.path.split(new_profile_dir)
# If the path ends with a '/' python split will return an empty string for
# the base name; so we now need to get the base name from the directory.
if not profile_base:
profile_base = os.path.basename(profile_parent)
saved_profile_location = '/sdcard/profile/%s' % profile_base
self.adb.Adb().PushIfNeeded(new_profile_dir, saved_profile_location)
self.adb.RunShellCommand('cp -r %s/* %s' % (saved_profile_location,
self.profile_dir))
# We now need to give the ownership back to the browser UID
dumpsys = self.adb.RunShellCommand('dumpsys package %s' % self.package)
id_line = next(line for line in dumpsys if 'userId=' in line)
uid = re.search('\d+', id_line).group()
files = self.adb.RunShellCommandWithSU('ls "%s"' % self.profile_dir)
files.remove('lib')
paths = ['%s/%s' % (self.profile_dir, f) for f in files]
for path in paths:
extended_path = '%s %s/* %s/*/* %s/*/*/*' % (path, path, path, path)
self.adb.RunShellCommand('chown %s.%s %s' %
(uid, uid, extended_path))
class ContentShellBackendSettings(AndroidBrowserBackendSettings):
def __init__(self, adb, package):
super(ContentShellBackendSettings, self).__init__(
adb=adb,
activity='org.chromium.content_shell_apk.ContentShellActivity',
cmdline_file='/data/local/tmp/content-shell-command-line',
package=package,
pseudo_exec_name='content_shell',
supports_tab_control=False)
def GetDevtoolsRemotePort(self):
return 'localabstract:content_shell_devtools_remote'
@property
def is_content_shell(self):
return True
class ChromiumTestShellBackendSettings(AndroidBrowserBackendSettings):
def __init__(self, adb, package):
super(ChromiumTestShellBackendSettings, self).__init__(
adb=adb,
activity='org.chromium.chrome.testshell.ChromiumTestShellActivity',
cmdline_file='/data/local/tmp/chromium-testshell-command-line',
package=package,
pseudo_exec_name='chromium_testshell',
supports_tab_control=False)
def GetDevtoolsRemotePort(self):
return 'localabstract:chromium_testshell_devtools_remote'
@property
def is_content_shell(self):
return True
class WebviewBackendSettings(AndroidBrowserBackendSettings):
def __init__(self, adb, package):
super(WebviewBackendSettings, self).__init__(
adb=adb,
activity='com.android.webview.chromium.shell.TelemetryActivity',
cmdline_file='/data/local/tmp/webview-command-line',
package=package,
pseudo_exec_name='webview',
supports_tab_control=False)
def GetDevtoolsRemotePort(self):
# The DevTools socket name for WebView depends on the activity PID's.
retries = 0
timeout = 1
pid = None
while True:
pids = self.adb.ExtractPid(self.package)
if (len(pids) > 0):
pid = pids[-1]
break
time.sleep(timeout)
retries += 1
timeout *= 2
if retries == 4:
logging.critical('android_browser_backend: Timeout while waiting for '
'activity %s:%s to come up',
self.package,
self.activity)
raise exceptions.BrowserGoneException('Timeout waiting for PID.')
return 'localabstract:webview_devtools_remote_%s' % str(pid)
class AndroidBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
"""The backend for controlling a browser instance running on Android.
"""
def __init__(self, browser_options, backend_settings, rndis,
output_profile_path, extensions_to_load):
super(AndroidBrowserBackend, self).__init__(
is_content_shell=backend_settings.is_content_shell,
supports_extensions=False, browser_options=browser_options,
output_profile_path=output_profile_path,
extensions_to_load=extensions_to_load)
if len(extensions_to_load) > 0:
raise browser_backend.ExtensionsNotSupportedException(
'Android browser does not support extensions.')
# Initialize fields so that an explosion during init doesn't break in Close.
self._adb = backend_settings.adb
self._backend_settings = backend_settings
self._saved_cmdline = None
self._port = adb_commands.AllocateTestServerPort()
# Kill old browser.
self._adb.CloseApplication(self._backend_settings.package)
if self._adb.Adb().CanAccessProtectedFileContents():
if not self.browser_options.dont_override_profile:
self._backend_settings.RemoveProfile()
if self.browser_options.profile_dir:
self._backend_settings.PushProfile(self.browser_options.profile_dir)
# Pre-configure RNDIS forwarding.
self._rndis_forwarder = None
if rndis or self.browser_options.netsim:
self._rndis_forwarder = android_rndis.RndisForwarderWithRoot(self._adb)
self.WEBPAGEREPLAY_HOST = self._rndis_forwarder.host_ip
# TODO(szym): only override DNS if WPR has privileges to proxy on port 25.
self._override_dns = self.browser_options.netsim
def _SetUpCommandLine(self):
def QuoteIfNeeded(arg):
# Escape 'key=valueA valueB' to 'key="valueA valueB"'
# Already quoted values, or values without space are left untouched.
# This is required so CommandLine.java can parse valueB correctly rather
# than as a separate switch.
params = arg.split('=', 1)
if len(params) != 2:
return arg
key, values = params
if ' ' not in values:
return arg
if values[0] in '"\'' and values[-1] == values[0]:
return arg
return '%s="%s"' % (key, values)
args = [self._backend_settings.pseudo_exec_name]
args.extend(self.GetBrowserStartupArgs())
args = ' '.join(map(QuoteIfNeeded, args))
self._SetCommandLineFile(args)
def _SetCommandLineFile(self, file_contents):
logging.debug('Using command line: ' + file_contents)
def IsProtectedFile(name):
if self._adb.Adb().FileExistsOnDevice(name):
return not self._adb.Adb().IsFileWritableOnDevice(name)
else:
parent_name = os.path.dirname(name)
if parent_name != '':
return IsProtectedFile(parent_name)
else:
return True
if IsProtectedFile(self._backend_settings.cmdline_file):
if not self._adb.Adb().CanAccessProtectedFileContents():
logging.critical('Cannot set Chrome command line. '
'Fix this by flashing to a userdebug build.')
sys.exit(1)
self._saved_cmdline = ''.join(self._adb.Adb().GetProtectedFileContents(
self._backend_settings.cmdline_file) or [])
self._adb.Adb().SetProtectedFileContents(
self._backend_settings.cmdline_file, file_contents)
else:
self._saved_cmdline = ''.join(self._adb.Adb().GetFileContents(
self._backend_settings.cmdline_file) or [])
self._adb.Adb().SetFileContents(self._backend_settings.cmdline_file,
file_contents)
def Start(self):
self._SetUpCommandLine()
self._adb.RunShellCommand('logcat -c')
if self.browser_options.startup_url:
url = self.browser_options.startup_url
elif self.browser_options.profile_dir:
url = None
else:
# If we have no existing tabs start with a blank page since default
# startup with the NTP can lead to race conditions with Telemetry
url = 'about:blank'
self._adb.StartActivity(self._backend_settings.package,
self._backend_settings.activity,
True,
None,
None,
url)
self._adb.Forward('tcp:%d' % self._port,
self._backend_settings.GetDevtoolsRemotePort())
try:
self._WaitForBrowserToComeUp()
self._PostBrowserStartupInitialization()
except exceptions.BrowserGoneException:
logging.critical('Failed to connect to browser.')
if not self._adb.Adb().CanAccessProtectedFileContents():
logging.critical(
'Resolve this by either: '
'(1) Flashing to a userdebug build OR '
'(2) Manually enabling web debugging in Chrome at '
'Settings > Developer tools > Enable USB Web debugging.')
sys.exit(1)
except:
import traceback
traceback.print_exc()
self.Close()
raise
finally:
self._SetCommandLineFile(self._saved_cmdline or '')
def GetBrowserStartupArgs(self):
args = super(AndroidBrowserBackend, self).GetBrowserStartupArgs()
if self._override_dns:
args = [arg for arg in args
if not arg.startswith('--host-resolver-rules')]
args.append('--enable-remote-debugging')
args.append('--disable-fre')
args.append('--disable-external-intent-requests')
return args
@property
def adb(self):
return self._adb
@property
def pid(self):
pids = self._adb.ExtractPid(self._backend_settings.package)
if not pids:
raise exceptions.BrowserGoneException(self.GetStackTrace())
return int(pids[0])
@property
def browser_directory(self):
return None
@property
def profile_directory(self):
return self._backend_settings.profile_dir
@property
def package(self):
return self._backend_settings.package
@property
def activity(self):
return self._backend_settings.activity
@property
def supports_tab_control(self):
return self._backend_settings.supports_tab_control
def __del__(self):
self.Close()
def Close(self):
super(AndroidBrowserBackend, self).Close()
self._adb.CloseApplication(self._backend_settings.package)
if self._output_profile_path:
logging.info("Pulling profile directory from device: '%s'->'%s'.",
self._backend_settings.profile_dir,
self._output_profile_path)
# To minimize bandwidth it might be good to look at whether all the data
# pulled down is really needed e.g. .pak files.
if not os.path.exists(self._output_profile_path):
os.makedirs(self._output_profile_pathame)
files = self.adb.RunShellCommandWithSU(
'ls "%s"' % self._backend_settings.profile_dir)
for f in files:
# Don't pull lib, since it is created by the installer.
if f != 'lib':
source = '%s%s' % (self._backend_settings.profile_dir, f)
dest = os.path.join(self._output_profile_path, f)
# self._adb.Pull(source, dest) doesn't work because its timeout
# is fixed in android's adb_interface at 60 seconds, which may
# be too short to pull the cache.
cmd = 'pull %s %s' % (source, dest)
self._adb.Adb().Adb().SendCommand(cmd, timeout_time=240)
def IsBrowserRunning(self):
pids = self._adb.ExtractPid(self._backend_settings.package)
return len(pids) != 0
def GetRemotePort(self, local_port):
return local_port
def GetStandardOutput(self):
return '\n'.join(self._adb.RunShellCommand('logcat -d -t 500'))
def GetStackTrace(self):
def Decorate(title, content):
return title + '\n' + content + '\n' + '*' * 80 + '\n'
# Get the last lines of logcat (large enough to contain stacktrace)
logcat = self.GetStandardOutput()
ret = Decorate('Logcat', logcat)
stack = os.path.join(util.GetChromiumSrcDir(), 'third_party',
'android_platform', 'development', 'scripts', 'stack')
# Try to symbolize logcat.
if os.path.exists(stack):
p = subprocess.Popen([stack], stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
ret += Decorate('Stack from Logcat', p.communicate(input=logcat)[0])
# Try to get tombstones.
tombstones = os.path.join(util.GetChromiumSrcDir(), 'build', 'android',
'tombstones.py')
if os.path.exists(tombstones):
ret += Decorate('Tombstones',
subprocess.Popen([tombstones, '-w', '--device',
self._adb.device()],
stdout=subprocess.PIPE).communicate()[0])
return ret
def AddReplayServerOptions(self, extra_wpr_args):
"""Override. Only add --no-dns_forwarding if not using RNDIS."""
if not self._override_dns:
extra_wpr_args.append('--no-dns_forwarding')
if self.browser_options.netsim:
extra_wpr_args.append('--net=%s' % self.browser_options.netsim)
def CreateForwarder(self, *port_pairs):
if self._rndis_forwarder:
forwarder = self._rndis_forwarder
forwarder.SetPorts(*port_pairs)
assert self.WEBPAGEREPLAY_HOST == forwarder.host_ip, (
'Host IP address on the RNDIS interface changed. Must restart browser!')
if self._override_dns:
forwarder.OverrideDns()
return forwarder
assert not self._override_dns, ('The user-space forwarder does not support '
'DNS override!')
logging.warning('Using the user-space forwarder.\n')
return adb_commands.Forwarder(self._adb, *port_pairs)