Home | History | Annotate | Download | only in chrome_inspector
      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 
      5 import functools
      6 import logging
      7 import os
      8 import socket
      9 import sys
     10 
     11 from py_trace_event import trace_event
     12 
     13 from telemetry.core import exceptions
     14 from telemetry import decorators
     15 from telemetry.internal.backends.chrome_inspector import devtools_http
     16 from telemetry.internal.backends.chrome_inspector import inspector_console
     17 from telemetry.internal.backends.chrome_inspector import inspector_memory
     18 from telemetry.internal.backends.chrome_inspector import inspector_page
     19 from telemetry.internal.backends.chrome_inspector import inspector_runtime
     20 from telemetry.internal.backends.chrome_inspector import inspector_websocket
     21 from telemetry.internal.backends.chrome_inspector import websocket
     22 from telemetry.util import js_template
     23 
     24 import py_utils
     25 
     26 
     27 def _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 
     45 class 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