| # 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) |