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