• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 py_trace_event import trace_event
12
13from telemetry.core import exceptions
14from telemetry import decorators
15from telemetry.internal.backends.chrome_inspector import devtools_http
16from telemetry.internal.backends.chrome_inspector import inspector_console
17from telemetry.internal.backends.chrome_inspector import inspector_memory
18from telemetry.internal.backends.chrome_inspector import inspector_page
19from telemetry.internal.backends.chrome_inspector import inspector_runtime
20from telemetry.internal.backends.chrome_inspector import inspector_websocket
21from telemetry.internal.backends.chrome_inspector import websocket
22from telemetry.util import js_template
23
24import py_utils
25
26
27def _HandleInspectorWebSocketExceptions(func):
28  """Decorator for converting inspector_websocket exceptions.
29
30  When an inspector_websocket exception is thrown in the original function,
31  this decorator converts it into a telemetry exception and adds debugging
32  information.
33  """
34  @functools.wraps(func)
35  def inner(inspector_backend, *args, **kwargs):
36    try:
37      return func(inspector_backend, *args, **kwargs)
38    except (socket.error, websocket.WebSocketException,
39            inspector_websocket.WebSocketDisconnected) as e:
40      inspector_backend._ConvertExceptionFromInspectorWebsocket(e)
41
42  return inner
43
44
45class InspectorBackend(object):
46  """Class for communicating with a devtools client.
47
48  The owner of an instance of this class is responsible for calling
49  Disconnect() before disposing of the instance.
50  """
51
52  __metaclass__ = trace_event.TracedMetaClass
53
54  def __init__(self, app, devtools_client, context, timeout=120):
55    self._websocket = inspector_websocket.InspectorWebsocket()
56    self._websocket.RegisterDomain(
57        'Inspector', self._HandleInspectorDomainNotification)
58
59    self._app = app
60    self._devtools_client = devtools_client
61    # Be careful when using the context object, since the data may be
62    # outdated since this is never updated once InspectorBackend is
63    # created. Consider an updating strategy for this. (For an example
64    # of the subtlety, see the logic for self.url property.)
65    self._context = context
66
67    logging.debug('InspectorBackend._Connect() to %s', self.debugger_url)
68    try:
69      self._websocket.Connect(self.debugger_url, timeout)
70      self._console = inspector_console.InspectorConsole(self._websocket)
71      self._memory = inspector_memory.InspectorMemory(self._websocket)
72      self._page = inspector_page.InspectorPage(
73          self._websocket, timeout=timeout)
74      self._runtime = inspector_runtime.InspectorRuntime(self._websocket)
75    except (websocket.WebSocketException, exceptions.TimeoutException,
76            py_utils.TimeoutException) as e:
77      self._ConvertExceptionFromInspectorWebsocket(e)
78
79  def Disconnect(self):
80    """Disconnects the inspector websocket.
81
82    This method intentionally leaves the self._websocket object around, so that
83    future calls it to it will fail with a relevant error.
84    """
85    if self._websocket:
86      self._websocket.Disconnect()
87
88  def __del__(self):
89    self.Disconnect()
90
91  @property
92  def app(self):
93    return self._app
94
95  @property
96  def url(self):
97    """Returns the URL of the tab, as reported by devtools.
98
99    Raises:
100      devtools_http.DevToolsClientConnectionError
101    """
102    return self._devtools_client.GetUrl(self.id)
103
104  @property
105  def id(self):
106    return self._context['id']
107
108  @property
109  def debugger_url(self):
110    return self._context['webSocketDebuggerUrl']
111
112  def GetWebviewInspectorBackends(self):
113    """Returns a list of InspectorBackend instances associated with webviews.
114
115    Raises:
116      devtools_http.DevToolsClientConnectionError
117    """
118    inspector_backends = []
119    devtools_context_map = self._devtools_client.GetUpdatedInspectableContexts()
120    for context in devtools_context_map.contexts:
121      if context['type'] == 'webview':
122        inspector_backends.append(
123            devtools_context_map.GetInspectorBackend(context['id']))
124    return inspector_backends
125
126  def IsInspectable(self):
127    """Whether the tab is inspectable, as reported by devtools."""
128    try:
129      return self._devtools_client.IsInspectable(self.id)
130    except devtools_http.DevToolsClientConnectionError:
131      return False
132
133  # Public methods implemented in JavaScript.
134
135  @property
136  @decorators.Cache
137  def screenshot_supported(self):
138    if (self.app.platform.GetOSName() == 'linux' and (
139        os.getenv('DISPLAY') not in [':0', ':0.0'])):
140      # Displays other than 0 mean we are likely running in something like
141      # xvfb where screenshotting doesn't work.
142      return False
143    return True
144
145  @_HandleInspectorWebSocketExceptions
146  def Screenshot(self, timeout):
147    assert self.screenshot_supported, 'Browser does not support screenshotting'
148    return self._page.CaptureScreenshot(timeout)
149
150  # Memory public methods.
151
152  @_HandleInspectorWebSocketExceptions
153  def GetDOMStats(self, timeout):
154    """Gets memory stats from the DOM.
155
156    Raises:
157      inspector_memory.InspectorMemoryException
158      exceptions.TimeoutException
159      exceptions.DevtoolsTargetCrashException
160    """
161    dom_counters = self._memory.GetDOMCounters(timeout)
162    return {
163      'document_count': dom_counters['documents'],
164      'node_count': dom_counters['nodes'],
165      'event_listener_count': dom_counters['jsEventListeners']
166    }
167
168  # Page public methods.
169
170  @_HandleInspectorWebSocketExceptions
171  def WaitForNavigate(self, timeout):
172    self._page.WaitForNavigate(timeout)
173
174  @_HandleInspectorWebSocketExceptions
175  def Navigate(self, url, script_to_evaluate_on_commit, timeout):
176    self._page.Navigate(url, script_to_evaluate_on_commit, timeout)
177
178  @_HandleInspectorWebSocketExceptions
179  def GetCookieByName(self, name, timeout):
180    return self._page.GetCookieByName(name, timeout)
181
182  # Console public methods.
183
184  @_HandleInspectorWebSocketExceptions
185  def GetCurrentConsoleOutputBuffer(self, timeout=10):
186    return self._console.GetCurrentConsoleOutputBuffer(timeout)
187
188  # Runtime public methods.
189
190  @_HandleInspectorWebSocketExceptions
191  def ExecuteJavaScript(self, statement, **kwargs):
192    """Executes a given JavaScript statement. Does not return the result.
193
194    Example: runner.ExecuteJavaScript('var foo = {{ value }};', value='hi');
195
196    Args:
197      statement: The statement to execute (provided as a string).
198
199    Optional keyword args:
200      timeout: The number of seconds to wait for the statement to execute.
201      context_id: The id of an iframe where to execute the code; the main page
202          has context_id=1, the first iframe context_id=2, etc.
203      Additional keyword arguments provide values to be interpolated within
204          the statement. See telemetry.util.js_template for details.
205
206    Raises:
207      py_utils.TimeoutException
208      exceptions.EvaluationException
209      exceptions.WebSocketException
210      exceptions.DevtoolsTargetCrashException
211    """
212    # Use the default both when timeout=None or the option is ommited.
213    timeout = kwargs.pop('timeout', None) or 60
214    context_id = kwargs.pop('context_id', None)
215    statement = js_template.Render(statement, **kwargs)
216    self._runtime.Execute(statement, context_id, timeout)
217
218  @_HandleInspectorWebSocketExceptions
219  def EvaluateJavaScript(self, expression, **kwargs):
220    """Returns the result of evaluating a given JavaScript expression.
221
222    Example: runner.ExecuteJavaScript('document.location.href');
223
224    Args:
225      expression: The expression to execute (provided as a string).
226
227    Optional keyword args:
228      timeout: The number of seconds to wait for the expression to evaluate.
229      context_id: The id of an iframe where to execute the code; the main page
230          has context_id=1, the first iframe context_id=2, etc.
231      Additional keyword arguments provide values to be interpolated within
232          the expression. See telemetry.util.js_template for details.
233
234    Raises:
235      py_utils.TimeoutException
236      exceptions.EvaluationException
237      exceptions.WebSocketException
238      exceptions.DevtoolsTargetCrashException
239    """
240    # Use the default both when timeout=None or the option is ommited.
241    timeout = kwargs.pop('timeout', None) or 60
242    context_id = kwargs.pop('context_id', None)
243    expression = js_template.Render(expression, **kwargs)
244    return self._runtime.Evaluate(expression, context_id, timeout)
245
246  def WaitForJavaScriptCondition(self, condition, **kwargs):
247    """Wait for a JavaScript condition to become truthy.
248
249    Example: runner.WaitForJavaScriptCondition('window.foo == 10');
250
251    Args:
252      condition: The JavaScript condition (provided as string).
253
254    Optional keyword args:
255      timeout: The number in seconds to wait for the condition to become
256          True (default to 60).
257      context_id: The id of an iframe where to execute the code; the main page
258          has context_id=1, the first iframe context_id=2, etc.
259      Additional keyword arguments provide values to be interpolated within
260          the expression. See telemetry.util.js_template for details.
261
262    Returns:
263      The value returned by the JavaScript condition that got interpreted as
264      true.
265
266    Raises:
267      py_utils.TimeoutException
268      exceptions.EvaluationException
269      exceptions.WebSocketException
270      exceptions.DevtoolsTargetCrashException
271    """
272    # Use the default both when timeout=None or the option is ommited.
273    timeout = kwargs.pop('timeout', None) or 60
274    context_id = kwargs.pop('context_id', None)
275    condition = js_template.Render(condition, **kwargs)
276
277    def IsJavaScriptExpressionTrue():
278      return self._runtime.Evaluate(condition, context_id, timeout)
279
280    try:
281      return py_utils.WaitFor(IsJavaScriptExpressionTrue, timeout)
282    except py_utils.TimeoutException as e:
283      # Try to make timeouts a little more actionable by dumping console output.
284      debug_message = None
285      try:
286        debug_message = (
287            'Console output:\n%s' %
288            self.GetCurrentConsoleOutputBuffer())
289      except Exception as e:
290        debug_message = (
291            'Exception thrown when trying to capture console output: %s' %
292            repr(e))
293      raise py_utils.TimeoutException(
294          e.message + '\n' + debug_message)
295
296  @_HandleInspectorWebSocketExceptions
297  def EnableAllContexts(self):
298    """Allows access to iframes.
299
300    Raises:
301      exceptions.WebSocketDisconnected
302      exceptions.TimeoutException
303      exceptions.DevtoolsTargetCrashException
304    """
305    return self._runtime.EnableAllContexts()
306
307  @_HandleInspectorWebSocketExceptions
308  def SynthesizeScrollGesture(self, x=100, y=800, xDistance=0, yDistance=-500,
309                              xOverscroll=None, yOverscroll=None,
310                              preventFling=None, speed=None,
311                              gestureSourceType=None, repeatCount=None,
312                              repeatDelayMs=None, interactionMarkerName=None,
313                              timeout=60):
314    """Runs an inspector command that causes a repeatable browser driven scroll.
315
316    Args:
317      x: X coordinate of the start of the gesture in CSS pixels.
318      y: Y coordinate of the start of the gesture in CSS pixels.
319      xDistance: Distance to scroll along the X axis (positive to scroll left).
320      yDistance: Distance to scroll along the Y axis (positive to scroll up).
321      xOverscroll: Number of additional pixels to scroll back along the X axis.
322      xOverscroll: Number of additional pixels to scroll back along the Y axis.
323      preventFling: Prevents a fling gesture.
324      speed: Swipe speed in pixels per second.
325      gestureSourceType: Which type of input events to be generated.
326      repeatCount: Number of additional repeats beyond the first scroll.
327      repeatDelayMs: Number of milliseconds delay between each repeat.
328      interactionMarkerName: The name of the interaction markers to generate.
329
330    Raises:
331      exceptions.TimeoutException
332      exceptions.DevtoolsTargetCrashException
333    """
334    params = {
335        'x': x,
336        'y': y,
337        'xDistance': xDistance,
338        'yDistance': yDistance
339    }
340
341    if preventFling is not None:
342      params['preventFling'] = preventFling
343
344    if xOverscroll is not None:
345      params['xOverscroll'] = xOverscroll
346
347    if yOverscroll is not None:
348      params['yOverscroll'] = yOverscroll
349
350    if speed is not None:
351      params['speed'] = speed
352
353    if repeatCount is not None:
354      params['repeatCount'] = repeatCount
355
356    if gestureSourceType is not None:
357      params['gestureSourceType'] = gestureSourceType
358
359    if repeatDelayMs is not None:
360      params['repeatDelayMs'] = repeatDelayMs
361
362    if interactionMarkerName is not None:
363      params['interactionMarkerName'] = interactionMarkerName
364
365    scroll_command = {
366      'method': 'Input.synthesizeScrollGesture',
367      'params': params
368    }
369    return self._runtime.RunInspectorCommand(scroll_command, timeout)
370
371  @_HandleInspectorWebSocketExceptions
372  def DispatchKeyEvent(self, keyEventType='char', modifiers=None,
373                       timestamp=None, text=None, unmodifiedText=None,
374                       keyIdentifier=None, domCode=None, domKey=None,
375                       windowsVirtualKeyCode=None, nativeVirtualKeyCode=None,
376                       autoRepeat=None, isKeypad=None, isSystemKey=None,
377                       timeout=60):
378    """Dispatches a key event to the page.
379
380    Args:
381      type: Type of the key event. Allowed values: 'keyDown', 'keyUp',
382          'rawKeyDown', 'char'.
383      modifiers: Bit field representing pressed modifier keys. Alt=1, Ctrl=2,
384          Meta/Command=4, Shift=8 (default: 0).
385      timestamp: Time at which the event occurred. Measured in UTC time in
386          seconds since January 1, 1970 (default: current time).
387      text: Text as generated by processing a virtual key code with a keyboard
388          layout. Not needed for for keyUp and rawKeyDown events (default: '').
389      unmodifiedText: Text that would have been generated by the keyboard if no
390          modifiers were pressed (except for shift). Useful for shortcut
391          (accelerator) key handling (default: "").
392      keyIdentifier: Unique key identifier (e.g., 'U+0041') (default: '').
393      windowsVirtualKeyCode: Windows virtual key code (default: 0).
394      nativeVirtualKeyCode: Native virtual key code (default: 0).
395      autoRepeat: Whether the event was generated from auto repeat (default:
396          False).
397      isKeypad: Whether the event was generated from the keypad (default:
398          False).
399      isSystemKey: Whether the event was a system key event (default: False).
400
401    Raises:
402      exceptions.TimeoutException
403      exceptions.DevtoolsTargetCrashException
404    """
405    params = {
406      'type': keyEventType,
407    }
408
409    if modifiers is not None:
410      params['modifiers'] = modifiers
411    if timestamp is not None:
412      params['timestamp'] = timestamp
413    if text is not None:
414      params['text'] = text
415    if unmodifiedText is not None:
416      params['unmodifiedText'] = unmodifiedText
417    if keyIdentifier is not None:
418      params['keyIdentifier'] = keyIdentifier
419    if domCode is not None:
420      params['code'] = domCode
421    if domKey is not None:
422      params['key'] = domKey
423    if windowsVirtualKeyCode is not None:
424      params['windowsVirtualKeyCode'] = windowsVirtualKeyCode
425    if nativeVirtualKeyCode is not None:
426      params['nativeVirtualKeyCode'] = nativeVirtualKeyCode
427    if autoRepeat is not None:
428      params['autoRepeat'] = autoRepeat
429    if isKeypad is not None:
430      params['isKeypad'] = isKeypad
431    if isSystemKey is not None:
432      params['isSystemKey'] = isSystemKey
433
434    key_command = {
435      'method': 'Input.dispatchKeyEvent',
436      'params': params
437    }
438    return self._runtime.RunInspectorCommand(key_command, timeout)
439
440  # Methods used internally by other backends.
441
442  def _HandleInspectorDomainNotification(self, res):
443    if (res['method'] == 'Inspector.detached' and
444        res.get('params', {}).get('reason', '') == 'replaced_with_devtools'):
445      self._WaitForInspectorToGoAway()
446      return
447    if res['method'] == 'Inspector.targetCrashed':
448      exception = exceptions.DevtoolsTargetCrashException(self.app)
449      self._AddDebuggingInformation(exception)
450      raise exception
451
452  def _WaitForInspectorToGoAway(self):
453    self._websocket.Disconnect()
454    raw_input('The connection to Chrome was lost to the inspector ui.\n'
455              'Please close the inspector and press enter to resume '
456              'Telemetry run...')
457    raise exceptions.DevtoolsTargetCrashException(
458        self.app, 'Devtool connection with the browser was interrupted due to '
459        'the opening of an inspector.')
460
461  def _ConvertExceptionFromInspectorWebsocket(self, error):
462    """Converts an Exception from inspector_websocket.
463
464    This method always raises a Telemetry exception. It appends debugging
465    information. The exact exception raised depends on |error|.
466
467    Args:
468      error: An instance of socket.error or websocket.WebSocketException.
469    Raises:
470      exceptions.TimeoutException: A timeout occurred.
471      exceptions.DevtoolsTargetCrashException: On any other error, the most
472        likely explanation is that the devtool's target crashed.
473    """
474    if isinstance(error, websocket.WebSocketTimeoutException):
475      new_error = exceptions.TimeoutException()
476      new_error.AddDebuggingMessage(exceptions.AppCrashException(
477          self.app, 'The app is probably crashed:\n'))
478    else:
479      new_error = exceptions.DevtoolsTargetCrashException(self.app)
480
481    original_error_msg = 'Original exception:\n' + str(error)
482    new_error.AddDebuggingMessage(original_error_msg)
483    self._AddDebuggingInformation(new_error)
484
485    raise new_error, None, sys.exc_info()[2]
486
487  def _AddDebuggingInformation(self, error):
488    """Adds debugging information to error.
489
490    Args:
491      error: An instance of exceptions.Error.
492    """
493    if self.IsInspectable():
494      msg = (
495          'Received a socket error in the browser connection and the tab '
496          'still exists. The operation probably timed out.'
497      )
498    else:
499      msg = (
500          'Received a socket error in the browser connection and the tab no '
501          'longer exists. The tab probably crashed.'
502      )
503    error.AddDebuggingMessage(msg)
504    error.AddDebuggingMessage('Debugger url: %s' % self.debugger_url)
505
506  @_HandleInspectorWebSocketExceptions
507  def CollectGarbage(self):
508    self._page.CollectGarbage()
509