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