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 logging 6import os 7import pprint 8import shlex 9import socket 10 11from telemetry.core import exceptions 12from telemetry import decorators 13from telemetry.internal.backends import browser_backend 14from telemetry.internal.backends.chrome import extension_backend 15from telemetry.internal.backends.chrome import tab_list_backend 16from telemetry.internal.backends.chrome_inspector import devtools_client_backend 17from telemetry.internal.backends.chrome_inspector import inspector_websocket 18from telemetry.internal.browser import web_contents 19 20import py_utils 21 22 23class ChromeBrowserBackend(browser_backend.BrowserBackend): 24 """An abstract class for chrome browser backends. Provides basic functionality 25 once a remote-debugger port has been established.""" 26 # It is OK to have abstract methods. pylint: disable=abstract-method 27 28 def __init__(self, platform_backend, browser_options, 29 browser_directory, profile_directory, 30 supports_extensions, supports_tab_control, build_dir=None): 31 """ 32 Args: 33 platform_backend: The platform_backend.PlatformBackend instance to use. 34 browser_options: The browser_options.BrowserOptions instance to use. 35 browser_directory: A string containing a path to the directory where the 36 the browser is installed. This is typically the directory containing 37 the browser executable, but not guaranteed. 38 profile_directory: A string containing a path to the directory to store 39 browser profile information in. 40 supports_extensions: A boolean indicating whether the browser supports 41 extensions. 42 supports_tab_control: A boolean indicating whether the browser supports 43 the concept of tabs. 44 build_dir: A string containing a path to the directory that the browser 45 was built in, for finding debug artifacts. Can be None if the browser 46 was not locally built, or the directory otherwise cannot be 47 determined. 48 """ 49 super(ChromeBrowserBackend, self).__init__( 50 platform_backend=platform_backend, 51 browser_options=browser_options, 52 supports_extensions=supports_extensions, 53 tab_list_backend=tab_list_backend.TabListBackend) 54 self._browser_directory = browser_directory 55 self._profile_directory = profile_directory 56 self._supports_tab_control = supports_tab_control 57 self._build_dir = build_dir 58 59 self._devtools_client = None 60 61 self._extensions_to_load = browser_options.extensions_to_load 62 if not supports_extensions and len(self._extensions_to_load) > 0: 63 raise browser_backend.ExtensionsNotSupportedException( 64 'Extensions are not supported on the selected browser') 65 66 if self.browser_options.dont_override_profile: 67 logging.warning('Not overriding profile. This can cause unexpected ' 68 'effects due to profile-specific settings, such as ' 69 'about:flags settings, cookies, and extensions.') 70 71 @property 72 def build_dir(self): 73 return self._build_dir 74 75 @property 76 def devtools_client(self): 77 return self._devtools_client 78 79 @property 80 @decorators.Cache 81 def extension_backend(self): 82 if not self.supports_extensions: 83 return None 84 return extension_backend.ExtensionBackendDict(self) 85 86 def _ArgsNeedProxyServer(self, args): 87 """Returns True if args for Chrome indicate the need for proxy server.""" 88 if '--enable-spdy-proxy-auth' in args: 89 return True 90 return [arg for arg in args if arg.startswith('--proxy-server=')] 91 92 def HasDevToolsConnection(self): 93 return self._devtools_client and self._devtools_client.IsAlive() 94 95 def _FindDevToolsPortAndTarget(self): 96 """Clients should return a (devtools_port, browser_target) pair. 97 98 May also raise EnvironmentError (IOError or OSError) if this information 99 could not be determined; the call will be retried until it succeeds or 100 a timeout is met. 101 """ 102 raise NotImplementedError 103 104 def _GetDevToolsClient(self): 105 # If the agent does not appear to be ready, it could be because we got the 106 # details of an older agent that no longer exists. It's thus important to 107 # re-read and update the port and target on each retry. 108 try: 109 devtools_port, browser_target = self._FindDevToolsPortAndTarget() 110 except EnvironmentError: 111 return None # Port information not ready, will retry. 112 113 return devtools_client_backend.GetDevToolsBackEndIfReady( 114 devtools_port=devtools_port, 115 app_backend=self, 116 browser_target=browser_target) 117 118 def BindDevToolsClient(self): 119 """Find an existing DevTools agent and bind this browser backend to it.""" 120 if self._devtools_client: 121 # In case we are launching a second browser instance (as is done by 122 # the CrOS backend), ensure that the old devtools_client is closed, 123 # otherwise re-creating it will fail. 124 self._devtools_client.Close() 125 self._devtools_client = None 126 127 try: 128 self._devtools_client = py_utils.WaitFor( 129 self._GetDevToolsClient, 130 timeout=self.browser_options.browser_startup_timeout) 131 except (py_utils.TimeoutException, exceptions.ProcessGoneException) as e: 132 if not self.IsBrowserRunning(): 133 logging.exception(e) # crbug.com/940075 134 raise exceptions.BrowserGoneException(self.browser, e) 135 raise exceptions.BrowserConnectionGoneException(self.browser, e) 136 137 def _WaitForExtensionsToLoad(self): 138 """ Wait for all extensions to load. 139 Be sure to check whether the browser_backend supports_extensions before 140 calling this method. 141 """ 142 assert self._supports_extensions 143 assert self._devtools_client, ( 144 'Waiting for extensions required devtool client to be initiated first') 145 try: 146 py_utils.WaitFor(self._AllExtensionsLoaded, timeout=60) 147 except py_utils.TimeoutException: 148 logging.error('ExtensionsToLoad: ' + repr( 149 [e.extension_id for e in self._extensions_to_load])) 150 logging.error('Extension list: ' + pprint.pformat( 151 self.extension_backend, indent=4)) 152 raise 153 154 def _AllExtensionsLoaded(self): 155 # Extension pages are loaded from an about:blank page, 156 # so we need to check that the document URL is the extension 157 # page in addition to the ready state. 158 for e in self._extensions_to_load: 159 try: 160 extension_objects = self.extension_backend[e.extension_id] 161 except KeyError: 162 return False 163 for extension_object in extension_objects: 164 try: 165 res = extension_object.EvaluateJavaScript( 166 """ 167 document.URL.lastIndexOf({{ url }}, 0) == 0 && 168 (document.readyState == 'complete' || 169 document.readyState == 'interactive') 170 """, 171 url='chrome-extension://%s/' % e.extension_id) 172 except exceptions.EvaluateException: 173 # If the inspected page is not ready, we will get an error 174 # when we evaluate a JS expression, but we can just keep polling 175 # until the page is ready (crbug.com/251913). 176 res = None 177 178 # TODO(tengs): We don't have full support for getting the Chrome 179 # version before launch, so for now we use a generic workaround to 180 # check for an extension binding bug in old versions of Chrome. 181 # See crbug.com/263162 for details. 182 if res and extension_object.EvaluateJavaScript( 183 'chrome.runtime == null'): 184 extension_object.Reload() 185 if not res: 186 return False 187 return True 188 189 @property 190 def browser_directory(self): 191 return self._browser_directory 192 193 @property 194 def profile_directory(self): 195 return self._profile_directory 196 197 @property 198 def supports_tab_control(self): 199 return self._supports_tab_control 200 201 def GetProcessName(self, cmd_line): 202 """Returns a user-friendly name for the process of the given |cmd_line|.""" 203 if not cmd_line: 204 # TODO(tonyg): Eventually we should make all of these known and add an 205 # assertion. 206 return 'unknown' 207 if 'nacl_helper_bootstrap' in cmd_line: 208 return 'nacl_helper_bootstrap' 209 if ':sandboxed_process' in cmd_line: 210 return 'renderer' 211 if ':privileged_process' in cmd_line: 212 return 'gpu-process' 213 args = shlex.split(cmd_line) 214 types = [arg.split('=')[1] for arg in args if arg.startswith('--type=')] 215 if not types: 216 return 'browser' 217 return types[0] 218 219 @staticmethod 220 def GetThreadType(thread_name): 221 if not thread_name: 222 return 'unknown' 223 if (thread_name.startswith('Chrome_ChildIO') or 224 thread_name.startswith('Chrome_IO')): 225 return 'io' 226 if thread_name.startswith('Compositor'): 227 return 'compositor' 228 if thread_name.startswith('CrGpuMain'): 229 return 'gpu' 230 if (thread_name.startswith('ChildProcessMai') or 231 thread_name.startswith('CrRendererMain')): 232 return 'main' 233 if thread_name.startswith('RenderThread'): 234 return 'render' 235 236 def Close(self): 237 # If Chrome tracing is running, flush the trace before closing the browser. 238 tracing_backend = self._platform_backend.tracing_controller_backend 239 if tracing_backend.is_chrome_tracing_running: 240 tracing_backend.FlushTracing() 241 242 if self._devtools_client: 243 if "ENSURE_CLEAN_CHROME_SHUTDOWN" in os.environ: 244 # Forces a clean shutdown by sending a command to close the browser via 245 # the devtools client. Uses a long timeout as a clean shutdown can 246 # sometime take a long time to complete. 247 self._devtools_client.CloseBrowser() 248 py_utils.WaitFor(lambda: not self.IsBrowserRunning(), 300) 249 self._devtools_client.Close() 250 self._devtools_client = None 251 252 253 def GetSystemInfo(self): 254 try: 255 return self.devtools_client.GetSystemInfo() 256 except (inspector_websocket.WebSocketException, socket.error) as e: 257 if not self.IsBrowserRunning(): 258 raise exceptions.BrowserGoneException(self.browser, e) 259 raise exceptions.BrowserConnectionGoneException(self.browser, e) 260 261 @property 262 def supports_memory_dumping(self): 263 return True 264 265 def DumpMemory(self, timeout=None, detail_level=None): 266 return self.devtools_client.DumpMemory(timeout=timeout, 267 detail_level=detail_level) 268 269 @property 270 def supports_overriding_memory_pressure_notifications(self): 271 return True 272 273 def SetMemoryPressureNotificationsSuppressed( 274 self, suppressed, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): 275 self.devtools_client.SetMemoryPressureNotificationsSuppressed( 276 suppressed, timeout) 277 278 def SimulateMemoryPressureNotification( 279 self, pressure_level, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): 280 self.devtools_client.SimulateMemoryPressureNotification( 281 pressure_level, timeout) 282 283 def GetDirectoryPathsToFlushOsPageCacheFor(self): 284 """ Return a list of directories to purge from OS page cache. 285 286 Will only be called when page cache clearing is necessary for a benchmark. 287 The caller will then attempt to purge all files from OS page cache for each 288 returned directory recursively. 289 """ 290 paths_to_flush = [] 291 if self.profile_directory: 292 paths_to_flush.append(self.profile_directory) 293 if self.browser_directory: 294 paths_to_flush.append(self.browser_directory) 295 return paths_to_flush 296 297 @property 298 def supports_cpu_metrics(self): 299 return True 300 301 @property 302 def supports_memory_metrics(self): 303 return True 304 305 def ExecuteBrowserCommand(self, command_id, timeout): 306 self.devtools_client.ExecuteBrowserCommand(command_id, timeout) 307