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 pprint 7import shlex 8import sys 9 10from telemetry.core import exceptions 11from telemetry.core import util 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 system_info_backend 16from telemetry.internal.backends.chrome import tab_list_backend 17from telemetry.internal.backends.chrome_inspector import devtools_client_backend 18from telemetry.internal.browser import user_agent 19from telemetry.internal.browser import web_contents 20from telemetry.testing import options_for_unittests 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, supports_tab_control, 29 supports_extensions, browser_options, output_profile_path, 30 extensions_to_load): 31 super(ChromeBrowserBackend, self).__init__( 32 platform_backend=platform_backend, 33 supports_extensions=supports_extensions, 34 browser_options=browser_options, 35 tab_list_backend=tab_list_backend.TabListBackend) 36 self._port = None 37 38 self._supports_tab_control = supports_tab_control 39 self._devtools_client = None 40 self._system_info_backend = None 41 42 self._output_profile_path = output_profile_path 43 self._extensions_to_load = extensions_to_load 44 45 if (self.browser_options.dont_override_profile and 46 not options_for_unittests.AreSet()): 47 sys.stderr.write('Warning: Not overriding profile. This can cause ' 48 'unexpected effects due to profile-specific settings, ' 49 'such as about:flags settings, cookies, and ' 50 'extensions.\n') 51 52 @property 53 def devtools_client(self): 54 return self._devtools_client 55 56 @property 57 @decorators.Cache 58 def extension_backend(self): 59 if not self.supports_extensions: 60 return None 61 return extension_backend.ExtensionBackendDict(self) 62 63 def _ArgsNeedProxyServer(self, args): 64 """Returns True if args for Chrome indicate the need for proxy server.""" 65 if '--enable-spdy-proxy-auth' in args: 66 return True 67 return [arg for arg in args if arg.startswith('--proxy-server=')] 68 69 def GetBrowserStartupArgs(self): 70 args = [] 71 args.extend(self.browser_options.extra_browser_args) 72 args.append('--enable-net-benchmarking') 73 args.append('--metrics-recording-only') 74 args.append('--no-default-browser-check') 75 args.append('--no-first-run') 76 77 # Turn on GPU benchmarking extension for all runs. The only side effect of 78 # the extension being on is that render stats are tracked. This is believed 79 # to be effectively free. And, by doing so here, it avoids us having to 80 # programmatically inspect a pageset's actions in order to determine if it 81 # might eventually scroll. 82 args.append('--enable-gpu-benchmarking') 83 84 # Set --no-proxy-server to work around some XP issues unless 85 # some other flag indicates a proxy is needed. 86 if not self._ArgsNeedProxyServer(args): 87 self.browser_options.no_proxy_server = True 88 89 if self.browser_options.disable_background_networking: 90 args.append('--disable-background-networking') 91 args.extend(self.GetReplayBrowserStartupArgs()) 92 args.extend(user_agent.GetChromeUserAgentArgumentFromType( 93 self.browser_options.browser_user_agent_type)) 94 95 extensions = [extension.local_path 96 for extension in self._extensions_to_load 97 if not extension.is_component] 98 extension_str = ','.join(extensions) 99 if len(extensions) > 0: 100 args.append('--load-extension=%s' % extension_str) 101 102 component_extensions = [extension.local_path 103 for extension in self._extensions_to_load 104 if extension.is_component] 105 component_extension_str = ','.join(component_extensions) 106 if len(component_extensions) > 0: 107 args.append('--load-component-extension=%s' % component_extension_str) 108 109 if self.browser_options.no_proxy_server: 110 args.append('--no-proxy-server') 111 112 if self.browser_options.disable_component_extensions_with_background_pages: 113 args.append('--disable-component-extensions-with-background-pages') 114 115 # Disables the start page, as well as other external apps that can 116 # steal focus or make measurements inconsistent. 117 if self.browser_options.disable_default_apps: 118 args.append('--disable-default-apps') 119 120 if self.browser_options.enable_logging: 121 args.append('--enable-logging') 122 args.append('--v=1') 123 return args 124 125 def GetReplayBrowserStartupArgs(self): 126 network_backend = self.platform_backend.network_controller_backend 127 if not network_backend.is_replay_active: 128 return [] 129 replay_args = [] 130 if not network_backend.is_test_ca_installed: 131 # Ignore certificate errors if the platform backend has not created 132 # and installed a root certificate. 133 replay_args.append('--ignore-certificate-errors') 134 # Force hostnames to resolve to the replay's host_ip. 135 replay_args.append('--host-resolver-rules=MAP * %s,EXCLUDE localhost' % 136 network_backend.host_ip) 137 # Force the browser to send HTTP/HTTPS requests to fixed ports if they 138 # are not the standard HTTP/HTTPS ports. 139 device_ports = network_backend.wpr_device_ports 140 if device_ports.http != 80: 141 replay_args.append('--testing-fixed-http-port=%s' % device_ports.http) 142 if device_ports.https != 443: 143 replay_args.append('--testing-fixed-https-port=%s' % device_ports.https) 144 return replay_args 145 146 def HasBrowserFinishedLaunching(self): 147 assert self._port, 'No DevTools port info available.' 148 return devtools_client_backend.IsDevToolsAgentAvailable(self._port, self) 149 150 def _InitDevtoolsClientBackend(self, remote_devtools_port=None): 151 """ Initiate the devtool client backend which allow browser connection 152 through browser' devtool. 153 154 Args: 155 remote_devtools_port: The remote devtools port, if 156 any. Otherwise assumed to be the same as self._port. 157 """ 158 assert not self._devtools_client, ( 159 'Devtool client backend cannot be init twice') 160 self._devtools_client = devtools_client_backend.DevToolsClientBackend( 161 self._port, remote_devtools_port or self._port, self) 162 163 def _WaitForBrowserToComeUp(self): 164 """ Wait for browser to come up. """ 165 try: 166 timeout = self.browser_options.browser_startup_timeout 167 util.WaitFor(self.HasBrowserFinishedLaunching, timeout=timeout) 168 except (exceptions.TimeoutException, exceptions.ProcessGoneException) as e: 169 if not self.IsBrowserRunning(): 170 raise exceptions.BrowserGoneException(self.browser, e) 171 raise exceptions.BrowserConnectionGoneException(self.browser, e) 172 173 def _WaitForExtensionsToLoad(self): 174 """ Wait for all extensions to load. 175 Be sure to check whether the browser_backend supports_extensions before 176 calling this method. 177 """ 178 assert self._supports_extensions 179 assert self._devtools_client, ( 180 'Waiting for extensions required devtool client to be initiated first') 181 try: 182 util.WaitFor(self._AllExtensionsLoaded, timeout=60) 183 except exceptions.TimeoutException: 184 logging.error('ExtensionsToLoad: ' + 185 repr([e.extension_id for e in self._extensions_to_load])) 186 logging.error('Extension list: ' + 187 pprint.pformat(self.extension_backend, indent=4)) 188 raise 189 190 def _AllExtensionsLoaded(self): 191 # Extension pages are loaded from an about:blank page, 192 # so we need to check that the document URL is the extension 193 # page in addition to the ready state. 194 extension_ready_js = """ 195 document.URL.lastIndexOf('chrome-extension://%s/', 0) == 0 && 196 (document.readyState == 'complete' || 197 document.readyState == 'interactive') 198 """ 199 for e in self._extensions_to_load: 200 try: 201 extension_objects = self.extension_backend[e.extension_id] 202 except KeyError: 203 return False 204 for extension_object in extension_objects: 205 try: 206 res = extension_object.EvaluateJavaScript( 207 extension_ready_js % e.extension_id) 208 except exceptions.EvaluateException: 209 # If the inspected page is not ready, we will get an error 210 # when we evaluate a JS expression, but we can just keep polling 211 # until the page is ready (crbug.com/251913). 212 res = None 213 214 # TODO(tengs): We don't have full support for getting the Chrome 215 # version before launch, so for now we use a generic workaround to 216 # check for an extension binding bug in old versions of Chrome. 217 # See crbug.com/263162 for details. 218 if res and extension_object.EvaluateJavaScript( 219 'chrome.runtime == null'): 220 extension_object.Reload() 221 if not res: 222 return False 223 return True 224 225 @property 226 def browser_directory(self): 227 raise NotImplementedError() 228 229 @property 230 def profile_directory(self): 231 raise NotImplementedError() 232 233 @property 234 def supports_tab_control(self): 235 return self._supports_tab_control 236 237 @property 238 def supports_tracing(self): 239 return True 240 241 def StartTracing(self, trace_options, custom_categories=None, 242 timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): 243 """ 244 Args: 245 trace_options: An tracing_options.TracingOptions instance. 246 custom_categories: An optional string containing a list of 247 comma separated categories that will be traced 248 instead of the default category set. Example: use 249 "webkit,cc,disabled-by-default-cc.debug" to trace only 250 those three event categories. 251 """ 252 return self.devtools_client.StartChromeTracing( 253 trace_options, custom_categories, timeout) 254 255 def StopTracing(self, trace_data_builder): 256 self.devtools_client.StopChromeTracing(trace_data_builder) 257 258 def GetProcessName(self, cmd_line): 259 """Returns a user-friendly name for the process of the given |cmd_line|.""" 260 if not cmd_line: 261 # TODO(tonyg): Eventually we should make all of these known and add an 262 # assertion. 263 return 'unknown' 264 if 'nacl_helper_bootstrap' in cmd_line: 265 return 'nacl_helper_bootstrap' 266 if ':sandboxed_process' in cmd_line: 267 return 'renderer' 268 if ':privileged_process' in cmd_line: 269 return 'gpu-process' 270 args = shlex.split(cmd_line) 271 types = [arg.split('=')[1] for arg in args if arg.startswith('--type=')] 272 if not types: 273 return 'browser' 274 return types[0] 275 276 def Close(self): 277 if self._devtools_client: 278 self._devtools_client.Close() 279 self._devtools_client = None 280 281 @property 282 def supports_system_info(self): 283 return self.GetSystemInfo() != None 284 285 def GetSystemInfo(self): 286 if self._system_info_backend is None: 287 self._system_info_backend = system_info_backend.SystemInfoBackend( 288 self._port) 289 return self._system_info_backend.GetSystemInfo() 290 291 @property 292 def supports_memory_dumping(self): 293 return True 294 295 def DumpMemory(self, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): 296 return self.devtools_client.DumpMemory(timeout) 297 298 @property 299 def supports_overriding_memory_pressure_notifications(self): 300 return True 301 302 def SetMemoryPressureNotificationsSuppressed( 303 self, suppressed, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): 304 self.devtools_client.SetMemoryPressureNotificationsSuppressed( 305 suppressed, timeout) 306 307 def SimulateMemoryPressureNotification( 308 self, pressure_level, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): 309 self.devtools_client.SimulateMemoryPressureNotification( 310 pressure_level, timeout) 311 312 @property 313 def supports_cpu_metrics(self): 314 return True 315 316 @property 317 def supports_memory_metrics(self): 318 return True 319 320 @property 321 def supports_power_metrics(self): 322 return True 323