1# Copyright 2014 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 logging 6import re 7import socket 8import sys 9 10from telemetry.core import exceptions 11from telemetry import decorators 12from telemetry.internal.backends import browser_backend 13from telemetry.internal.backends.chrome_inspector import devtools_http 14from telemetry.internal.backends.chrome_inspector import inspector_backend 15from telemetry.internal.backends.chrome_inspector import inspector_websocket 16from telemetry.internal.backends.chrome_inspector import memory_backend 17from telemetry.internal.backends.chrome_inspector import tracing_backend 18from telemetry.internal.backends.chrome_inspector import websocket 19from telemetry.internal.platform.tracing_agent import chrome_tracing_agent 20from telemetry.internal.platform.tracing_agent import ( 21 chrome_tracing_devtools_manager) 22from telemetry.timeline import trace_data as trace_data_module 23 24 25BROWSER_INSPECTOR_WEBSOCKET_URL = 'ws://127.0.0.1:%i/devtools/browser' 26 27 28class TabNotFoundError(exceptions.Error): 29 pass 30 31 32def IsDevToolsAgentAvailable(port, app_backend): 33 """Returns True if a DevTools agent is available on the given port.""" 34 if (isinstance(app_backend, browser_backend.BrowserBackend) and 35 app_backend.supports_tracing): 36 inspector_websocket_instance = inspector_websocket.InspectorWebsocket() 37 try: 38 if not _IsInspectorWebsocketAvailable(inspector_websocket_instance, port): 39 return False 40 finally: 41 inspector_websocket_instance.Disconnect() 42 43 devtools_http_instance = devtools_http.DevToolsHttp(port) 44 try: 45 return _IsDevToolsAgentAvailable(devtools_http_instance) 46 finally: 47 devtools_http_instance.Disconnect() 48 49 50def _IsInspectorWebsocketAvailable(inspector_websocket_instance, port): 51 try: 52 inspector_websocket_instance.Connect(BROWSER_INSPECTOR_WEBSOCKET_URL % port) 53 except websocket.WebSocketException: 54 return False 55 except socket.error: 56 return False 57 except Exception as e: 58 sys.stderr.write('Unidentified exception while checking if wesocket is' 59 'available on port %i. Exception message: %s\n' % 60 (port, e.message)) 61 return False 62 else: 63 return True 64 65 66# TODO(nednguyen): Find a more reliable way to check whether the devtool agent 67# is still alive. 68def _IsDevToolsAgentAvailable(devtools_http_instance): 69 try: 70 devtools_http_instance.Request('') 71 except devtools_http.DevToolsClientConnectionError: 72 return False 73 else: 74 return True 75 76 77class DevToolsClientBackend(object): 78 """An object that communicates with Chrome's devtools. 79 80 This class owns a map of InspectorBackends. It is responsible for creating 81 them and destroying them. 82 """ 83 def __init__(self, devtools_port, remote_devtools_port, app_backend): 84 """Creates a new DevToolsClientBackend. 85 86 A DevTools agent must exist on the given devtools_port. 87 88 Args: 89 devtools_port: The port to use to connect to DevTools agent. 90 remote_devtools_port: In some cases (e.g., app running on 91 Android device, devtools_port is the forwarded port on the 92 host platform. We also need to know the remote_devtools_port 93 so that we can uniquely identify the DevTools agent. 94 app_backend: For the app that contains the DevTools agent. 95 """ 96 self._devtools_port = devtools_port 97 self._remote_devtools_port = remote_devtools_port 98 self._devtools_http = devtools_http.DevToolsHttp(devtools_port) 99 self._browser_inspector_websocket = None 100 self._tracing_backend = None 101 self._memory_backend = None 102 self._app_backend = app_backend 103 self._devtools_context_map_backend = _DevToolsContextMapBackend( 104 self._app_backend, self) 105 106 if not self.supports_tracing: 107 return 108 chrome_tracing_devtools_manager.RegisterDevToolsClient( 109 self, self._app_backend.platform_backend) 110 111 # Telemetry has started Chrome tracing if there is trace config, so start 112 # tracing on this newly created devtools client if needed. 113 trace_config = (self._app_backend.platform_backend 114 .tracing_controller_backend.GetChromeTraceConfig()) 115 if not trace_config: 116 self._CreateTracingBackendIfNeeded(is_tracing_running=False) 117 return 118 119 if self.support_startup_tracing: 120 self._CreateTracingBackendIfNeeded(is_tracing_running=True) 121 return 122 123 self._CreateTracingBackendIfNeeded(is_tracing_running=False) 124 self.StartChromeTracing( 125 trace_config=trace_config, 126 custom_categories=trace_config.tracing_category_filter.filter_string) 127 128 @property 129 def remote_port(self): 130 return self._remote_devtools_port 131 132 @property 133 def supports_tracing(self): 134 if not isinstance(self._app_backend, browser_backend.BrowserBackend): 135 return False 136 return self._app_backend.supports_tracing 137 138 @property 139 def supports_overriding_memory_pressure_notifications(self): 140 if not isinstance(self._app_backend, browser_backend.BrowserBackend): 141 return False 142 return self._app_backend.supports_overriding_memory_pressure_notifications 143 144 145 @property 146 def is_tracing_running(self): 147 if not self.supports_tracing: 148 return False 149 if not self._tracing_backend: 150 return False 151 return self._tracing_backend.is_tracing_running 152 153 @property 154 def support_startup_tracing(self): 155 # Startup tracing with --trace-config-file flag was not supported until 156 # Chromium branch number 2512 (see crrev.com/1309243004 and 157 # crrev.com/1353583002). 158 if not chrome_tracing_agent.ChromeTracingAgent.IsStartupTracingSupported( 159 self._app_backend.platform_backend): 160 return False 161 # TODO(zhenw): Remove this once stable Chrome and reference browser have 162 # passed 2512. 163 return self.GetChromeBranchNumber() >= 2512 164 165 def IsAlive(self): 166 """Whether the DevTools server is available and connectable.""" 167 return (self._devtools_http and 168 _IsDevToolsAgentAvailable(self._devtools_http)) 169 170 def Close(self): 171 if self._tracing_backend: 172 self._tracing_backend.Close() 173 self._tracing_backend = None 174 if self._memory_backend: 175 self._memory_backend.Close() 176 self._memory_backend = None 177 178 if self._devtools_context_map_backend: 179 self._devtools_context_map_backend.Clear() 180 181 # Close the browser inspector socket last (in case the backend needs to 182 # interact with it before closing). 183 if self._browser_inspector_websocket: 184 self._browser_inspector_websocket.Disconnect() 185 self._browser_inspector_websocket = None 186 187 assert self._devtools_http 188 self._devtools_http.Disconnect() 189 self._devtools_http = None 190 191 192 @decorators.Cache 193 def GetChromeBranchNumber(self): 194 # Detect version information. 195 resp = self._devtools_http.RequestJson('version') 196 if 'Protocol-Version' in resp: 197 if 'Browser' in resp: 198 branch_number_match = re.search(r'Chrome/\d+\.\d+\.(\d+)\.\d+', 199 resp['Browser']) 200 else: 201 branch_number_match = re.search( 202 r'Chrome/\d+\.\d+\.(\d+)\.\d+ (Mobile )?Safari', 203 resp['User-Agent']) 204 205 if branch_number_match: 206 branch_number = int(branch_number_match.group(1)) 207 if branch_number: 208 return branch_number 209 210 # Branch number can't be determined, so fail any branch number checks. 211 return 0 212 213 def _ListInspectableContexts(self): 214 return self._devtools_http.RequestJson('') 215 216 def RequestNewTab(self, timeout): 217 """Creates a new tab. 218 219 Returns: 220 A JSON string as returned by DevTools. Example: 221 { 222 "description": "", 223 "devtoolsFrontendUrl": 224 "/devtools/inspector.html?ws=host:port/devtools/page/id-string", 225 "id": "id-string", 226 "title": "Page Title", 227 "type": "page", 228 "url": "url", 229 "webSocketDebuggerUrl": "ws://host:port/devtools/page/id-string" 230 } 231 232 Raises: 233 devtools_http.DevToolsClientConnectionError 234 """ 235 return self._devtools_http.Request('new', timeout=timeout) 236 237 def CloseTab(self, tab_id, timeout): 238 """Closes the tab with the given id. 239 240 Raises: 241 devtools_http.DevToolsClientConnectionError 242 TabNotFoundError 243 """ 244 try: 245 return self._devtools_http.Request('close/%s' % tab_id, 246 timeout=timeout) 247 except devtools_http.DevToolsClientUrlError: 248 error = TabNotFoundError( 249 'Unable to close tab, tab id not found: %s' % tab_id) 250 raise error, None, sys.exc_info()[2] 251 252 def ActivateTab(self, tab_id, timeout): 253 """Activates the tab with the given id. 254 255 Raises: 256 devtools_http.DevToolsClientConnectionError 257 TabNotFoundError 258 """ 259 try: 260 return self._devtools_http.Request('activate/%s' % tab_id, 261 timeout=timeout) 262 except devtools_http.DevToolsClientUrlError: 263 error = TabNotFoundError( 264 'Unable to activate tab, tab id not found: %s' % tab_id) 265 raise error, None, sys.exc_info()[2] 266 267 def GetUrl(self, tab_id): 268 """Returns the URL of the tab with |tab_id|, as reported by devtools. 269 270 Raises: 271 devtools_http.DevToolsClientConnectionError 272 """ 273 for c in self._ListInspectableContexts(): 274 if c['id'] == tab_id: 275 return c['url'] 276 return None 277 278 def IsInspectable(self, tab_id): 279 """Whether the tab with |tab_id| is inspectable, as reported by devtools. 280 281 Raises: 282 devtools_http.DevToolsClientConnectionError 283 """ 284 contexts = self._ListInspectableContexts() 285 return tab_id in [c['id'] for c in contexts] 286 287 def GetUpdatedInspectableContexts(self): 288 """Returns an updated instance of _DevToolsContextMapBackend.""" 289 contexts = self._ListInspectableContexts() 290 self._devtools_context_map_backend._Update(contexts) 291 return self._devtools_context_map_backend 292 293 def _CreateTracingBackendIfNeeded(self, is_tracing_running=False): 294 assert self.supports_tracing 295 if not self._tracing_backend: 296 self._CreateAndConnectBrowserInspectorWebsocketIfNeeded() 297 self._tracing_backend = tracing_backend.TracingBackend( 298 self._browser_inspector_websocket, is_tracing_running) 299 300 def _CreateMemoryBackendIfNeeded(self): 301 assert self.supports_overriding_memory_pressure_notifications 302 if not self._memory_backend: 303 self._CreateAndConnectBrowserInspectorWebsocketIfNeeded() 304 self._memory_backend = memory_backend.MemoryBackend( 305 self._browser_inspector_websocket) 306 307 def _CreateAndConnectBrowserInspectorWebsocketIfNeeded(self): 308 if not self._browser_inspector_websocket: 309 self._browser_inspector_websocket = ( 310 inspector_websocket.InspectorWebsocket()) 311 self._browser_inspector_websocket.Connect( 312 BROWSER_INSPECTOR_WEBSOCKET_URL % self._devtools_port) 313 314 def IsChromeTracingSupported(self): 315 if not self.supports_tracing: 316 return False 317 self._CreateTracingBackendIfNeeded() 318 return self._tracing_backend.IsTracingSupported() 319 320 def StartChromeTracing( 321 self, trace_config, custom_categories=None, timeout=10): 322 """ 323 Args: 324 trace_config: An tracing_config.TracingConfig instance. 325 custom_categories: An optional string containing a list of 326 comma separated categories that will be traced 327 instead of the default category set. Example: use 328 "webkit,cc,disabled-by-default-cc.debug" to trace only 329 those three event categories. 330 """ 331 assert trace_config and trace_config.enable_chrome_trace 332 self._CreateTracingBackendIfNeeded() 333 return self._tracing_backend.StartTracing( 334 trace_config, custom_categories, timeout) 335 336 def StopChromeTracing(self, trace_data_builder, timeout=30): 337 assert self.is_tracing_running 338 try: 339 context_map = self.GetUpdatedInspectableContexts() 340 for context in context_map.contexts: 341 if context['type'] not in ['iframe', 'page', 'webview']: 342 continue 343 context_id = context['id'] 344 backend = context_map.GetInspectorBackend(context_id) 345 success = backend.EvaluateJavaScript( 346 "console.time('" + backend.id + "');" + 347 "console.timeEnd('" + backend.id + "');" + 348 "console.time.toString().indexOf('[native code]') != -1;") 349 if not success: 350 raise Exception('Page stomped on console.time') 351 trace_data_builder.AddEventsTo( 352 trace_data_module.TAB_ID_PART, [backend.id]) 353 finally: 354 self._tracing_backend.StopTracing(trace_data_builder, timeout) 355 356 def DumpMemory(self, timeout=30): 357 """Dumps memory. 358 359 Returns: 360 GUID of the generated dump if successful, None otherwise. 361 362 Raises: 363 TracingTimeoutException: If more than |timeout| seconds has passed 364 since the last time any data is received. 365 TracingUnrecoverableException: If there is a websocket error. 366 TracingUnexpectedResponseException: If the response contains an error 367 or does not contain the expected result. 368 """ 369 self._CreateTracingBackendIfNeeded() 370 return self._tracing_backend.DumpMemory(timeout) 371 372 def SetMemoryPressureNotificationsSuppressed(self, suppressed, timeout=30): 373 """Enable/disable suppressing memory pressure notifications. 374 375 Args: 376 suppressed: If true, memory pressure notifications will be suppressed. 377 timeout: The timeout in seconds. 378 379 Raises: 380 MemoryTimeoutException: If more than |timeout| seconds has passed 381 since the last time any data is received. 382 MemoryUnrecoverableException: If there is a websocket error. 383 MemoryUnexpectedResponseException: If the response contains an error 384 or does not contain the expected result. 385 """ 386 self._CreateMemoryBackendIfNeeded() 387 return self._memory_backend.SetMemoryPressureNotificationsSuppressed( 388 suppressed, timeout) 389 390 def SimulateMemoryPressureNotification(self, pressure_level, timeout=30): 391 """Simulate a memory pressure notification. 392 393 Args: 394 pressure level: The memory pressure level of the notification ('moderate' 395 or 'critical'). 396 timeout: The timeout in seconds. 397 398 Raises: 399 MemoryTimeoutException: If more than |timeout| seconds has passed 400 since the last time any data is received. 401 MemoryUnrecoverableException: If there is a websocket error. 402 MemoryUnexpectedResponseException: If the response contains an error 403 or does not contain the expected result. 404 """ 405 self._CreateMemoryBackendIfNeeded() 406 return self._memory_backend.SimulateMemoryPressureNotification( 407 pressure_level, timeout) 408 409 410class _DevToolsContextMapBackend(object): 411 def __init__(self, app_backend, devtools_client): 412 self._app_backend = app_backend 413 self._devtools_client = devtools_client 414 self._contexts = None 415 self._inspector_backends_dict = {} 416 417 @property 418 def contexts(self): 419 """The most up to date contexts data. 420 421 Returned in the order returned by devtools agent.""" 422 return self._contexts 423 424 def GetContextInfo(self, context_id): 425 for context in self._contexts: 426 if context['id'] == context_id: 427 return context 428 raise KeyError('Cannot find a context with id=%s' % context_id) 429 430 def GetInspectorBackend(self, context_id): 431 """Gets an InspectorBackend instance for the given context_id. 432 433 This lazily creates InspectorBackend for the context_id if it does 434 not exist yet. Otherwise, it will return the cached instance.""" 435 if context_id in self._inspector_backends_dict: 436 return self._inspector_backends_dict[context_id] 437 438 for context in self._contexts: 439 if context['id'] == context_id: 440 new_backend = inspector_backend.InspectorBackend( 441 self._app_backend.app, self._devtools_client, context) 442 self._inspector_backends_dict[context_id] = new_backend 443 return new_backend 444 445 raise KeyError('Cannot find a context with id=%s' % context_id) 446 447 def _Update(self, contexts): 448 # Remove InspectorBackend that is not in the current inspectable 449 # contexts list. 450 context_ids = [context['id'] for context in contexts] 451 for context_id in self._inspector_backends_dict.keys(): 452 if context_id not in context_ids: 453 backend = self._inspector_backends_dict[context_id] 454 backend.Disconnect() 455 del self._inspector_backends_dict[context_id] 456 457 valid_contexts = [] 458 for context in contexts: 459 # If the context does not have webSocketDebuggerUrl, skip it. 460 # If an InspectorBackend is already created for the tab, 461 # webSocketDebuggerUrl will be missing, and this is expected. 462 context_id = context['id'] 463 if context_id not in self._inspector_backends_dict: 464 if 'webSocketDebuggerUrl' not in context: 465 logging.debug('webSocketDebuggerUrl missing, removing %s' 466 % context_id) 467 continue 468 valid_contexts.append(context) 469 self._contexts = valid_contexts 470 471 def Clear(self): 472 for backend in self._inspector_backends_dict.values(): 473 backend.Disconnect() 474 self._inspector_backends_dict = {} 475 self._contexts = None 476