blob: 2f467f198797d23122d40eee6fda75742e02afda [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 httplib
import json
import logging
import pprint
import re
import socket
import sys
import urllib2
from telemetry import decorators
from telemetry.core import exceptions
from telemetry.core import forwarders
from telemetry.core import user_agent
from telemetry.core import util
from telemetry.core import web_contents
from telemetry.core import wpr_modes
from telemetry.core import wpr_server
from telemetry.core.backends import browser_backend
from telemetry.core.backends.chrome import extension_backend
from telemetry.core.backends.chrome import system_info_backend
from telemetry.core.backends.chrome import tab_list_backend
from telemetry.core.backends.chrome import tracing_backend
from telemetry.unittest import options_for_unittests
class ChromeBrowserBackend(browser_backend.BrowserBackend):
"""An abstract class for chrome browser backends. Provides basic functionality
once a remote-debugger port has been established."""
# It is OK to have abstract methods. pylint: disable=W0223
def __init__(self, is_content_shell, supports_extensions, browser_options,
output_profile_path, extensions_to_load):
super(ChromeBrowserBackend, self).__init__(
is_content_shell=is_content_shell,
supports_extensions=supports_extensions,
browser_options=browser_options,
tab_list_backend=tab_list_backend.TabListBackend)
self._port = None
self._inspector_protocol_version = 0
self._chrome_branch_number = None
self._tracing_backend = None
self._system_info_backend = None
self._output_profile_path = output_profile_path
self._extensions_to_load = extensions_to_load
if browser_options.netsim:
self.wpr_port_pairs = forwarders.PortPairs(
http=forwarders.PortPair(80, 80),
https=forwarders.PortPair(443, 443),
dns=forwarders.PortPair(53, 53))
else:
self.wpr_port_pairs = forwarders.PortPairs(
http=forwarders.PortPair(0, 0),
https=forwarders.PortPair(0, 0),
dns=None)
if (self.browser_options.dont_override_profile and
not options_for_unittests.AreSet()):
sys.stderr.write('Warning: Not overriding profile. This can cause '
'unexpected effects due to profile-specific settings, '
'such as about:flags settings, cookies, and '
'extensions.\n')
def AddReplayServerOptions(self, extra_wpr_args):
if self.browser_options.netsim:
extra_wpr_args.append('--net=%s' % self.browser_options.netsim)
else:
extra_wpr_args.append('--no-dns_forwarding')
@property
@decorators.Cache
def extension_backend(self):
if not self.supports_extensions:
return None
return extension_backend.ExtensionBackendDict(self)
def GetBrowserStartupArgs(self):
args = []
args.extend(self.browser_options.extra_browser_args)
args.append('--disable-background-networking')
args.append('--enable-net-benchmarking')
args.append('--metrics-recording-only')
args.append('--no-default-browser-check')
args.append('--no-first-run')
# Turn on GPU benchmarking extension for all runs. The only side effect of
# the extension being on is that render stats are tracked. This is believed
# to be effectively free. And, by doing so here, it avoids us having to
# programmatically inspect a pageset's actions in order to determine if it
# might eventually scroll.
args.append('--enable-gpu-benchmarking')
# Set --no-proxy-server to work around some XP issues unless
# some other flag indicates a proxy is needed.
if not '--enable-spdy-proxy-auth' in args:
args.append('--no-proxy-server')
if self.browser_options.netsim:
args.append('--ignore-certificate-errors')
elif self.browser_options.wpr_mode != wpr_modes.WPR_OFF:
args.extend(wpr_server.GetChromeFlags(self.forwarder_factory.host_ip,
self.wpr_port_pairs))
args.extend(user_agent.GetChromeUserAgentArgumentFromType(
self.browser_options.browser_user_agent_type))
extensions = [extension.local_path
for extension in self._extensions_to_load
if not extension.is_component]
extension_str = ','.join(extensions)
if len(extensions) > 0:
args.append('--load-extension=%s' % extension_str)
component_extensions = [extension.local_path
for extension in self._extensions_to_load
if extension.is_component]
component_extension_str = ','.join(component_extensions)
if len(component_extensions) > 0:
args.append('--load-component-extension=%s' % component_extension_str)
if self.browser_options.no_proxy_server:
args.append('--no-proxy-server')
if self.browser_options.disable_component_extensions_with_background_pages:
args.append('--disable-component-extensions-with-background-pages')
return args
def HasBrowserFinishedLaunching(self):
try:
self.Request('')
except (exceptions.BrowserGoneException,
exceptions.BrowserConnectionGoneException):
return False
else:
return True
def _WaitForBrowserToComeUp(self, wait_for_extensions=True):
try:
util.WaitFor(self.HasBrowserFinishedLaunching, timeout=30)
except (util.TimeoutException, exceptions.ProcessGoneException) as e:
raise exceptions.BrowserGoneException(self.GetStackTrace())
def AllExtensionsLoaded():
# Extension pages are loaded from an about:blank page,
# so we need to check that the document URL is the extension
# page in addition to the ready state.
extension_ready_js = """
document.URL.lastIndexOf('chrome-extension://%s/', 0) == 0 &&
(document.readyState == 'complete' ||
document.readyState == 'interactive')
"""
for e in self._extensions_to_load:
if not e.extension_id in self.extension_backend:
return False
extension_object = self.extension_backend[e.extension_id]
try:
res = extension_object.EvaluateJavaScript(
extension_ready_js % e.extension_id)
except exceptions.EvaluateException:
# If the inspected page is not ready, we will get an error
# when we evaluate a JS expression, but we can just keep polling
# until the page is ready (crbug.com/251913).
res = None
# TODO(tengs): We don't have full support for getting the Chrome
# version before launch, so for now we use a generic workaround to
# check for an extension binding bug in old versions of Chrome.
# See crbug.com/263162 for details.
if res and extension_object.EvaluateJavaScript(
'chrome.runtime == null'):
extension_object.Reload()
if not res:
return False
return True
if wait_for_extensions and self._supports_extensions:
try:
util.WaitFor(AllExtensionsLoaded, timeout=60)
except util.TimeoutException:
logging.error('ExtensionsToLoad: ' +
repr([e.extension_id for e in self._extensions_to_load]))
logging.error('Extension list: ' +
pprint.pformat(self.extension_backend, indent=4))
raise
def _PostBrowserStartupInitialization(self):
# Detect version information.
data = self.Request('version')
resp = json.loads(data)
if 'Protocol-Version' in resp:
self._inspector_protocol_version = resp['Protocol-Version']
if self._chrome_branch_number:
return
if 'Browser' in resp:
branch_number_match = re.search('Chrome/\d+\.\d+\.(\d+)\.\d+',
resp['Browser'])
else:
branch_number_match = re.search(
'Chrome/\d+\.\d+\.(\d+)\.\d+ (Mobile )?Safari',
resp['User-Agent'])
if branch_number_match:
self._chrome_branch_number = int(branch_number_match.group(1))
if not self._chrome_branch_number:
# Content Shell returns '' for Browser, WebViewShell returns '0'.
# For now we have to fall-back and assume branch 1025.
self._chrome_branch_number = 1025
return
# Detection has failed: assume 18.0.1025.168 ~= Chrome Android.
self._inspector_protocol_version = 1.0
self._chrome_branch_number = 1025
def ListInspectableContexts(self):
return json.loads(self.Request(''))
def Request(self, path, timeout=None, throw_network_exception=False):
url = 'http://127.0.0.1:%i/json' % self._port
if path:
url += '/' + path
try:
proxy_handler = urllib2.ProxyHandler({}) # Bypass any system proxy.
opener = urllib2.build_opener(proxy_handler)
req = opener.open(url, timeout=timeout)
return req.read()
except (socket.error, httplib.BadStatusLine, urllib2.URLError) as e:
if throw_network_exception:
raise e
if not self.IsBrowserRunning():
raise exceptions.BrowserGoneException(e)
raise exceptions.BrowserConnectionGoneException(e)
@property
def browser_directory(self):
raise NotImplementedError()
@property
def profile_directory(self):
raise NotImplementedError()
@property
def chrome_branch_number(self):
assert self._chrome_branch_number
return self._chrome_branch_number
@property
def supports_tab_control(self):
return self.chrome_branch_number >= 1303
@property
def supports_tracing(self):
return self.is_content_shell or self.chrome_branch_number >= 1385
def StartTracing(self, custom_categories=None,
timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
""" custom_categories is an optional string containing a list of
comma separated categories that will be traced instead of the
default category set. Example: use
"webkit,cc,disabled-by-default-cc.debug" to trace only those three
event categories.
"""
if self._tracing_backend is None:
self._tracing_backend = tracing_backend.TracingBackend(self._port)
return self._tracing_backend.StartTracing(custom_categories, timeout)
@property
def is_tracing_running(self):
if not self._tracing_backend:
return None
return self._tracing_backend.is_tracing_running
def StopTracing(self):
""" Stops tracing and returns the result as TimelineData object. """
for (i, debugger_url) in enumerate(self._browser.tabs):
tab = self.tab_list_backend.Get(i, None)
if tab:
success = tab.EvaluateJavaScript(
"console.time('" + debugger_url + "');" +
"console.timeEnd('" + debugger_url + "');" +
"console.time.toString().indexOf('[native code]') != -1;")
if not success:
raise Exception('Page stomped on console.time')
self._tracing_backend.AddTabToMarkerMapping(tab, debugger_url)
return self._tracing_backend.StopTracing()
def GetProcessName(self, cmd_line):
"""Returns a user-friendly name for the process of the given |cmd_line|."""
if not cmd_line:
# TODO(tonyg): Eventually we should make all of these known and add an
# assertion.
return 'unknown'
if 'nacl_helper_bootstrap' in cmd_line:
return 'nacl_helper_bootstrap'
if ':sandboxed_process' in cmd_line:
return 'renderer'
m = re.match(r'.* --type=([^\s]*) .*', cmd_line)
if not m:
return 'browser'
return m.group(1)
def Close(self):
if self._tracing_backend:
self._tracing_backend.Close()
self._tracing_backend = None
@property
def supports_system_info(self):
return self.GetSystemInfo() != None
def GetSystemInfo(self):
if self._system_info_backend is None:
self._system_info_backend = system_info_backend.SystemInfoBackend(
self._port)
return self._system_info_backend.GetSystemInfo()
def _SetBranchNumber(self, version):
assert version
self._chrome_branch_number = re.search(r'\d+\.\d+\.(\d+)\.\d+',
version).group(1)