blob: b36bbd340e70f6fff574f1dc40920141bf868b3b [file] [log] [blame]
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import functools
import logging
import os
import socket
import sys
from py_trace_event import trace_event
from telemetry.core import exceptions
from telemetry import decorators
from telemetry.internal.backends.chrome_inspector import devtools_http
from telemetry.internal.backends.chrome_inspector import inspector_console
from telemetry.internal.backends.chrome_inspector import inspector_memory
from telemetry.internal.backends.chrome_inspector import inspector_page
from telemetry.internal.backends.chrome_inspector import inspector_runtime
from telemetry.internal.backends.chrome_inspector import inspector_websocket
from telemetry.internal.backends.chrome_inspector import websocket
def _HandleInspectorWebSocketExceptions(func):
"""Decorator for converting inspector_websocket exceptions.
When an inspector_websocket exception is thrown in the original function,
this decorator converts it into a telemetry exception and adds debugging
information.
"""
@functools.wraps(func)
def inner(inspector_backend, *args, **kwargs):
try:
return func(inspector_backend, *args, **kwargs)
except (socket.error, websocket.WebSocketException,
inspector_websocket.WebSocketDisconnected) as e:
inspector_backend._ConvertExceptionFromInspectorWebsocket(e)
return inner
class InspectorBackend(object):
"""Class for communicating with a devtools client.
The owner of an instance of this class is responsible for calling
Disconnect() before disposing of the instance.
"""
__metaclass__ = trace_event.TracedMetaClass
def __init__(self, app, devtools_client, context, timeout=120):
self._websocket = inspector_websocket.InspectorWebsocket()
self._websocket.RegisterDomain(
'Inspector', self._HandleInspectorDomainNotification)
self._app = app
self._devtools_client = devtools_client
# Be careful when using the context object, since the data may be
# outdated since this is never updated once InspectorBackend is
# created. Consider an updating strategy for this. (For an example
# of the subtlety, see the logic for self.url property.)
self._context = context
logging.debug('InspectorBackend._Connect() to %s', self.debugger_url)
try:
self._websocket.Connect(self.debugger_url, timeout)
self._console = inspector_console.InspectorConsole(self._websocket)
self._memory = inspector_memory.InspectorMemory(self._websocket)
self._page = inspector_page.InspectorPage(
self._websocket, timeout=timeout)
self._runtime = inspector_runtime.InspectorRuntime(self._websocket)
except (websocket.WebSocketException, exceptions.TimeoutException) as e:
self._ConvertExceptionFromInspectorWebsocket(e)
def Disconnect(self):
"""Disconnects the inspector websocket.
This method intentionally leaves the self._websocket object around, so that
future calls it to it will fail with a relevant error.
"""
if self._websocket:
self._websocket.Disconnect()
def __del__(self):
self.Disconnect()
@property
def app(self):
return self._app
@property
def url(self):
"""Returns the URL of the tab, as reported by devtools.
Raises:
devtools_http.DevToolsClientConnectionError
"""
return self._devtools_client.GetUrl(self.id)
@property
def id(self):
return self._context['id']
@property
def debugger_url(self):
return self._context['webSocketDebuggerUrl']
def GetWebviewInspectorBackends(self):
"""Returns a list of InspectorBackend instances associated with webviews.
Raises:
devtools_http.DevToolsClientConnectionError
"""
inspector_backends = []
devtools_context_map = self._devtools_client.GetUpdatedInspectableContexts()
for context in devtools_context_map.contexts:
if context['type'] == 'webview':
inspector_backends.append(
devtools_context_map.GetInspectorBackend(context['id']))
return inspector_backends
def IsInspectable(self):
"""Whether the tab is inspectable, as reported by devtools."""
try:
return self._devtools_client.IsInspectable(self.id)
except devtools_http.DevToolsClientConnectionError:
return False
# Public methods implemented in JavaScript.
@property
@decorators.Cache
def screenshot_supported(self):
if (self.app.platform.GetOSName() == 'linux' and (
os.getenv('DISPLAY') not in [':0', ':0.0'])):
# Displays other than 0 mean we are likely running in something like
# xvfb where screenshotting doesn't work.
return False
return True
@_HandleInspectorWebSocketExceptions
def Screenshot(self, timeout):
assert self.screenshot_supported, 'Browser does not support screenshotting'
return self._page.CaptureScreenshot(timeout)
# Memory public methods.
@_HandleInspectorWebSocketExceptions
def GetDOMStats(self, timeout):
"""Gets memory stats from the DOM.
Raises:
inspector_memory.InspectorMemoryException
exceptions.TimeoutException
exceptions.DevtoolsTargetCrashException
"""
dom_counters = self._memory.GetDOMCounters(timeout)
return {
'document_count': dom_counters['documents'],
'node_count': dom_counters['nodes'],
'event_listener_count': dom_counters['jsEventListeners']
}
# Page public methods.
@_HandleInspectorWebSocketExceptions
def WaitForNavigate(self, timeout):
self._page.WaitForNavigate(timeout)
@_HandleInspectorWebSocketExceptions
def Navigate(self, url, script_to_evaluate_on_commit, timeout):
self._page.Navigate(url, script_to_evaluate_on_commit, timeout)
@_HandleInspectorWebSocketExceptions
def GetCookieByName(self, name, timeout):
return self._page.GetCookieByName(name, timeout)
# Console public methods.
@_HandleInspectorWebSocketExceptions
def GetCurrentConsoleOutputBuffer(self, timeout=10):
return self._console.GetCurrentConsoleOutputBuffer(timeout)
# Runtime public methods.
@_HandleInspectorWebSocketExceptions
def ExecuteJavaScript(self, expr, context_id=None, timeout=60):
"""Executes a javascript expression without returning the result.
Raises:
exceptions.EvaluateException
exceptions.WebSocketDisconnected
exceptions.TimeoutException
exceptions.DevtoolsTargetCrashException
"""
self._runtime.Execute(expr, context_id, timeout)
@_HandleInspectorWebSocketExceptions
def EvaluateJavaScript(self, expr, context_id=None, timeout=60):
"""Evaluates a javascript expression and returns the result.
Raises:
exceptions.EvaluateException
exceptions.WebSocketDisconnected
exceptions.TimeoutException
exceptions.DevtoolsTargetCrashException
"""
return self._runtime.Evaluate(expr, context_id, timeout)
@_HandleInspectorWebSocketExceptions
def EnableAllContexts(self):
"""Allows access to iframes.
Raises:
exceptions.WebSocketDisconnected
exceptions.TimeoutException
exceptions.DevtoolsTargetCrashException
"""
return self._runtime.EnableAllContexts()
@_HandleInspectorWebSocketExceptions
def SynthesizeScrollGesture(self, x=100, y=800, xDistance=0, yDistance=-500,
xOverscroll=None, yOverscroll=None,
preventFling=True, speed=None,
gestureSourceType=None, repeatCount=None,
repeatDelayMs=None, interactionMarkerName=None,
timeout=60):
"""Runs an inspector command that causes a repeatable browser driven scroll.
Args:
x: X coordinate of the start of the gesture in CSS pixels.
y: Y coordinate of the start of the gesture in CSS pixels.
xDistance: Distance to scroll along the X axis (positive to scroll left).
yDistance: Distance to scroll along the Y axis (positive to scroll up).
xOverscroll: Number of additional pixels to scroll back along the X axis.
xOverscroll: Number of additional pixels to scroll back along the Y axis.
preventFling: Prevents a fling gesture.
speed: Swipe speed in pixels per second.
gestureSourceType: Which type of input events to be generated.
repeatCount: Number of additional repeats beyond the first scroll.
repeatDelayMs: Number of milliseconds delay between each repeat.
interactionMarkerName: The name of the interaction markers to generate.
Raises:
exceptions.TimeoutException
exceptions.DevtoolsTargetCrashException
"""
params = {
'x': x,
'y': y,
'xDistance': xDistance,
'yDistance': yDistance,
'preventFling': preventFling,
}
if xOverscroll is not None:
params['xOverscroll'] = xOverscroll
if yOverscroll is not None:
params['yOverscroll'] = yOverscroll
if speed is not None:
params['speed'] = speed
if repeatCount is not None:
params['repeatCount'] = repeatCount
if gestureSourceType is not None:
params['gestureSourceType'] = gestureSourceType
if repeatDelayMs is not None:
params['repeatDelayMs'] = repeatDelayMs
if interactionMarkerName is not None:
params['interactionMarkerName'] = interactionMarkerName
scroll_command = {
'method': 'Input.synthesizeScrollGesture',
'params': params
}
return self._runtime.RunInspectorCommand(scroll_command, timeout)
@_HandleInspectorWebSocketExceptions
def DispatchKeyEvent(self, keyEventType='char', modifiers=None,
timestamp=None, text=None, unmodifiedText=None,
keyIdentifier=None, domCode=None, domKey=None,
windowsVirtualKeyCode=None, nativeVirtualKeyCode=None,
autoRepeat=None, isKeypad=None, isSystemKey=None,
timeout=60):
"""Dispatches a key event to the page.
Args:
type: Type of the key event. Allowed values: 'keyDown', 'keyUp',
'rawKeyDown', 'char'.
modifiers: Bit field representing pressed modifier keys. Alt=1, Ctrl=2,
Meta/Command=4, Shift=8 (default: 0).
timestamp: Time at which the event occurred. Measured in UTC time in
seconds since January 1, 1970 (default: current time).
text: Text as generated by processing a virtual key code with a keyboard
layout. Not needed for for keyUp and rawKeyDown events (default: '').
unmodifiedText: Text that would have been generated by the keyboard if no
modifiers were pressed (except for shift). Useful for shortcut
(accelerator) key handling (default: "").
keyIdentifier: Unique key identifier (e.g., 'U+0041') (default: '').
windowsVirtualKeyCode: Windows virtual key code (default: 0).
nativeVirtualKeyCode: Native virtual key code (default: 0).
autoRepeat: Whether the event was generated from auto repeat (default:
False).
isKeypad: Whether the event was generated from the keypad (default:
False).
isSystemKey: Whether the event was a system key event (default: False).
Raises:
exceptions.TimeoutException
exceptions.DevtoolsTargetCrashException
"""
params = {
'type': keyEventType,
}
if modifiers is not None:
params['modifiers'] = modifiers
if timestamp is not None:
params['timestamp'] = timestamp
if text is not None:
params['text'] = text
if unmodifiedText is not None:
params['unmodifiedText'] = unmodifiedText
if keyIdentifier is not None:
params['keyIdentifier'] = keyIdentifier
if domCode is not None:
params['code'] = domCode
if domKey is not None:
params['key'] = domKey
if windowsVirtualKeyCode is not None:
params['windowsVirtualKeyCode'] = windowsVirtualKeyCode
if nativeVirtualKeyCode is not None:
params['nativeVirtualKeyCode'] = nativeVirtualKeyCode
if autoRepeat is not None:
params['autoRepeat'] = autoRepeat
if isKeypad is not None:
params['isKeypad'] = isKeypad
if isSystemKey is not None:
params['isSystemKey'] = isSystemKey
key_command = {
'method': 'Input.dispatchKeyEvent',
'params': params
}
return self._runtime.RunInspectorCommand(key_command, timeout)
# Methods used internally by other backends.
def _HandleInspectorDomainNotification(self, res):
if (res['method'] == 'Inspector.detached' and
res.get('params', {}).get('reason', '') == 'replaced_with_devtools'):
self._WaitForInspectorToGoAway()
return
if res['method'] == 'Inspector.targetCrashed':
exception = exceptions.DevtoolsTargetCrashException(self.app)
self._AddDebuggingInformation(exception)
raise exception
def _WaitForInspectorToGoAway(self):
self._websocket.Disconnect()
raw_input('The connection to Chrome was lost to the inspector ui.\n'
'Please close the inspector and press enter to resume '
'Telemetry run...')
raise exceptions.DevtoolsTargetCrashException(
self.app, 'Devtool connection with the browser was interrupted due to '
'the opening of an inspector.')
def _ConvertExceptionFromInspectorWebsocket(self, error):
"""Converts an Exception from inspector_websocket.
This method always raises a Telemetry exception. It appends debugging
information. The exact exception raised depends on |error|.
Args:
error: An instance of socket.error or websocket.WebSocketException.
Raises:
exceptions.TimeoutException: A timeout occurred.
exceptions.DevtoolsTargetCrashException: On any other error, the most
likely explanation is that the devtool's target crashed.
"""
if isinstance(error, websocket.WebSocketTimeoutException):
new_error = exceptions.TimeoutException()
new_error.AddDebuggingMessage(exceptions.AppCrashException(
self.app, 'The app is probably crashed:\n'))
else:
new_error = exceptions.DevtoolsTargetCrashException(self.app)
original_error_msg = 'Original exception:\n' + str(error)
new_error.AddDebuggingMessage(original_error_msg)
self._AddDebuggingInformation(new_error)
raise new_error, None, sys.exc_info()[2]
def _AddDebuggingInformation(self, error):
"""Adds debugging information to error.
Args:
error: An instance of exceptions.Error.
"""
if self.IsInspectable():
msg = (
'Received a socket error in the browser connection and the tab '
'still exists. The operation probably timed out.'
)
else:
msg = (
'Received a socket error in the browser connection and the tab no '
'longer exists. The tab probably crashed.'
)
error.AddDebuggingMessage(msg)
error.AddDebuggingMessage('Debugger url: %s' % self.debugger_url)
@_HandleInspectorWebSocketExceptions
def CollectGarbage(self):
self._page.CollectGarbage()