blob: f5fc1e508300965a0220176d1dc98d43f5440133 [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 desktop browsers that can be started and controlled by telemetry."""
import logging
import os
import shutil
import sys
import tempfile
import dependency_manager # pylint: disable=import-error
from py_utils import file_util
from telemetry.core import exceptions
from telemetry.core import platform as platform_module
from telemetry.internal.backends.chrome import chrome_startup_args
from telemetry.internal.backends.chrome import desktop_browser_backend
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 desktop_device
from telemetry.internal.util import binary_manager
# This is a workaround for https://goo.gl/1tGNgd
from telemetry.internal.util import path as path_module
class PossibleDesktopBrowser(possible_browser.PossibleBrowser):
"""A desktop browser that can be controlled."""
def __init__(self, browser_type, finder_options, executable, flash_path,
is_content_shell, browser_directory, is_local_build=False):
target_os = sys.platform.lower()
super(PossibleDesktopBrowser, self).__init__(
browser_type, target_os, not is_content_shell)
assert browser_type in FindAllBrowserTypes(finder_options), (
'Please add %s to desktop_browser_finder.FindAllBrowserTypes' %
browser_type)
self._local_executable = executable
self._flash_path = flash_path
self._is_content_shell = is_content_shell
self._browser_directory = browser_directory
self._profile_directory = None
self._extra_browser_args = set()
self.is_local_build = is_local_build
def __repr__(self):
return 'PossibleDesktopBrowser(type=%s, executable=%s, flash=%s)' % (
self.browser_type, self._local_executable, self._flash_path)
@property
def browser_directory(self):
return self._browser_directory
@property
def profile_directory(self):
return self._profile_directory
@property
def last_modification_time(self):
if os.path.exists(self._local_executable):
return os.path.getmtime(self._local_executable)
return -1
@property
def extra_browser_args(self):
return list(self._extra_browser_args)
def AddExtraBrowserArg(self, arg):
self._extra_browser_args.add(arg)
def _InitPlatformIfNeeded(self):
if self._platform:
return
self._platform = platform_module.GetHostPlatform()
# pylint: disable=protected-access
self._platform_backend = self._platform._platform_backend
def _GetPathsForOsPageCacheFlushing(self):
return [self.profile_directory, self.browser_directory]
def SetUpEnvironment(self, browser_options):
super(PossibleDesktopBrowser, self).SetUpEnvironment(browser_options)
if self._browser_options.dont_override_profile:
return
# If given, this directory's contents will be used to seed the profile.
source_profile = self._browser_options.profile_dir
if source_profile and self._is_content_shell:
raise RuntimeError('Profiles cannot be used with content shell')
self._profile_directory = tempfile.mkdtemp()
if source_profile:
logging.info('Seeding profile directory from: %s', source_profile)
# copytree requires the directory to not exist, so just delete the empty
# directory and re-create it.
os.rmdir(self._profile_directory)
shutil.copytree(source_profile, self._profile_directory)
# When using an existing profile directory, we need to make sure to
# delete the file containing the active DevTools port number.
devtools_file_path = os.path.join(
self._profile_directory,
desktop_browser_backend.DEVTOOLS_ACTIVE_PORT_FILE)
if os.path.isfile(devtools_file_path):
os.remove(devtools_file_path)
# Copy data into the profile if it hasn't already been added via
# |source_profile|.
for source, dest in self._browser_options.profile_files_to_copy:
full_dest_path = os.path.join(self._profile_directory, dest)
if not os.path.exists(full_dest_path):
file_util.CopyFileWithIntermediateDirectories(source, full_dest_path)
def _TearDownEnvironment(self):
if self._profile_directory and os.path.exists(self._profile_directory):
# Remove the profile directory, which was hosted on a temp dir.
shutil.rmtree(self._profile_directory, ignore_errors=True)
self._profile_directory = None
def Create(self, clear_caches=True):
if self._flash_path and not os.path.exists(self._flash_path):
logging.warning(
'Could not find Flash at %s. Continuing without Flash.\n'
'To run with Flash, check it out via http://go/read-src-internal',
self._flash_path)
self._flash_path = None
self._InitPlatformIfNeeded()
num_retries = 3
for x in range(0, num_retries):
returned_browser = None
try:
# Note: we need to regenerate the browser startup arguments for each
# browser startup attempt since the state of the startup arguments
# may not be guaranteed the same each time
# For example, see: crbug.com/865895#c17
startup_args = self.GetBrowserStartupArgs(self._browser_options)
startup_url = self._browser_options.startup_url
returned_browser = None
browser_backend = desktop_browser_backend.DesktopBrowserBackend(
self._platform_backend, self._browser_options,
self._browser_directory, self._profile_directory,
self._local_executable, self._flash_path, self._is_content_shell)
# TODO(crbug.com/811244): Remove when this is handled by shared state.
if clear_caches:
self._ClearCachesOnStart()
returned_browser = browser.Browser(
browser_backend, self._platform_backend, startup_args,
startup_url=startup_url)
# 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
# Do not retry if gpu assertion failure is raised.
except gpu_compositing_checker.GpuCompositingAssertionFailure:
raise
except Exception: # pylint: disable=broad-except
report = 'Browser creation failed (attempt %d of %d)' % (
(x + 1), num_retries)
if x < num_retries - 1:
report += ', retrying'
logging.warning(report)
# Attempt to clean up things left over from the failed browser startup.
try:
if returned_browser:
returned_browser.DumpStateUponFailure()
returned_browser.Close()
except Exception: # pylint: disable=broad-except
pass
# Re-raise the exception the last time through.
if x == num_retries - 1:
raise
def GetBrowserStartupArgs(self, browser_options):
startup_args = chrome_startup_args.GetFromBrowserOptions(browser_options)
startup_args.extend(chrome_startup_args.GetReplayArgs(
self._platform_backend.network_controller_backend))
# Setting port=0 allows the browser to choose a suitable port.
startup_args.append('--remote-debugging-port=0')
startup_args.append('--enable-crash-reporter-for-testing')
startup_args.append('--disable-component-update')
if not self._is_content_shell:
startup_args.append('--window-size=1280,1024')
if self._flash_path:
startup_args.append('--ppapi-flash-path=%s' % self._flash_path)
# Also specify the version of Flash as a large version, so that it is
# not overridden by the bundled or component-updated version of Flash.
startup_args.append('--ppapi-flash-version=99.9.999.999')
if self.profile_directory is not None:
if self._is_content_shell:
startup_args.append('--data-path=%s' % self.profile_directory)
else:
startup_args.append('--user-data-dir=%s' % self.profile_directory)
trace_config_file = (self._platform_backend.tracing_controller_backend
.GetChromeTraceConfigFile())
if trace_config_file:
startup_args.append('--trace-config-file=%s' % trace_config_file)
startup_args.extend(
[a for a in self.extra_browser_args if a not in startup_args])
return startup_args
def SupportsOptions(self, browser_options):
if ((len(browser_options.extensions_to_load) != 0)
and self._is_content_shell):
return False
return True
def UpdateExecutableIfNeeded(self):
pass
def SelectDefaultBrowser(possible_browsers):
local_builds_by_date = [
b for b in sorted(possible_browsers,
key=lambda b: b.last_modification_time)
if b.is_local_build]
if local_builds_by_date:
return local_builds_by_date[-1]
return None
def CanFindAvailableBrowsers():
return not platform_module.GetHostPlatform().GetOSName() == 'chromeos'
def FindAllBrowserTypes(_):
return [
'exact',
'reference',
'release',
'release_x64',
'debug',
'debug_x64',
'default',
'stable',
'beta',
'dev',
'canary',
'content-shell-debug',
'content-shell-debug_x64',
'content-shell-release',
'content-shell-release_x64',
'content-shell-default',
'system']
def FindAllAvailableBrowsers(finder_options, device):
"""Finds all the desktop browsers available on this machine."""
if not isinstance(device, desktop_device.DesktopDevice):
return []
browsers = []
if not CanFindAvailableBrowsers():
return []
has_x11_display = True
if sys.platform.startswith('linux') and os.getenv('DISPLAY') is None:
has_x11_display = False
os_name = platform_module.GetHostPlatform().GetOSName()
arch_name = platform_module.GetHostPlatform().GetArchName()
try:
flash_path = binary_manager.LocalPath('flash', arch_name, os_name)
except dependency_manager.NoPathFoundError:
flash_path = None
logging.warning(
'Chrome build location for %s_%s not found. Browser will be run '
'without Flash.', os_name, arch_name)
chromium_app_names = []
if sys.platform == 'darwin':
chromium_app_names.append('Chromium.app/Contents/MacOS/Chromium')
chromium_app_names.append('Google Chrome.app/Contents/MacOS/Google Chrome')
content_shell_app_name = 'Content Shell.app/Contents/MacOS/Content Shell'
elif sys.platform.startswith('linux'):
chromium_app_names.append('chrome')
content_shell_app_name = 'content_shell'
elif sys.platform.startswith('win'):
chromium_app_names.append('chrome.exe')
content_shell_app_name = 'content_shell.exe'
else:
raise Exception('Platform not recognized')
# Add the explicit browser executable if given and we can handle it.
if finder_options.browser_executable:
is_content_shell = finder_options.browser_executable.endswith(
content_shell_app_name)
# It is okay if the executable name doesn't match any of known chrome
# browser executables, since it may be of a different browser.
normalized_executable = os.path.expanduser(
finder_options.browser_executable)
if path_module.IsExecutable(normalized_executable):
browser_directory = os.path.dirname(finder_options.browser_executable)
browsers.append(PossibleDesktopBrowser(
'exact', finder_options, normalized_executable, flash_path,
is_content_shell,
browser_directory))
else:
raise exceptions.PathMissingError(
'%s specified by --browser-executable does not exist or is not '
'executable' %
normalized_executable)
def AddIfFound(browser_type, build_path, app_name, content_shell):
app = os.path.join(build_path, app_name)
if path_module.IsExecutable(app):
browsers.append(PossibleDesktopBrowser(
browser_type, finder_options, app, flash_path,
content_shell, build_path, is_local_build=True))
return True
return False
# Add local builds
for build_path in path_module.GetBuildDirectories(finder_options.chrome_root):
# TODO(agrieve): Extract browser_type from args.gn's is_debug.
browser_type = os.path.basename(build_path).lower()
for chromium_app_name in chromium_app_names:
AddIfFound(browser_type, build_path, chromium_app_name, False)
AddIfFound('content-shell-' + browser_type, build_path,
content_shell_app_name, True)
reference_build = None
if finder_options.browser_type == 'reference':
# Reference builds are only available in a Chromium checkout. We should not
# raise an error just because they don't exist.
os_name = platform_module.GetHostPlatform().GetOSName()
arch_name = platform_module.GetHostPlatform().GetArchName()
reference_build = binary_manager.FetchPath(
'chrome_stable', arch_name, os_name)
# Mac-specific options.
if sys.platform == 'darwin':
mac_canary_root = '/Applications/Google Chrome Canary.app/'
mac_canary = mac_canary_root + 'Contents/MacOS/Google Chrome Canary'
mac_system_root = '/Applications/Google Chrome.app'
mac_system = mac_system_root + '/Contents/MacOS/Google Chrome'
if path_module.IsExecutable(mac_canary):
browsers.append(PossibleDesktopBrowser('canary', finder_options,
mac_canary, None, False,
mac_canary_root))
if path_module.IsExecutable(mac_system):
browsers.append(PossibleDesktopBrowser('system', finder_options,
mac_system, None, False,
mac_system_root))
if reference_build and path_module.IsExecutable(reference_build):
reference_root = os.path.dirname(os.path.dirname(os.path.dirname(
reference_build)))
browsers.append(PossibleDesktopBrowser('reference', finder_options,
reference_build, None, False,
reference_root))
# Linux specific options.
if sys.platform.startswith('linux'):
versions = {
'system': os.path.split(os.path.realpath('/usr/bin/google-chrome'))[0],
'stable': '/opt/google/chrome',
'beta': '/opt/google/chrome-beta',
'dev': '/opt/google/chrome-unstable'
}
for version, root in versions.iteritems():
browser_path = os.path.join(root, 'chrome')
if path_module.IsExecutable(browser_path):
browsers.append(PossibleDesktopBrowser(version, finder_options,
browser_path, None, False, root))
if reference_build and path_module.IsExecutable(reference_build):
reference_root = os.path.dirname(reference_build)
browsers.append(PossibleDesktopBrowser('reference', finder_options,
reference_build, None, False,
reference_root))
# Win32-specific options.
if sys.platform.startswith('win'):
app_paths = [
('system', os.path.join('Google', 'Chrome', 'Application')),
('canary', os.path.join('Google', 'Chrome SxS', 'Application')),
]
if reference_build:
app_paths.append(
('reference', os.path.dirname(reference_build)))
for browser_name, app_path in app_paths:
for chromium_app_name in chromium_app_names:
full_path = path_module.FindInstalledWindowsApplication(
os.path.join(app_path, chromium_app_name))
if full_path:
browsers.append(PossibleDesktopBrowser(
browser_name, finder_options, full_path,
None, False, os.path.dirname(full_path)))
has_ozone_platform = False
for arg in finder_options.browser_options.extra_browser_args:
if "--ozone-platform" in arg:
has_ozone_platform = True
if len(browsers) and not has_x11_display and not has_ozone_platform:
logging.warning(
'Found (%s), but you do not have a DISPLAY environment set.', ','.join(
[b.browser_type for b in browsers]))
return []
return browsers