blob: 94433f74fb1390581fbd3e57cdce3d4524894e42 [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.
"""Finds android browsers that can be started and controlled by telemetry."""
import contextlib
import logging
import os
import shutil
import subprocess
import sys
from devil import base_error
from devil.android import apk_helper
from devil.android import flag_changer
from py_utils import dependency_util
from py_utils import file_util
from py_utils import tempfile_ext
from telemetry import compact_mode_options
from telemetry import decorators
from telemetry.core import exceptions
from telemetry.core import platform
from telemetry.internal.backends import android_browser_backend_settings
from telemetry.internal.backends.chrome import android_browser_backend
from telemetry.internal.backends.chrome import chrome_startup_args
from telemetry.internal.backends.chrome import gpu_compositing_checker
from telemetry.internal.browser import browser
from telemetry.internal.browser import possible_browser
from telemetry.internal.platform import android_device
from telemetry.internal.util import binary_manager
ANDROID_BACKEND_SETTINGS = (
android_browser_backend_settings.ANDROID_BACKEND_SETTINGS)
@contextlib.contextmanager
def _ProfileWithExtraFiles(profile_dir, profile_files_to_copy):
"""Yields a temporary directory populated with input files.
Args:
profile_dir: A directory whose contents will be copied to the output
directory.
profile_files_to_copy: A list of (source, dest) tuples to be copied to
the output directory.
Yields: A path to a temporary directory, named "_default_profile". This
directory will be cleaned up when this context exits.
"""
with tempfile_ext.NamedTemporaryDirectory() as tempdir:
# TODO(csharrison): "_default_profile" was chosen because this directory
# will be pushed to the device's sdcard. We don't want to choose a
# random name due to the extra failure mode of filling up the sdcard
# in the case of unclean test teardown. We should consider changing
# PushProfile to avoid writing to this intermediate location.
host_profile = os.path.join(tempdir, '_default_profile')
if profile_dir:
shutil.copytree(profile_dir, host_profile)
else:
os.mkdir(host_profile)
# Add files from |profile_files_to_copy| into the host profile
# directory. Don't copy files if they already exist.
for source, dest in profile_files_to_copy:
host_path = os.path.join(host_profile, dest)
if not os.path.exists(host_path):
file_util.CopyFileWithIntermediateDirectories(source, host_path)
yield host_profile
class PossibleAndroidBrowser(possible_browser.PossibleBrowser):
"""A launchable android browser instance."""
def __init__(self, browser_type, finder_options, android_platform,
backend_settings, local_apk=None):
super(PossibleAndroidBrowser, self).__init__(
browser_type, 'android', backend_settings.supports_tab_control)
assert browser_type in FindAllBrowserTypes(finder_options), (
'Please add %s to android_browser_finder.FindAllBrowserTypes' %
browser_type)
self._platform = android_platform
self._platform_backend = (
android_platform._platform_backend) # pylint: disable=protected-access
self._backend_settings = backend_settings
self._local_apk = local_apk
self._flag_changer = None
if self._local_apk is None and finder_options.chrome_root is not None:
self._local_apk = self._backend_settings.FindLocalApk(
self._platform_backend.device, finder_options.chrome_root)
# At this point the local_apk, if any, must exist.
assert self._local_apk is None or os.path.exists(self._local_apk)
self._embedder_apk = None
if self._backend_settings.requires_embedder:
if finder_options.webview_embedder_apk:
self._embedder_apk = finder_options.webview_embedder_apk
else:
self._embedder_apk = self._backend_settings.FindEmbedderApk(
self._local_apk, finder_options.chrome_root)
elif finder_options.webview_embedder_apk:
logging.warning(
'No embedder needed for %s, ignoring --webview-embedder-apk option',
self._backend_settings.browser_type)
# At this point the embedder_apk, if any, must exist.
assert self._embedder_apk is None or os.path.exists(self._embedder_apk)
def __repr__(self):
return 'PossibleAndroidBrowser(browser_type=%s)' % self.browser_type
@property
def settings(self):
"""Get the backend_settings for this possible browser."""
return self._backend_settings
@property
def browser_directory(self):
# On Android L+ the directory where base APK resides is also used for
# keeping extracted native libraries and .odex. Here is an example layout:
# /data/app/$package.apps.chrome-1/
# base.apk
# lib/arm/libchrome.so
# oat/arm/base.odex
# Declaring this toplevel directory as 'browser_directory' allows the cold
# startup benchmarks to flush OS pagecache for the native library, .odex and
# the APK.
apks = self._platform_backend.device.GetApplicationPaths(
self._backend_settings.package)
# A package can map to multiple APKs iff the package overrides the app on
# the system image. Such overrides should not happen on perf bots.
assert len(apks) == 1
base_apk = apks[0]
if not base_apk or not base_apk.endswith('/base.apk'):
return None
return base_apk[:-9]
@property
def profile_directory(self):
return self._platform_backend.GetProfileDir(self._backend_settings.package)
@property
def last_modification_time(self):
if self._local_apk:
return os.path.getmtime(self._local_apk)
return -1
def _GetPathsForOsPageCacheFlushing(self):
paths_to_flush = [self.profile_directory]
# On N+ the Monochrome is the most widely used configuration. Since Webview
# is used often, the typical usage is closer to have the DEX and the native
# library be resident in memory. Skip the pagecache flushing for browser
# directory on N+.
if self._platform_backend.device.build_version_sdk < 24:
paths_to_flush.append(self.browser_directory)
return paths_to_flush
def _InitPlatformIfNeeded(self):
pass
def _SetupProfile(self):
if self._browser_options.dont_override_profile:
return
# Just remove the existing profile if we don't have any files to copy over.
# This is because PushProfile does not support pushing completely empty
# directories.
profile_files_to_copy = self._browser_options.profile_files_to_copy
if not self._browser_options.profile_dir and not profile_files_to_copy:
self._platform_backend.RemoveProfile(
self._backend_settings.package,
self._backend_settings.profile_ignore_list)
return
with _ProfileWithExtraFiles(self._browser_options.profile_dir,
profile_files_to_copy) as profile_dir:
self._platform_backend.PushProfile(self._backend_settings.package,
profile_dir)
def SetUpEnvironment(self, browser_options):
super(PossibleAndroidBrowser, self).SetUpEnvironment(browser_options)
self._platform_backend.DismissCrashDialogIfNeeded()
device = self._platform_backend.device
startup_args = self.GetBrowserStartupArgs(self._browser_options)
device.adb.Logcat(clear=True)
# use legacy commandline path if in compatibility mode
self._flag_changer = flag_changer.FlagChanger(
device, self._backend_settings.command_line_name, use_legacy_path=
compact_mode_options.LEGACY_COMMAND_LINE_PATH in
browser_options.compatibility_mode)
self._flag_changer.ReplaceFlags(startup_args)
# Stop any existing browser found already running on the device. This is
# done *after* setting the command line flags, in case some other Android
# process manages to trigger Chrome's startup before we do.
self._platform_backend.StopApplication(self._backend_settings.package)
self._SetupProfile()
def _TearDownEnvironment(self):
self._RestoreCommandLineFlags()
def _RestoreCommandLineFlags(self):
if self._flag_changer is not None:
try:
self._flag_changer.Restore()
finally:
self._flag_changer = None
def _GetBrowserStartupUrl(self):
if self._browser_options.startup_url:
return self._browser_options.startup_url
elif self._browser_options.profile_dir:
return 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
return 'about:blank'
def Create(self, clear_caches=True):
"""Launch the browser on the device and return a Browser object."""
return self._GetBrowserInstance(
existing=False, clear_caches=clear_caches,
startup_url=self._GetBrowserStartupUrl())
def FindExistingBrowser(self):
"""Find a browser running on the device and bind a Browser object to it.
The returned Browser object will only be bound to a running browser
instance whose package name matches the one specified by the backend
settings of this possible browser.
A BrowserGoneException is raised if the browser cannot be found.
"""
return self._GetBrowserInstance(existing=True, clear_caches=False)
def _GetBrowserInstance(self, existing, clear_caches, startup_url=None):
browser_backend = android_browser_backend.AndroidBrowserBackend(
self._platform_backend, self._browser_options,
self.browser_directory, self.profile_directory,
self._backend_settings)
# TODO(crbug.com/811244): Remove when this is handled by shared state.
if clear_caches:
self._ClearCachesOnStart()
try:
returned_browser = browser.Browser(
browser_backend, self._platform_backend, startup_args=(),
startup_url=startup_url, find_existing=existing)
# TODO(crbug.com/916086): Move this assertion to callers.
if self._browser_options.assert_gpu_compositing:
gpu_compositing_checker.AssertGpuCompositingEnabled(
returned_browser.GetSystemInfo())
return returned_browser
except Exception:
exc_info = sys.exc_info()
logging.error(
'Failed with %s while creating Android browser.',
exc_info[0].__name__)
try:
browser_backend.Close()
except Exception: # pylint: disable=broad-except
logging.exception('Secondary failure while closing browser backend.')
raise exc_info[0], exc_info[1], exc_info[2]
def GetBrowserStartupArgs(self, browser_options):
startup_args = chrome_startup_args.GetFromBrowserOptions(browser_options)
# use the flag `--ignore-certificate-errors` if in compatibility mode
supports_spki_list = (
self._backend_settings.supports_spki_list and
compact_mode_options.IGNORE_CERTIFICATE_ERROR
not in browser_options.compatibility_mode)
startup_args.extend(chrome_startup_args.GetReplayArgs(
self._platform_backend.network_controller_backend,
supports_spki_list=supports_spki_list))
startup_args.append('--enable-remote-debugging')
startup_args.append('--disable-fre')
startup_args.append('--disable-external-intent-requests')
# Need to specify the user profile directory for
# --ignore-certificate-errors-spki-list to work.
startup_args.append('--user-data-dir=' + self.profile_directory)
return startup_args
def SupportsOptions(self, browser_options):
if len(browser_options.extensions_to_load) != 0:
return False
return True
def IsAvailable(self):
"""Returns True if the browser is or can be installed on the platform."""
has_local_apks = self._local_apk and (
not self._backend_settings.requires_embedder or self._embedder_apk)
return has_local_apks or self.platform.CanLaunchApplication(
self.settings.package)
@decorators.Cache
def UpdateExecutableIfNeeded(self):
# TODO(crbug.com/815133): This logic should belong to backend_settings.
if self._local_apk:
logging.warn('Installing %s on device if needed.', self._local_apk)
self.platform.InstallApplication(self._local_apk)
if self._embedder_apk:
logging.warn('Installing %s on device if needed.', self._embedder_apk)
self.platform.InstallApplication(self._embedder_apk)
if (self._backend_settings.GetApkName(
self._platform_backend.device) == 'Monochrome.apk'):
self._platform_backend.device.SetWebViewImplementation(
android_browser_backend_settings.ANDROID_CHROME.package)
def SelectDefaultBrowser(possible_browsers):
"""Return the newest possible browser."""
if not possible_browsers:
return None
return max(possible_browsers, key=lambda b: b.last_modification_time)
def CanFindAvailableBrowsers():
return android_device.CanDiscoverDevices()
def _CanPossiblyHandlePath(apk_path):
return apk_path and apk_path[-4:].lower() == '.apk'
def FindAllBrowserTypes(options):
del options # unused
browser_types = [b.browser_type for b in ANDROID_BACKEND_SETTINGS]
return browser_types + ['exact', 'reference']
def _FindAllPossibleBrowsers(finder_options, android_platform):
"""Testable version of FindAllAvailableBrowsers."""
if not android_platform:
return []
possible_browsers = []
if finder_options.webview_embedder_apk and not os.path.exists(
finder_options.webview_embedder_apk):
raise exceptions.PathMissingError(
'Unable to find apk specified by --webview-embedder-apk=%s' %
finder_options.browser_executable)
# Add the exact APK if given.
if _CanPossiblyHandlePath(finder_options.browser_executable):
if not os.path.exists(finder_options.browser_executable):
raise exceptions.PathMissingError(
'Unable to find exact apk specified by --browser-executable=%s' %
finder_options.browser_executable)
package_name = apk_helper.GetPackageName(finder_options.browser_executable)
try:
backend_settings = next(
b for b in ANDROID_BACKEND_SETTINGS if b.package == package_name)
except StopIteration:
raise exceptions.UnknownPackageError(
'%s specified by --browser-executable has an unknown package: %s' %
(finder_options.browser_executable, package_name))
possible_browsers.append(PossibleAndroidBrowser(
'exact',
finder_options,
android_platform,
backend_settings,
finder_options.browser_executable))
# Add the reference build if found.
os_version = dependency_util.GetChromeApkOsVersion(
android_platform.GetOSVersionName())
arch = android_platform.GetArchName()
try:
reference_build = binary_manager.FetchPath(
'chrome_stable', arch, 'android', os_version)
except (binary_manager.NoPathFoundError,
binary_manager.CloudStorageError):
reference_build = None
if reference_build and os.path.exists(reference_build):
# TODO(aiolos): how do we stably map the android chrome_stable apk to the
# correct backend settings?
possible_browsers.append(PossibleAndroidBrowser(
'reference',
finder_options,
android_platform,
android_browser_backend_settings.ANDROID_CHROME,
reference_build))
# Add any other known available browsers.
for settings in ANDROID_BACKEND_SETTINGS:
p_browser = PossibleAndroidBrowser(
settings.browser_type, finder_options, android_platform, settings)
if p_browser.IsAvailable():
possible_browsers.append(p_browser)
return possible_browsers
def FindAllAvailableBrowsers(finder_options, device):
"""Finds all the possible browsers on one device.
The device is either the only device on the host platform,
or |finder_options| specifies a particular device.
"""
if not isinstance(device, android_device.AndroidDevice):
return []
try:
android_platform = platform.GetPlatformForDevice(device, finder_options)
return _FindAllPossibleBrowsers(finder_options, android_platform)
except base_error.BaseError as e:
logging.error('Unable to find browsers on %s: %s', device.device_id, str(e))
ps_output = subprocess.check_output(['ps', '-ef'])
logging.error('Ongoing processes:\n%s', ps_output)
return []