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 contextlib
6import httplib
7import json
8import logging
9import pprint
10import re
11import socket
12import sys
13import urllib2
14
15from telemetry import decorators
16from telemetry.core import exceptions
17from telemetry.core import forwarders
18from telemetry.core import user_agent
19from telemetry.core import util
20from telemetry.core import web_contents
21from telemetry.core import wpr_modes
22from telemetry.core import wpr_server
23from telemetry.core.backends import browser_backend
24from telemetry.core.backends.chrome import extension_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.timeline import tracing_timeline_data
29from telemetry.unittest import options_for_unittests
30
31
32class ChromeBrowserBackend(browser_backend.BrowserBackend):
33  """An abstract class for chrome browser backends. Provides basic functionality
34  once a remote-debugger port has been established."""
35  # It is OK to have abstract methods. pylint: disable=W0223
36
37  def __init__(self, supports_tab_control, supports_extensions, browser_options,
38               output_profile_path, extensions_to_load):
39    super(ChromeBrowserBackend, self).__init__(
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._supports_tab_control = supports_tab_control
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('--enable-net-benchmarking')
87    args.append('--metrics-recording-only')
88    args.append('--no-default-browser-check')
89    args.append('--no-first-run')
90
91    # Turn on GPU benchmarking extension for all runs. The only side effect of
92    # the extension being on is that render stats are tracked. This is believed
93    # to be effectively free. And, by doing so here, it avoids us having to
94    # programmatically inspect a pageset's actions in order to determine if it
95    # might eventually scroll.
96    args.append('--enable-gpu-benchmarking')
97
98    # Set --no-proxy-server to work around some XP issues unless
99    # some other flag indicates a proxy is needed.
100    if not '--enable-spdy-proxy-auth' in args:
101      args.append('--no-proxy-server')
102
103    if self.browser_options.disable_background_networking:
104      args.append('--disable-background-networking')
105
106    if self.browser_options.netsim:
107      args.append('--ignore-certificate-errors')
108    elif self.browser_options.wpr_mode != wpr_modes.WPR_OFF:
109      args.extend(wpr_server.GetChromeFlags(self.forwarder_factory.host_ip,
110                                            self.wpr_port_pairs))
111    args.extend(user_agent.GetChromeUserAgentArgumentFromType(
112        self.browser_options.browser_user_agent_type))
113
114    extensions = [extension.local_path
115                  for extension in self._extensions_to_load
116                  if not extension.is_component]
117    extension_str = ','.join(extensions)
118    if len(extensions) > 0:
119      args.append('--load-extension=%s' % extension_str)
120
121    component_extensions = [extension.local_path
122                            for extension in self._extensions_to_load
123                            if extension.is_component]
124    component_extension_str = ','.join(component_extensions)
125    if len(component_extensions) > 0:
126      args.append('--load-component-extension=%s' % component_extension_str)
127
128    if self.browser_options.no_proxy_server:
129      args.append('--no-proxy-server')
130
131    if self.browser_options.disable_component_extensions_with_background_pages:
132      args.append('--disable-component-extensions-with-background-pages')
133
134    return args
135
136  def HasBrowserFinishedLaunching(self):
137    try:
138      self.Request('', timeout=.1)
139    except (exceptions.BrowserGoneException,
140            exceptions.BrowserConnectionGoneException):
141      return False
142    else:
143      return True
144
145  def _WaitForBrowserToComeUp(self, wait_for_extensions=True):
146    try:
147      util.WaitFor(self.HasBrowserFinishedLaunching, timeout=30)
148    except (util.TimeoutException, exceptions.ProcessGoneException) as e:
149      if not self.IsBrowserRunning():
150        raise exceptions.BrowserGoneException(self.browser, e)
151      raise exceptions.BrowserConnectionGoneException(self.browser, e)
152
153    def AllExtensionsLoaded():
154      # Extension pages are loaded from an about:blank page,
155      # so we need to check that the document URL is the extension
156      # page in addition to the ready state.
157      extension_ready_js = """
158          document.URL.lastIndexOf('chrome-extension://%s/', 0) == 0 &&
159          (document.readyState == 'complete' ||
160           document.readyState == 'interactive')
161      """
162      for e in self._extensions_to_load:
163        try:
164          extension_objects = self.extension_backend[e.extension_id]
165        except KeyError:
166          return False
167        for extension_object in extension_objects:
168          try:
169            res = extension_object.EvaluateJavaScript(
170                extension_ready_js % e.extension_id)
171          except exceptions.EvaluateException:
172            # If the inspected page is not ready, we will get an error
173            # when we evaluate a JS expression, but we can just keep polling
174            # until the page is ready (crbug.com/251913).
175            res = None
176
177          # TODO(tengs): We don't have full support for getting the Chrome
178          # version before launch, so for now we use a generic workaround to
179          # check for an extension binding bug in old versions of Chrome.
180          # See crbug.com/263162 for details.
181          if res and extension_object.EvaluateJavaScript(
182              'chrome.runtime == null'):
183            extension_object.Reload()
184          if not res:
185            return False
186      return True
187
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 ListInspectableContexts(self):
199    return json.loads(self.Request(''))
200
201  def Request(self, path, timeout=30, throw_network_exception=False):
202    url = 'http://127.0.0.1:%i/json' % self._port
203    if path:
204      url += '/' + path
205    try:
206      proxy_handler = urllib2.ProxyHandler({})  # Bypass any system proxy.
207      opener = urllib2.build_opener(proxy_handler)
208      with contextlib.closing(opener.open(url, timeout=timeout)) as req:
209        return req.read()
210    except (socket.error, httplib.BadStatusLine, urllib2.URLError) as e:
211      if throw_network_exception:
212        raise e
213      if not self.IsBrowserRunning():
214        raise exceptions.BrowserGoneException(self.browser, e)
215      raise exceptions.BrowserConnectionGoneException(self.browser, e)
216
217  @property
218  def browser_directory(self):
219    raise NotImplementedError()
220
221  @property
222  def profile_directory(self):
223    raise NotImplementedError()
224
225  @property
226  @decorators.Cache
227  def chrome_branch_number(self):
228    # Detect version information.
229    data = self.Request('version')
230    resp = json.loads(data)
231    if 'Protocol-Version' in resp:
232      if 'Browser' in resp:
233        branch_number_match = re.search('Chrome/\d+\.\d+\.(\d+)\.\d+',
234                                        resp['Browser'])
235      else:
236        branch_number_match = re.search(
237            'Chrome/\d+\.\d+\.(\d+)\.\d+ (Mobile )?Safari',
238            resp['User-Agent'])
239
240      if branch_number_match:
241        branch_number = int(branch_number_match.group(1))
242        if branch_number:
243          return branch_number
244
245    # Branch number can't be determined, so fail any branch number checks.
246    return 0
247
248  @property
249  def supports_tab_control(self):
250    return self._supports_tab_control
251
252  @property
253  def supports_tracing(self):
254    return True
255
256  def StartTracing(self, trace_options, custom_categories=None,
257                   timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
258    """
259    Args:
260        trace_options: An tracing_options.TracingOptions instance.
261        custom_categories: An optional string containing a list of
262                         comma separated categories that will be traced
263                         instead of the default category set.  Example: use
264                         "webkit,cc,disabled-by-default-cc.debug" to trace only
265                         those three event categories.
266    """
267    assert trace_options and trace_options.enable_chrome_trace
268    if self._tracing_backend is None:
269      self._tracing_backend = tracing_backend.TracingBackend(self._port, self)
270    return self._tracing_backend.StartTracing(
271        trace_options, custom_categories, timeout)
272
273  @property
274  def is_tracing_running(self):
275    if not self._tracing_backend:
276      return None
277    return self._tracing_backend.is_tracing_running
278
279  def StopTracing(self):
280    """ Stops tracing and returns the result as TimelineData object. """
281    tab_ids_list = []
282    for (i, _) in enumerate(self._browser.tabs):
283      tab = self.tab_list_backend.Get(i, None)
284      if tab:
285        success = tab.EvaluateJavaScript(
286            "console.time('" + tab.id + "');" +
287            "console.timeEnd('" + tab.id + "');" +
288            "console.time.toString().indexOf('[native code]') != -1;")
289        if not success:
290          raise Exception('Page stomped on console.time')
291        tab_ids_list.append(tab.id)
292    trace_events = self._tracing_backend.StopTracing()
293    # Augment tab_ids data to trace events.
294    event_data = {'traceEvents' : trace_events, 'tabIds': tab_ids_list}
295    return tracing_timeline_data.TracingTimelineData(event_data)
296
297  def GetProcessName(self, cmd_line):
298    """Returns a user-friendly name for the process of the given |cmd_line|."""
299    if not cmd_line:
300      # TODO(tonyg): Eventually we should make all of these known and add an
301      # assertion.
302      return 'unknown'
303    if 'nacl_helper_bootstrap' in cmd_line:
304      return 'nacl_helper_bootstrap'
305    if ':sandboxed_process' in cmd_line:
306      return 'renderer'
307    m = re.match(r'.* --type=([^\s]*) .*', cmd_line)
308    if not m:
309      return 'browser'
310    return m.group(1)
311
312  def Close(self):
313    if self._tracing_backend:
314      self._tracing_backend.Close()
315      self._tracing_backend = None
316
317  @property
318  def supports_system_info(self):
319    return self.GetSystemInfo() != None
320
321  def GetSystemInfo(self):
322    if self._system_info_backend is None:
323      self._system_info_backend = system_info_backend.SystemInfoBackend(
324          self._port)
325    return self._system_info_backend.GetSystemInfo()
326