chrome_browser_backend.py revision 5c02ac1a9c1b504631c0a3d2b6e737b5d738bae1
1# Copyright 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import httplib
6import json
7import logging
8import pprint
9import re
10import socket
11import sys
12import urllib2
13
14from telemetry import decorators
15from telemetry.core import exceptions
16from telemetry.core import forwarders
17from telemetry.core import user_agent
18from telemetry.core import util
19from telemetry.core import web_contents
20from telemetry.core import wpr_modes
21from telemetry.core import wpr_server
22from telemetry.core.backends import browser_backend
23from telemetry.core.backends.chrome import extension_backend
24from telemetry.core.backends.chrome import system_info_backend
25from telemetry.core.backends.chrome import tab_list_backend
26from telemetry.core.backends.chrome import tracing_backend
27from telemetry.unittest import options_for_unittests
28
29
30class ChromeBrowserBackend(browser_backend.BrowserBackend):
31  """An abstract class for chrome browser backends. Provides basic functionality
32  once a remote-debugger port has been established."""
33  # It is OK to have abstract methods. pylint: disable=W0223
34
35  def __init__(self, is_content_shell, supports_extensions, browser_options,
36               output_profile_path, extensions_to_load):
37    super(ChromeBrowserBackend, self).__init__(
38        is_content_shell=is_content_shell,
39        supports_extensions=supports_extensions,
40        browser_options=browser_options,
41        tab_list_backend=tab_list_backend.TabListBackend)
42    self._port = None
43
44    self._inspector_protocol_version = 0
45    self._chrome_branch_number = None
46    self._tracing_backend = None
47    self._system_info_backend = None
48
49    self._output_profile_path = output_profile_path
50    self._extensions_to_load = extensions_to_load
51
52    if browser_options.netsim:
53      self.wpr_port_pairs = forwarders.PortPairs(
54          http=forwarders.PortPair(80, 80),
55          https=forwarders.PortPair(443, 443),
56          dns=forwarders.PortPair(53, 53))
57    else:
58      self.wpr_port_pairs = forwarders.PortPairs(
59          http=forwarders.PortPair(0, 0),
60          https=forwarders.PortPair(0, 0),
61          dns=None)
62
63    if (self.browser_options.dont_override_profile and
64        not options_for_unittests.AreSet()):
65      sys.stderr.write('Warning: Not overriding profile. This can cause '
66                       'unexpected effects due to profile-specific settings, '
67                       'such as about:flags settings, cookies, and '
68                       'extensions.\n')
69
70  def AddReplayServerOptions(self, extra_wpr_args):
71    if self.browser_options.netsim:
72      extra_wpr_args.append('--net=%s' % self.browser_options.netsim)
73    else:
74      extra_wpr_args.append('--no-dns_forwarding')
75
76  @property
77  @decorators.Cache
78  def extension_backend(self):
79    if not self.supports_extensions:
80      return None
81    return extension_backend.ExtensionBackendDict(self)
82
83  def GetBrowserStartupArgs(self):
84    args = []
85    args.extend(self.browser_options.extra_browser_args)
86    args.append('--disable-background-networking')
87    args.append('--enable-net-benchmarking')
88    args.append('--metrics-recording-only')
89    args.append('--no-default-browser-check')
90    args.append('--no-first-run')
91
92    # Turn on GPU benchmarking extension for all runs. The only side effect of
93    # the extension being on is that render stats are tracked. This is believed
94    # to be effectively free. And, by doing so here, it avoids us having to
95    # programmatically inspect a pageset's actions in order to determine if it
96    # might eventually scroll.
97    args.append('--enable-gpu-benchmarking')
98
99    # Set --no-proxy-server to work around some XP issues unless
100    # some other flag indicates a proxy is needed.
101    if not '--enable-spdy-proxy-auth' in args:
102      args.append('--no-proxy-server')
103
104    if self.browser_options.netsim:
105      args.append('--ignore-certificate-errors')
106    elif self.browser_options.wpr_mode != wpr_modes.WPR_OFF:
107      args.extend(wpr_server.GetChromeFlags(self.forwarder_factory.host_ip,
108                                            self.wpr_port_pairs))
109    args.extend(user_agent.GetChromeUserAgentArgumentFromType(
110        self.browser_options.browser_user_agent_type))
111
112    extensions = [extension.local_path
113                  for extension in self._extensions_to_load
114                  if not extension.is_component]
115    extension_str = ','.join(extensions)
116    if len(extensions) > 0:
117      args.append('--load-extension=%s' % extension_str)
118
119    component_extensions = [extension.local_path
120                            for extension in self._extensions_to_load
121                            if extension.is_component]
122    component_extension_str = ','.join(component_extensions)
123    if len(component_extensions) > 0:
124      args.append('--load-component-extension=%s' % component_extension_str)
125
126    if self.browser_options.no_proxy_server:
127      args.append('--no-proxy-server')
128
129    if self.browser_options.disable_component_extensions_with_background_pages:
130      args.append('--disable-component-extensions-with-background-pages')
131
132    return args
133
134  def HasBrowserFinishedLaunching(self):
135    try:
136      self.Request('')
137    except (exceptions.BrowserGoneException,
138            exceptions.BrowserConnectionGoneException):
139      return False
140    else:
141      return True
142
143  def _WaitForBrowserToComeUp(self, wait_for_extensions=True):
144    try:
145      util.WaitFor(self.HasBrowserFinishedLaunching, timeout=30)
146    except (util.TimeoutException, exceptions.ProcessGoneException) as e:
147      raise exceptions.BrowserGoneException(self.GetStackTrace())
148
149    def AllExtensionsLoaded():
150      # Extension pages are loaded from an about:blank page,
151      # so we need to check that the document URL is the extension
152      # page in addition to the ready state.
153      extension_ready_js = """
154          document.URL.lastIndexOf('chrome-extension://%s/', 0) == 0 &&
155          (document.readyState == 'complete' ||
156           document.readyState == 'interactive')
157      """
158      for e in self._extensions_to_load:
159        if not e.extension_id in self.extension_backend:
160          return False
161        extension_object = self.extension_backend[e.extension_id]
162        try:
163          res = extension_object.EvaluateJavaScript(
164              extension_ready_js % e.extension_id)
165        except exceptions.EvaluateException:
166          # If the inspected page is not ready, we will get an error
167          # when we evaluate a JS expression, but we can just keep polling
168          # until the page is ready (crbug.com/251913).
169          res = None
170
171        # TODO(tengs): We don't have full support for getting the Chrome
172        # version before launch, so for now we use a generic workaround to
173        # check for an extension binding bug in old versions of Chrome.
174        # See crbug.com/263162 for details.
175        if res and extension_object.EvaluateJavaScript(
176            'chrome.runtime == null'):
177          extension_object.Reload()
178        if not res:
179          return False
180      return True
181    if wait_for_extensions and self._supports_extensions:
182      try:
183        util.WaitFor(AllExtensionsLoaded, timeout=60)
184      except util.TimeoutException:
185        logging.error('ExtensionsToLoad: ' +
186            repr([e.extension_id for e in self._extensions_to_load]))
187        logging.error('Extension list: ' +
188            pprint.pformat(self.extension_backend, indent=4))
189        raise
190
191  def _PostBrowserStartupInitialization(self):
192    # Detect version information.
193    data = self.Request('version')
194    resp = json.loads(data)
195    if 'Protocol-Version' in resp:
196      self._inspector_protocol_version = resp['Protocol-Version']
197
198      if self._chrome_branch_number:
199        return
200
201      if 'Browser' in resp:
202        branch_number_match = re.search('Chrome/\d+\.\d+\.(\d+)\.\d+',
203                                        resp['Browser'])
204      else:
205        branch_number_match = re.search(
206            'Chrome/\d+\.\d+\.(\d+)\.\d+ (Mobile )?Safari',
207            resp['User-Agent'])
208
209      if branch_number_match:
210        self._chrome_branch_number = int(branch_number_match.group(1))
211
212      if not self._chrome_branch_number:
213        # Content Shell returns '' for Browser, WebViewShell returns '0'.
214        # For now we have to fall-back and assume branch 1025.
215        self._chrome_branch_number = 1025
216      return
217
218    # Detection has failed: assume 18.0.1025.168 ~= Chrome Android.
219    self._inspector_protocol_version = 1.0
220    self._chrome_branch_number = 1025
221
222  def ListInspectableContexts(self):
223    return json.loads(self.Request(''))
224
225  def Request(self, path, timeout=None, throw_network_exception=False):
226    url = 'http://127.0.0.1:%i/json' % self._port
227    if path:
228      url += '/' + path
229    try:
230      proxy_handler = urllib2.ProxyHandler({})  # Bypass any system proxy.
231      opener = urllib2.build_opener(proxy_handler)
232      req = opener.open(url, timeout=timeout)
233      return req.read()
234    except (socket.error, httplib.BadStatusLine, urllib2.URLError) as e:
235      if throw_network_exception:
236        raise e
237      if not self.IsBrowserRunning():
238        raise exceptions.BrowserGoneException(e)
239      raise exceptions.BrowserConnectionGoneException(e)
240
241  @property
242  def browser_directory(self):
243    raise NotImplementedError()
244
245  @property
246  def profile_directory(self):
247    raise NotImplementedError()
248
249  @property
250  def chrome_branch_number(self):
251    assert self._chrome_branch_number
252    return self._chrome_branch_number
253
254  @property
255  def supports_tab_control(self):
256    return self.chrome_branch_number >= 1303
257
258  @property
259  def supports_tracing(self):
260    return self.is_content_shell or self.chrome_branch_number >= 1385
261
262  def StartTracing(self, custom_categories=None,
263                   timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
264    """ custom_categories is an optional string containing a list of
265    comma separated categories that will be traced instead of the
266    default category set.  Example: use
267    "webkit,cc,disabled-by-default-cc.debug" to trace only those three
268    event categories.
269    """
270    if self._tracing_backend is None:
271      self._tracing_backend = tracing_backend.TracingBackend(self._port)
272    return self._tracing_backend.StartTracing(custom_categories, timeout)
273
274  @property
275  def is_tracing_running(self):
276    if not self._tracing_backend:
277      return None
278    return self._tracing_backend.is_tracing_running
279
280  def StopTracing(self):
281    """ Stops tracing and returns the result as TimelineData object. """
282    for (i, debugger_url) in enumerate(self._browser.tabs):
283      tab = self.tab_list_backend.Get(i, None)
284      if tab:
285        success = tab.EvaluateJavaScript(
286            "console.time('" + debugger_url + "');" +
287            "console.timeEnd('" + debugger_url + "');" +
288            "console.time.toString().indexOf('[native code]') != -1;")
289        if not success:
290          raise Exception('Page stomped on console.time')
291        self._tracing_backend.AddTabToMarkerMapping(tab, debugger_url)
292    return self._tracing_backend.StopTracing()
293
294  def GetProcessName(self, cmd_line):
295    """Returns a user-friendly name for the process of the given |cmd_line|."""
296    if not cmd_line:
297      # TODO(tonyg): Eventually we should make all of these known and add an
298      # assertion.
299      return 'unknown'
300    if 'nacl_helper_bootstrap' in cmd_line:
301      return 'nacl_helper_bootstrap'
302    if ':sandboxed_process' in cmd_line:
303      return 'renderer'
304    m = re.match(r'.* --type=([^\s]*) .*', cmd_line)
305    if not m:
306      return 'browser'
307    return m.group(1)
308
309  def Close(self):
310    if self._tracing_backend:
311      self._tracing_backend.Close()
312      self._tracing_backend = None
313
314  @property
315  def supports_system_info(self):
316    return self.GetSystemInfo() != None
317
318  def GetSystemInfo(self):
319    if self._system_info_backend is None:
320      self._system_info_backend = system_info_backend.SystemInfoBackend(
321          self._port)
322    return self._system_info_backend.GetSystemInfo()
323
324  def _SetBranchNumber(self, version):
325    assert version
326    self._chrome_branch_number = re.search(r'\d+\.\d+\.(\d+)\.\d+',
327                                           version).group(1)
328