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