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 os
7import pprint
8import shlex
9import socket
10
11from telemetry.core import exceptions
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 tab_list_backend
16from telemetry.internal.backends.chrome_inspector import devtools_client_backend
17from telemetry.internal.backends.chrome_inspector import inspector_websocket
18from telemetry.internal.browser import web_contents
19
20import py_utils
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, browser_options,
29               browser_directory, profile_directory,
30               supports_extensions, supports_tab_control, build_dir=None):
31    """
32    Args:
33      platform_backend: The platform_backend.PlatformBackend instance to use.
34      browser_options: The browser_options.BrowserOptions instance to use.
35      browser_directory: A string containing a path to the directory where the
36          the browser is installed. This is typically the directory containing
37          the browser executable, but not guaranteed.
38      profile_directory: A string containing a path to the directory to store
39          browser profile information in.
40      supports_extensions: A boolean indicating whether the browser supports
41          extensions.
42      supports_tab_control: A boolean indicating whether the browser supports
43          the concept of tabs.
44      build_dir: A string containing a path to the directory that the browser
45          was built in, for finding debug artifacts. Can be None if the browser
46          was not locally built, or the directory otherwise cannot be
47          determined.
48    """
49    super(ChromeBrowserBackend, self).__init__(
50        platform_backend=platform_backend,
51        browser_options=browser_options,
52        supports_extensions=supports_extensions,
53        tab_list_backend=tab_list_backend.TabListBackend)
54    self._browser_directory = browser_directory
55    self._profile_directory = profile_directory
56    self._supports_tab_control = supports_tab_control
57    self._build_dir = build_dir
58
59    self._devtools_client = None
60
61    self._extensions_to_load = browser_options.extensions_to_load
62    if not supports_extensions and len(self._extensions_to_load) > 0:
63      raise browser_backend.ExtensionsNotSupportedException(
64          'Extensions are not supported on the selected browser')
65
66    if self.browser_options.dont_override_profile:
67      logging.warning('Not overriding profile. This can cause unexpected '
68                      'effects due to profile-specific settings, such as '
69                      'about:flags settings, cookies, and extensions.')
70
71  @property
72  def build_dir(self):
73    return self._build_dir
74
75  @property
76  def devtools_client(self):
77    return self._devtools_client
78
79  @property
80  @decorators.Cache
81  def extension_backend(self):
82    if not self.supports_extensions:
83      return None
84    return extension_backend.ExtensionBackendDict(self)
85
86  def _ArgsNeedProxyServer(self, args):
87    """Returns True if args for Chrome indicate the need for proxy server."""
88    if '--enable-spdy-proxy-auth' in args:
89      return True
90    return [arg for arg in args if arg.startswith('--proxy-server=')]
91
92  def HasDevToolsConnection(self):
93    return self._devtools_client and self._devtools_client.IsAlive()
94
95  def _FindDevToolsPortAndTarget(self):
96    """Clients should return a (devtools_port, browser_target) pair.
97
98    May also raise EnvironmentError (IOError or OSError) if this information
99    could not be determined; the call will be retried until it succeeds or
100    a timeout is met.
101    """
102    raise NotImplementedError
103
104  def _GetDevToolsClient(self):
105    # If the agent does not appear to be ready, it could be because we got the
106    # details of an older agent that no longer exists. It's thus important to
107    # re-read and update the port and target on each retry.
108    try:
109      devtools_port, browser_target = self._FindDevToolsPortAndTarget()
110    except EnvironmentError:
111      return None  # Port information not ready, will retry.
112
113    return devtools_client_backend.GetDevToolsBackEndIfReady(
114        devtools_port=devtools_port,
115        app_backend=self,
116        browser_target=browser_target)
117
118  def BindDevToolsClient(self):
119    """Find an existing DevTools agent and bind this browser backend to it."""
120    if self._devtools_client:
121      # In case we are launching a second browser instance (as is done by
122      # the CrOS backend), ensure that the old devtools_client is closed,
123      # otherwise re-creating it will fail.
124      self._devtools_client.Close()
125      self._devtools_client = None
126
127    try:
128      self._devtools_client = py_utils.WaitFor(
129          self._GetDevToolsClient,
130          timeout=self.browser_options.browser_startup_timeout)
131    except (py_utils.TimeoutException, exceptions.ProcessGoneException) as e:
132      if not self.IsBrowserRunning():
133        logging.exception(e)  # crbug.com/940075
134        raise exceptions.BrowserGoneException(self.browser, e)
135      raise exceptions.BrowserConnectionGoneException(self.browser, e)
136
137  def _WaitForExtensionsToLoad(self):
138    """ Wait for all extensions to load.
139    Be sure to check whether the browser_backend supports_extensions before
140    calling this method.
141    """
142    assert self._supports_extensions
143    assert self._devtools_client, (
144        'Waiting for extensions required devtool client to be initiated first')
145    try:
146      py_utils.WaitFor(self._AllExtensionsLoaded, timeout=60)
147    except py_utils.TimeoutException:
148      logging.error('ExtensionsToLoad: ' + repr(
149          [e.extension_id for e in self._extensions_to_load]))
150      logging.error('Extension list: ' + pprint.pformat(
151          self.extension_backend, indent=4))
152      raise
153
154  def _AllExtensionsLoaded(self):
155    # Extension pages are loaded from an about:blank page,
156    # so we need to check that the document URL is the extension
157    # page in addition to the ready state.
158    for e in self._extensions_to_load:
159      try:
160        extension_objects = self.extension_backend[e.extension_id]
161      except KeyError:
162        return False
163      for extension_object in extension_objects:
164        try:
165          res = extension_object.EvaluateJavaScript(
166              """
167              document.URL.lastIndexOf({{ url }}, 0) == 0 &&
168              (document.readyState == 'complete' ||
169               document.readyState == 'interactive')
170              """,
171              url='chrome-extension://%s/' % 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
189  @property
190  def browser_directory(self):
191    return self._browser_directory
192
193  @property
194  def profile_directory(self):
195    return self._profile_directory
196
197  @property
198  def supports_tab_control(self):
199    return self._supports_tab_control
200
201  def GetProcessName(self, cmd_line):
202    """Returns a user-friendly name for the process of the given |cmd_line|."""
203    if not cmd_line:
204      # TODO(tonyg): Eventually we should make all of these known and add an
205      # assertion.
206      return 'unknown'
207    if 'nacl_helper_bootstrap' in cmd_line:
208      return 'nacl_helper_bootstrap'
209    if ':sandboxed_process' in cmd_line:
210      return 'renderer'
211    if ':privileged_process' in cmd_line:
212      return 'gpu-process'
213    args = shlex.split(cmd_line)
214    types = [arg.split('=')[1] for arg in args if arg.startswith('--type=')]
215    if not types:
216      return 'browser'
217    return types[0]
218
219  @staticmethod
220  def GetThreadType(thread_name):
221    if not thread_name:
222      return 'unknown'
223    if (thread_name.startswith('Chrome_ChildIO') or
224        thread_name.startswith('Chrome_IO')):
225      return 'io'
226    if thread_name.startswith('Compositor'):
227      return 'compositor'
228    if thread_name.startswith('CrGpuMain'):
229      return 'gpu'
230    if (thread_name.startswith('ChildProcessMai') or
231        thread_name.startswith('CrRendererMain')):
232      return 'main'
233    if thread_name.startswith('RenderThread'):
234      return 'render'
235
236  def Close(self):
237    # If Chrome tracing is running, flush the trace before closing the browser.
238    tracing_backend = self._platform_backend.tracing_controller_backend
239    if tracing_backend.is_chrome_tracing_running:
240      tracing_backend.FlushTracing()
241
242    if self._devtools_client:
243      if "ENSURE_CLEAN_CHROME_SHUTDOWN" in os.environ:
244        # Forces a clean shutdown by sending a command to close the browser via
245        # the devtools client. Uses a long timeout as a clean shutdown can
246        # sometime take a long time to complete.
247        self._devtools_client.CloseBrowser()
248        py_utils.WaitFor(lambda: not self.IsBrowserRunning(), 300)
249      self._devtools_client.Close()
250      self._devtools_client = None
251
252
253  def GetSystemInfo(self):
254    try:
255      return self.devtools_client.GetSystemInfo()
256    except (inspector_websocket.WebSocketException, socket.error) as e:
257      if not self.IsBrowserRunning():
258        raise exceptions.BrowserGoneException(self.browser, e)
259      raise exceptions.BrowserConnectionGoneException(self.browser, e)
260
261  @property
262  def supports_memory_dumping(self):
263    return True
264
265  def DumpMemory(self, timeout=None, detail_level=None):
266    return self.devtools_client.DumpMemory(timeout=timeout,
267                                           detail_level=detail_level)
268
269  @property
270  def supports_overriding_memory_pressure_notifications(self):
271    return True
272
273  def SetMemoryPressureNotificationsSuppressed(
274      self, suppressed, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
275    self.devtools_client.SetMemoryPressureNotificationsSuppressed(
276        suppressed, timeout)
277
278  def SimulateMemoryPressureNotification(
279      self, pressure_level, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
280    self.devtools_client.SimulateMemoryPressureNotification(
281        pressure_level, timeout)
282
283  def GetDirectoryPathsToFlushOsPageCacheFor(self):
284    """ Return a list of directories to purge from OS page cache.
285
286    Will only be called when page cache clearing is necessary for a benchmark.
287    The caller will then attempt to purge all files from OS page cache for each
288    returned directory recursively.
289    """
290    paths_to_flush = []
291    if self.profile_directory:
292      paths_to_flush.append(self.profile_directory)
293    if self.browser_directory:
294      paths_to_flush.append(self.browser_directory)
295    return paths_to_flush
296
297  @property
298  def supports_cpu_metrics(self):
299    return True
300
301  @property
302  def supports_memory_metrics(self):
303    return True
304
305  def ExecuteBrowserCommand(self, command_id, timeout):
306    self.devtools_client.ExecuteBrowserCommand(command_id, timeout)
307