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