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