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 functools
6import logging
7import os
8import socket
9import sys
10
11from telemetry.core import exceptions
12from telemetry import decorators
13from telemetry.internal.backends.chrome_inspector import devtools_http
14from telemetry.internal.backends.chrome_inspector import inspector_console
15from telemetry.internal.backends.chrome_inspector import inspector_memory
16from telemetry.internal.backends.chrome_inspector import inspector_page
17from telemetry.internal.backends.chrome_inspector import inspector_runtime
18from telemetry.internal.backends.chrome_inspector import inspector_websocket
19from telemetry.internal.backends.chrome_inspector import websocket
20
21
22def _HandleInspectorWebSocketExceptions(func):
23  """Decorator for converting inspector_websocket exceptions.
24
25  When an inspector_websocket exception is thrown in the original function,
26  this decorator converts it into a telemetry exception and adds debugging
27  information.
28  """
29  @functools.wraps(func)
30  def inner(inspector_backend, *args, **kwargs):
31    try:
32      return func(inspector_backend, *args, **kwargs)
33    except (socket.error, websocket.WebSocketException,
34            inspector_websocket.WebSocketDisconnected) as e:
35      inspector_backend._ConvertExceptionFromInspectorWebsocket(e)
36
37  return inner
38
39
40class InspectorBackend(object):
41  """Class for communicating with a devtools client.
42
43  The owner of an instance of this class is responsible for calling
44  Disconnect() before disposing of the instance.
45  """
46  def __init__(self, app, devtools_client, context, timeout=60):
47    self._websocket = inspector_websocket.InspectorWebsocket()
48    self._websocket.RegisterDomain(
49        'Inspector', self._HandleInspectorDomainNotification)
50
51    self._app = app
52    self._devtools_client = devtools_client
53    # Be careful when using the context object, since the data may be
54    # outdated since this is never updated once InspectorBackend is
55    # created. Consider an updating strategy for this. (For an example
56    # of the subtlety, see the logic for self.url property.)
57    self._context = context
58
59    logging.debug('InspectorBackend._Connect() to %s', self.debugger_url)
60    try:
61      self._websocket.Connect(self.debugger_url)
62      self._console = inspector_console.InspectorConsole(self._websocket)
63      self._memory = inspector_memory.InspectorMemory(self._websocket)
64      self._page = inspector_page.InspectorPage(
65          self._websocket, timeout=timeout)
66      self._runtime = inspector_runtime.InspectorRuntime(self._websocket)
67    except (websocket.WebSocketException, exceptions.TimeoutException) as e:
68      self._ConvertExceptionFromInspectorWebsocket(e)
69
70  def Disconnect(self):
71    """Disconnects the inspector websocket.
72
73    This method intentionally leaves the self._websocket object around, so that
74    future calls it to it will fail with a relevant error.
75    """
76    if self._websocket:
77      self._websocket.Disconnect()
78
79  def __del__(self):
80    self.Disconnect()
81
82  @property
83  def app(self):
84    return self._app
85
86  @property
87  def url(self):
88    """Returns the URL of the tab, as reported by devtools.
89
90    Raises:
91      devtools_http.DevToolsClientConnectionError
92    """
93    return self._devtools_client.GetUrl(self.id)
94
95  @property
96  def id(self):
97    return self._context['id']
98
99  @property
100  def debugger_url(self):
101    return self._context['webSocketDebuggerUrl']
102
103  def GetWebviewInspectorBackends(self):
104    """Returns a list of InspectorBackend instances associated with webviews.
105
106    Raises:
107      devtools_http.DevToolsClientConnectionError
108    """
109    inspector_backends = []
110    devtools_context_map = self._devtools_client.GetUpdatedInspectableContexts()
111    for context in devtools_context_map.contexts:
112      if context['type'] == 'webview':
113        inspector_backends.append(
114            devtools_context_map.GetInspectorBackend(context['id']))
115    return inspector_backends
116
117  def IsInspectable(self):
118    """Whether the tab is inspectable, as reported by devtools."""
119    try:
120      return self._devtools_client.IsInspectable(self.id)
121    except devtools_http.DevToolsClientConnectionError:
122      return False
123
124  # Public methods implemented in JavaScript.
125
126  @property
127  @decorators.Cache
128  def screenshot_supported(self):
129    if (self.app.platform.GetOSName() == 'linux' and (
130        os.getenv('DISPLAY') not in [':0', ':0.0'])):
131      # Displays other than 0 mean we are likely running in something like
132      # xvfb where screenshotting doesn't work.
133      return False
134    return True
135
136  @_HandleInspectorWebSocketExceptions
137  def Screenshot(self, timeout):
138    assert self.screenshot_supported, 'Browser does not support screenshotting'
139    return self._page.CaptureScreenshot(timeout)
140
141  # Memory public methods.
142
143  @_HandleInspectorWebSocketExceptions
144  def GetDOMStats(self, timeout):
145    """Gets memory stats from the DOM.
146
147    Raises:
148      inspector_memory.InspectorMemoryException
149      exceptions.TimeoutException
150      exceptions.DevtoolsTargetCrashException
151    """
152    dom_counters = self._memory.GetDOMCounters(timeout)
153    return {
154      'document_count': dom_counters['documents'],
155      'node_count': dom_counters['nodes'],
156      'event_listener_count': dom_counters['jsEventListeners']
157    }
158
159  # Page public methods.
160
161  @_HandleInspectorWebSocketExceptions
162  def WaitForNavigate(self, timeout):
163    self._page.WaitForNavigate(timeout)
164
165  @_HandleInspectorWebSocketExceptions
166  def Navigate(self, url, script_to_evaluate_on_commit, timeout):
167    self._page.Navigate(url, script_to_evaluate_on_commit, timeout)
168
169  @_HandleInspectorWebSocketExceptions
170  def GetCookieByName(self, name, timeout):
171    return self._page.GetCookieByName(name, timeout)
172
173  # Console public methods.
174
175  @_HandleInspectorWebSocketExceptions
176  def GetCurrentConsoleOutputBuffer(self, timeout=10):
177    return self._console.GetCurrentConsoleOutputBuffer(timeout)
178
179  # Runtime public methods.
180
181  @_HandleInspectorWebSocketExceptions
182  def ExecuteJavaScript(self, expr, context_id=None, timeout=60):
183    """Executes a javascript expression without returning the result.
184
185    Raises:
186      exceptions.EvaluateException
187      exceptions.WebSocketDisconnected
188      exceptions.TimeoutException
189      exceptions.DevtoolsTargetCrashException
190    """
191    self._runtime.Execute(expr, context_id, timeout)
192
193  @_HandleInspectorWebSocketExceptions
194  def EvaluateJavaScript(self, expr, context_id=None, timeout=60):
195    """Evaluates a javascript expression and returns the result.
196
197    Raises:
198      exceptions.EvaluateException
199      exceptions.WebSocketDisconnected
200      exceptions.TimeoutException
201      exceptions.DevtoolsTargetCrashException
202    """
203    return self._runtime.Evaluate(expr, context_id, timeout)
204
205  @_HandleInspectorWebSocketExceptions
206  def EnableAllContexts(self):
207    """Allows access to iframes.
208
209    Raises:
210      exceptions.WebSocketDisconnected
211      exceptions.TimeoutException
212      exceptions.DevtoolsTargetCrashException
213    """
214    return self._runtime.EnableAllContexts()
215
216  @_HandleInspectorWebSocketExceptions
217  def SynthesizeScrollGesture(self, x=100, y=800, xDistance=0, yDistance=-500,
218                              xOverscroll=None, yOverscroll=None,
219                              preventFling=True, speed=None,
220                              gestureSourceType=None, repeatCount=None,
221                              repeatDelayMs=None, interactionMarkerName=None,
222                              timeout=60):
223    """Runs an inspector command that causes a repeatable browser driven scroll.
224
225    Args:
226      x: X coordinate of the start of the gesture in CSS pixels.
227      y: Y coordinate of the start of the gesture in CSS pixels.
228      xDistance: Distance to scroll along the X axis (positive to scroll left).
229      yDistance: Distance to scroll along the Y axis (positive to scroll up).
230      xOverscroll: Number of additional pixels to scroll back along the X axis.
231      xOverscroll: Number of additional pixels to scroll back along the Y axis.
232      preventFling: Prevents a fling gesture.
233      speed: Swipe speed in pixels per second.
234      gestureSourceType: Which type of input events to be generated.
235      repeatCount: Number of additional repeats beyond the first scroll.
236      repeatDelayMs: Number of milliseconds delay between each repeat.
237      interactionMarkerName: The name of the interaction markers to generate.
238
239    Raises:
240      exceptions.TimeoutException
241      exceptions.DevtoolsTargetCrashException
242    """
243    params = {
244        'x': x,
245        'y': y,
246        'xDistance': xDistance,
247        'yDistance': yDistance,
248        'preventFling': preventFling,
249    }
250
251    if xOverscroll is not None:
252      params['xOverscroll'] = xOverscroll
253
254    if yOverscroll is not None:
255      params['yOverscroll'] = yOverscroll
256
257    if speed is not None:
258      params['speed'] = speed
259
260    if repeatCount is not None:
261      params['repeatCount'] = repeatCount
262
263    if gestureSourceType is not None:
264      params['gestureSourceType'] = gestureSourceType
265
266    if repeatDelayMs is not None:
267      params['repeatDelayMs'] = repeatDelayMs
268
269    if interactionMarkerName is not None:
270      params['interactionMarkerName'] = interactionMarkerName
271
272    scroll_command = {
273      'method': 'Input.synthesizeScrollGesture',
274      'params': params
275    }
276    return self._runtime.RunInspectorCommand(scroll_command, timeout)
277
278  # Methods used internally by other backends.
279
280  def _HandleInspectorDomainNotification(self, res):
281    if (res['method'] == 'Inspector.detached' and
282        res.get('params', {}).get('reason', '') == 'replaced_with_devtools'):
283      self._WaitForInspectorToGoAway()
284      return
285    if res['method'] == 'Inspector.targetCrashed':
286      exception = exceptions.DevtoolsTargetCrashException(self.app)
287      self._AddDebuggingInformation(exception)
288      raise exception
289
290  def _WaitForInspectorToGoAway(self):
291    self._websocket.Disconnect()
292    raw_input('The connection to Chrome was lost to the inspector ui.\n'
293              'Please close the inspector and press enter to resume '
294              'Telemetry run...')
295    raise exceptions.DevtoolsTargetCrashException(
296        self.app, 'Devtool connection with the browser was interrupted due to '
297        'the opening of an inspector.')
298
299  def _ConvertExceptionFromInspectorWebsocket(self, error):
300    """Converts an Exception from inspector_websocket.
301
302    This method always raises a Telemetry exception. It appends debugging
303    information. The exact exception raised depends on |error|.
304
305    Args:
306      error: An instance of socket.error or websocket.WebSocketException.
307    Raises:
308      exceptions.TimeoutException: A timeout occurred.
309      exceptions.DevtoolsTargetCrashException: On any other error, the most
310        likely explanation is that the devtool's target crashed.
311    """
312    if isinstance(error, websocket.WebSocketTimeoutException):
313      new_error = exceptions.TimeoutException()
314      new_error.AddDebuggingMessage(exceptions.AppCrashException(
315          self.app, 'The app is probably crashed:\n'))
316    else:
317      new_error = exceptions.DevtoolsTargetCrashException(self.app)
318
319    original_error_msg = 'Original exception:\n' + str(error)
320    new_error.AddDebuggingMessage(original_error_msg)
321    self._AddDebuggingInformation(new_error)
322
323    raise new_error, None, sys.exc_info()[2]
324
325  def _AddDebuggingInformation(self, error):
326    """Adds debugging information to error.
327
328    Args:
329      error: An instance of exceptions.Error.
330    """
331    if self.IsInspectable():
332      msg = (
333          'Received a socket error in the browser connection and the tab '
334          'still exists. The operation probably timed out.'
335      )
336    else:
337      msg = (
338          'Received a socket error in the browser connection and the tab no '
339          'longer exists. The tab probably crashed.'
340      )
341    error.AddDebuggingMessage(msg)
342    error.AddDebuggingMessage('Debugger url: %s' % self.debugger_url)
343
344  @_HandleInspectorWebSocketExceptions
345  def CollectGarbage(self):
346    self._page.CollectGarbage()
347