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 logging
6import pprint
7import shlex
8import sys
9
10from telemetry.core import exceptions
11from telemetry.core import util
12from telemetry import decorators
13from telemetry.internal.backends import browser_backend
14from telemetry.internal.backends.chrome import extension_backend
15from telemetry.internal.backends.chrome import system_info_backend
16from telemetry.internal.backends.chrome import tab_list_backend
17from telemetry.internal.backends.chrome_inspector import devtools_client_backend
18from telemetry.internal.browser import user_agent
19from telemetry.internal.browser import web_contents
20from telemetry.testing import options_for_unittests
21
22
23class ChromeBrowserBackend(browser_backend.BrowserBackend):
24  """An abstract class for chrome browser backends. Provides basic functionality
25  once a remote-debugger port has been established."""
26  # It is OK to have abstract methods. pylint: disable=abstract-method
27
28  def __init__(self, platform_backend, supports_tab_control,
29               supports_extensions, browser_options, output_profile_path,
30               extensions_to_load):
31    super(ChromeBrowserBackend, self).__init__(
32        platform_backend=platform_backend,
33        supports_extensions=supports_extensions,
34        browser_options=browser_options,
35        tab_list_backend=tab_list_backend.TabListBackend)
36    self._port = None
37
38    self._supports_tab_control = supports_tab_control
39    self._devtools_client = None
40    self._system_info_backend = None
41
42    self._output_profile_path = output_profile_path
43    self._extensions_to_load = extensions_to_load
44
45    if (self.browser_options.dont_override_profile and
46        not options_for_unittests.AreSet()):
47      sys.stderr.write('Warning: Not overriding profile. This can cause '
48                       'unexpected effects due to profile-specific settings, '
49                       'such as about:flags settings, cookies, and '
50                       'extensions.\n')
51
52  @property
53  def devtools_client(self):
54    return self._devtools_client
55
56  @property
57  @decorators.Cache
58  def extension_backend(self):
59    if not self.supports_extensions:
60      return None
61    return extension_backend.ExtensionBackendDict(self)
62
63  def _ArgsNeedProxyServer(self, args):
64    """Returns True if args for Chrome indicate the need for proxy server."""
65    if '--enable-spdy-proxy-auth' in args:
66      return True
67    return [arg for arg in args if arg.startswith('--proxy-server=')]
68
69  def GetBrowserStartupArgs(self):
70    args = []
71    args.extend(self.browser_options.extra_browser_args)
72    args.append('--enable-net-benchmarking')
73    args.append('--metrics-recording-only')
74    args.append('--no-default-browser-check')
75    args.append('--no-first-run')
76
77    # Turn on GPU benchmarking extension for all runs. The only side effect of
78    # the extension being on is that render stats are tracked. This is believed
79    # to be effectively free. And, by doing so here, it avoids us having to
80    # programmatically inspect a pageset's actions in order to determine if it
81    # might eventually scroll.
82    args.append('--enable-gpu-benchmarking')
83
84    # Set --no-proxy-server to work around some XP issues unless
85    # some other flag indicates a proxy is needed.
86    if not self._ArgsNeedProxyServer(args):
87      self.browser_options.no_proxy_server = True
88
89    if self.browser_options.disable_background_networking:
90      args.append('--disable-background-networking')
91    args.extend(self.GetReplayBrowserStartupArgs())
92    args.extend(user_agent.GetChromeUserAgentArgumentFromType(
93        self.browser_options.browser_user_agent_type))
94
95    extensions = [extension.local_path
96                  for extension in self._extensions_to_load
97                  if not extension.is_component]
98    extension_str = ','.join(extensions)
99    if len(extensions) > 0:
100      args.append('--load-extension=%s' % extension_str)
101
102    component_extensions = [extension.local_path
103                            for extension in self._extensions_to_load
104                            if extension.is_component]
105    component_extension_str = ','.join(component_extensions)
106    if len(component_extensions) > 0:
107      args.append('--load-component-extension=%s' % component_extension_str)
108
109    if self.browser_options.no_proxy_server:
110      args.append('--no-proxy-server')
111
112    if self.browser_options.disable_component_extensions_with_background_pages:
113      args.append('--disable-component-extensions-with-background-pages')
114
115    # Disables the start page, as well as other external apps that can
116    # steal focus or make measurements inconsistent.
117    if self.browser_options.disable_default_apps:
118      args.append('--disable-default-apps')
119
120    if self.browser_options.enable_logging:
121      args.append('--enable-logging')
122      args.append('--v=1')
123    return args
124
125  def GetReplayBrowserStartupArgs(self):
126    network_backend = self.platform_backend.network_controller_backend
127    if not network_backend.is_replay_active:
128      return []
129    replay_args = []
130    if not network_backend.is_test_ca_installed:
131      # Ignore certificate errors if the platform backend has not created
132      # and installed a root certificate.
133      replay_args.append('--ignore-certificate-errors')
134    # Force hostnames to resolve to the replay's host_ip.
135    replay_args.append('--host-resolver-rules=MAP * %s,EXCLUDE localhost' %
136                         network_backend.host_ip)
137    # Force the browser to send HTTP/HTTPS requests to fixed ports if they
138    # are not the standard HTTP/HTTPS ports.
139    device_ports = network_backend.wpr_device_ports
140    if device_ports.http != 80:
141      replay_args.append('--testing-fixed-http-port=%s' % device_ports.http)
142    if device_ports.https != 443:
143      replay_args.append('--testing-fixed-https-port=%s' % device_ports.https)
144    return replay_args
145
146  def HasBrowserFinishedLaunching(self):
147    assert self._port, 'No DevTools port info available.'
148    return devtools_client_backend.IsDevToolsAgentAvailable(self._port, self)
149
150  def _InitDevtoolsClientBackend(self, remote_devtools_port=None):
151    """ Initiate the devtool client backend which allow browser connection
152    through browser' devtool.
153
154    Args:
155      remote_devtools_port: The remote devtools port, if
156          any. Otherwise assumed to be the same as self._port.
157    """
158    assert not self._devtools_client, (
159        'Devtool client backend cannot be init twice')
160    self._devtools_client = devtools_client_backend.DevToolsClientBackend(
161        self._port, remote_devtools_port or self._port, self)
162
163  def _WaitForBrowserToComeUp(self):
164    """ Wait for browser to come up. """
165    try:
166      timeout = self.browser_options.browser_startup_timeout
167      util.WaitFor(self.HasBrowserFinishedLaunching, timeout=timeout)
168    except (exceptions.TimeoutException, exceptions.ProcessGoneException) as e:
169      if not self.IsBrowserRunning():
170        raise exceptions.BrowserGoneException(self.browser, e)
171      raise exceptions.BrowserConnectionGoneException(self.browser, e)
172
173  def _WaitForExtensionsToLoad(self):
174    """ Wait for all extensions to load.
175    Be sure to check whether the browser_backend supports_extensions before
176    calling this method.
177    """
178    assert self._supports_extensions
179    assert self._devtools_client, (
180        'Waiting for extensions required devtool client to be initiated first')
181    try:
182      util.WaitFor(self._AllExtensionsLoaded, timeout=60)
183    except exceptions.TimeoutException:
184      logging.error('ExtensionsToLoad: ' +
185          repr([e.extension_id for e in self._extensions_to_load]))
186      logging.error('Extension list: ' +
187          pprint.pformat(self.extension_backend, indent=4))
188      raise
189
190  def _AllExtensionsLoaded(self):
191    # Extension pages are loaded from an about:blank page,
192    # so we need to check that the document URL is the extension
193    # page in addition to the ready state.
194    extension_ready_js = """
195        document.URL.lastIndexOf('chrome-extension://%s/', 0) == 0 &&
196        (document.readyState == 'complete' ||
197         document.readyState == 'interactive')
198    """
199    for e in self._extensions_to_load:
200      try:
201        extension_objects = self.extension_backend[e.extension_id]
202      except KeyError:
203        return False
204      for extension_object in extension_objects:
205        try:
206          res = extension_object.EvaluateJavaScript(
207              extension_ready_js % e.extension_id)
208        except exceptions.EvaluateException:
209          # If the inspected page is not ready, we will get an error
210          # when we evaluate a JS expression, but we can just keep polling
211          # until the page is ready (crbug.com/251913).
212          res = None
213
214        # TODO(tengs): We don't have full support for getting the Chrome
215        # version before launch, so for now we use a generic workaround to
216        # check for an extension binding bug in old versions of Chrome.
217        # See crbug.com/263162 for details.
218        if res and extension_object.EvaluateJavaScript(
219            'chrome.runtime == null'):
220          extension_object.Reload()
221        if not res:
222          return False
223    return True
224
225  @property
226  def browser_directory(self):
227    raise NotImplementedError()
228
229  @property
230  def profile_directory(self):
231    raise NotImplementedError()
232
233  @property
234  def supports_tab_control(self):
235    return self._supports_tab_control
236
237  @property
238  def supports_tracing(self):
239    return True
240
241  def StartTracing(self, trace_options, custom_categories=None,
242                   timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
243    """
244    Args:
245        trace_options: An tracing_options.TracingOptions instance.
246        custom_categories: An optional string containing a list of
247                         comma separated categories that will be traced
248                         instead of the default category set.  Example: use
249                         "webkit,cc,disabled-by-default-cc.debug" to trace only
250                         those three event categories.
251    """
252    return self.devtools_client.StartChromeTracing(
253        trace_options, custom_categories, timeout)
254
255  def StopTracing(self, trace_data_builder):
256    self.devtools_client.StopChromeTracing(trace_data_builder)
257
258  def GetProcessName(self, cmd_line):
259    """Returns a user-friendly name for the process of the given |cmd_line|."""
260    if not cmd_line:
261      # TODO(tonyg): Eventually we should make all of these known and add an
262      # assertion.
263      return 'unknown'
264    if 'nacl_helper_bootstrap' in cmd_line:
265      return 'nacl_helper_bootstrap'
266    if ':sandboxed_process' in cmd_line:
267      return 'renderer'
268    if ':privileged_process' in cmd_line:
269      return 'gpu-process'
270    args = shlex.split(cmd_line)
271    types = [arg.split('=')[1] for arg in args if arg.startswith('--type=')]
272    if not types:
273      return 'browser'
274    return types[0]
275
276  def Close(self):
277    if self._devtools_client:
278      self._devtools_client.Close()
279      self._devtools_client = None
280
281  @property
282  def supports_system_info(self):
283    return self.GetSystemInfo() != None
284
285  def GetSystemInfo(self):
286    if self._system_info_backend is None:
287      self._system_info_backend = system_info_backend.SystemInfoBackend(
288          self._port)
289    return self._system_info_backend.GetSystemInfo()
290
291  @property
292  def supports_memory_dumping(self):
293    return True
294
295  def DumpMemory(self, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
296    return self.devtools_client.DumpMemory(timeout)
297
298  @property
299  def supports_overriding_memory_pressure_notifications(self):
300    return True
301
302  def SetMemoryPressureNotificationsSuppressed(
303      self, suppressed, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
304    self.devtools_client.SetMemoryPressureNotificationsSuppressed(
305        suppressed, timeout)
306
307  def SimulateMemoryPressureNotification(
308      self, pressure_level, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
309    self.devtools_client.SimulateMemoryPressureNotification(
310        pressure_level, timeout)
311
312  @property
313  def supports_cpu_metrics(self):
314    return True
315
316  @property
317  def supports_memory_metrics(self):
318    return True
319
320  @property
321  def supports_power_metrics(self):
322    return True
323