chrome_browser_backend.py revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
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 httplib 6import json 7import logging 8import pprint 9import re 10import socket 11import sys 12import urllib2 13 14from telemetry import decorators 15from telemetry.core import exceptions 16from telemetry.core import forwarders 17from telemetry.core import user_agent 18from telemetry.core import util 19from telemetry.core import web_contents 20from telemetry.core import wpr_modes 21from telemetry.core import wpr_server 22from telemetry.core.backends import browser_backend 23from telemetry.core.backends.chrome import extension_backend 24from telemetry.core.backends.chrome import misc_web_contents_backend 25from telemetry.core.backends.chrome import system_info_backend 26from telemetry.core.backends.chrome import tab_list_backend 27from telemetry.core.backends.chrome import tracing_backend 28from telemetry.unittest import options_for_unittests 29 30 31class ChromeBrowserBackend(browser_backend.BrowserBackend): 32 """An abstract class for chrome browser backends. Provides basic functionality 33 once a remote-debugger port has been established.""" 34 # It is OK to have abstract methods. pylint: disable=W0223 35 36 def __init__(self, is_content_shell, supports_extensions, browser_options, 37 output_profile_path, extensions_to_load): 38 super(ChromeBrowserBackend, self).__init__( 39 is_content_shell=is_content_shell, 40 supports_extensions=supports_extensions, 41 browser_options=browser_options, 42 tab_list_backend=tab_list_backend.TabListBackend) 43 self._port = None 44 45 self._inspector_protocol_version = 0 46 self._chrome_branch_number = None 47 self._tracing_backend = None 48 self._system_info_backend = None 49 50 self._output_profile_path = output_profile_path 51 self._extensions_to_load = extensions_to_load 52 53 if browser_options.netsim: 54 self.wpr_port_pairs = forwarders.PortPairs( 55 http=forwarders.PortPair(80, 80), 56 https=forwarders.PortPair(443, 443), 57 dns=forwarders.PortPair(53, 53)) 58 else: 59 self.wpr_port_pairs = forwarders.PortPairs( 60 http=forwarders.PortPair(0, 0), 61 https=forwarders.PortPair(0, 0), 62 dns=None) 63 64 if (self.browser_options.dont_override_profile and 65 not options_for_unittests.AreSet()): 66 sys.stderr.write('Warning: Not overriding profile. This can cause ' 67 'unexpected effects due to profile-specific settings, ' 68 'such as about:flags settings, cookies, and ' 69 'extensions.\n') 70 71 def AddReplayServerOptions(self, extra_wpr_args): 72 if self.browser_options.netsim: 73 extra_wpr_args.append('--net=%s' % self.browser_options.netsim) 74 else: 75 extra_wpr_args.append('--no-dns_forwarding') 76 77 @property 78 @decorators.Cache 79 def misc_web_contents_backend(self): 80 """Access to chrome://oobe/login page.""" 81 return misc_web_contents_backend.MiscWebContentsBackend(self) 82 83 @property 84 @decorators.Cache 85 def extension_backend(self): 86 if not self.supports_extensions: 87 return None 88 return extension_backend.ExtensionBackendDict(self) 89 90 def GetBrowserStartupArgs(self): 91 args = [] 92 args.extend(self.browser_options.extra_browser_args) 93 args.append('--disable-background-networking') 94 args.append('--enable-net-benchmarking') 95 args.append('--metrics-recording-only') 96 args.append('--no-default-browser-check') 97 args.append('--no-first-run') 98 99 # Turn on GPU benchmarking extension for all runs. The only side effect of 100 # the extension being on is that render stats are tracked. This is believed 101 # to be effectively free. And, by doing so here, it avoids us having to 102 # programmatically inspect a pageset's actions in order to determine if it 103 # might eventually scroll. 104 args.append('--enable-gpu-benchmarking') 105 106 # Set --no-proxy-server to work around some XP issues unless 107 # some other flag indicates a proxy is needed. 108 if not '--enable-spdy-proxy-auth' in args: 109 args.append('--no-proxy-server') 110 111 if self.browser_options.netsim: 112 args.append('--ignore-certificate-errors') 113 elif self.browser_options.wpr_mode != wpr_modes.WPR_OFF: 114 args.extend(wpr_server.GetChromeFlags(self.forwarder_factory.host_ip, 115 self.wpr_port_pairs)) 116 args.extend(user_agent.GetChromeUserAgentArgumentFromType( 117 self.browser_options.browser_user_agent_type)) 118 119 extensions = [extension.local_path 120 for extension in self._extensions_to_load 121 if not extension.is_component] 122 extension_str = ','.join(extensions) 123 if len(extensions) > 0: 124 args.append('--load-extension=%s' % extension_str) 125 126 component_extensions = [extension.local_path 127 for extension in self._extensions_to_load 128 if extension.is_component] 129 component_extension_str = ','.join(component_extensions) 130 if len(component_extensions) > 0: 131 args.append('--load-component-extension=%s' % component_extension_str) 132 133 if self.browser_options.no_proxy_server: 134 args.append('--no-proxy-server') 135 136 if self.browser_options.disable_component_extensions_with_background_pages: 137 args.append('--disable-component-extensions-with-background-pages') 138 139 return args 140 141 def HasBrowserFinishedLaunching(self): 142 try: 143 self.Request('') 144 except (exceptions.BrowserGoneException, 145 exceptions.BrowserConnectionGoneException): 146 return False 147 else: 148 return True 149 150 def _WaitForBrowserToComeUp(self, wait_for_extensions=True): 151 try: 152 util.WaitFor(self.HasBrowserFinishedLaunching, timeout=30) 153 except (util.TimeoutException, exceptions.ProcessGoneException) as e: 154 raise exceptions.BrowserGoneException(self.GetStackTrace()) 155 156 def AllExtensionsLoaded(): 157 # Extension pages are loaded from an about:blank page, 158 # so we need to check that the document URL is the extension 159 # page in addition to the ready state. 160 extension_ready_js = """ 161 document.URL.lastIndexOf('chrome-extension://%s/', 0) == 0 && 162 (document.readyState == 'complete' || 163 document.readyState == 'interactive') 164 """ 165 for e in self._extensions_to_load: 166 if not e.extension_id in self.extension_backend: 167 return False 168 extension_object = self.extension_backend[e.extension_id] 169 try: 170 res = extension_object.EvaluateJavaScript( 171 extension_ready_js % 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 if wait_for_extensions and self._supports_extensions: 189 try: 190 util.WaitFor(AllExtensionsLoaded, timeout=60) 191 except util.TimeoutException: 192 logging.error('ExtensionsToLoad: ' + 193 repr([e.extension_id for e in self._extensions_to_load])) 194 logging.error('Extension list: ' + 195 pprint.pformat(self._extension_backend, indent=4)) 196 raise 197 198 def _PostBrowserStartupInitialization(self): 199 # Detect version information. 200 data = self.Request('version') 201 resp = json.loads(data) 202 if 'Protocol-Version' in resp: 203 self._inspector_protocol_version = resp['Protocol-Version'] 204 205 if self._chrome_branch_number: 206 return 207 208 if 'Browser' in resp: 209 branch_number_match = re.search('Chrome/\d+\.\d+\.(\d+)\.\d+', 210 resp['Browser']) 211 else: 212 branch_number_match = re.search( 213 'Chrome/\d+\.\d+\.(\d+)\.\d+ (Mobile )?Safari', 214 resp['User-Agent']) 215 216 if branch_number_match: 217 self._chrome_branch_number = int(branch_number_match.group(1)) 218 219 if not self._chrome_branch_number: 220 # Content Shell returns '' for Browser, WebViewShell returns '0'. 221 # For now we have to fall-back and assume branch 1025. 222 self._chrome_branch_number = 1025 223 return 224 225 # Detection has failed: assume 18.0.1025.168 ~= Chrome Android. 226 self._inspector_protocol_version = 1.0 227 self._chrome_branch_number = 1025 228 229 def ListInspectableContexts(self): 230 return json.loads(self.Request('')) 231 232 def Request(self, path, timeout=None, throw_network_exception=False): 233 url = 'http://127.0.0.1:%i/json' % self._port 234 if path: 235 url += '/' + path 236 try: 237 proxy_handler = urllib2.ProxyHandler({}) # Bypass any system proxy. 238 opener = urllib2.build_opener(proxy_handler) 239 req = opener.open(url, timeout=timeout) 240 return req.read() 241 except (socket.error, httplib.BadStatusLine, urllib2.URLError) as e: 242 if throw_network_exception: 243 raise e 244 if not self.IsBrowserRunning(): 245 raise exceptions.BrowserGoneException(e) 246 raise exceptions.BrowserConnectionGoneException(e) 247 248 @property 249 def browser_directory(self): 250 raise NotImplementedError() 251 252 @property 253 def profile_directory(self): 254 raise NotImplementedError() 255 256 @property 257 def chrome_branch_number(self): 258 assert self._chrome_branch_number 259 return self._chrome_branch_number 260 261 @property 262 def supports_tab_control(self): 263 return self.chrome_branch_number >= 1303 264 265 @property 266 def supports_tracing(self): 267 return self.is_content_shell or self.chrome_branch_number >= 1385 268 269 def StartTracing(self, custom_categories=None, 270 timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): 271 """ custom_categories is an optional string containing a list of 272 comma separated categories that will be traced instead of the 273 default category set. Example: use 274 "webkit,cc,disabled-by-default-cc.debug" to trace only those three 275 event categories. 276 """ 277 if self._tracing_backend is None: 278 self._tracing_backend = tracing_backend.TracingBackend(self._port) 279 return self._tracing_backend.StartTracing(custom_categories, timeout) 280 281 @property 282 def is_tracing_running(self): 283 if not self._tracing_backend: 284 return None 285 return self._tracing_backend.is_tracing_running 286 287 def StopTracing(self): 288 """ Stops tracing and returns the result as TimelineData object. """ 289 for (i, debugger_url) in enumerate(self._browser.tabs): 290 tab = self.tab_list_backend.Get(i, None) 291 if tab: 292 success = tab.EvaluateJavaScript( 293 "console.time('" + debugger_url + "');" + 294 "console.timeEnd('" + debugger_url + "');" + 295 "console.time.toString().indexOf('[native code]') != -1;") 296 if not success: 297 raise Exception('Page stomped on console.time') 298 self._tracing_backend.AddTabToMarkerMapping(tab, debugger_url) 299 return self._tracing_backend.StopTracing() 300 301 def GetProcessName(self, cmd_line): 302 """Returns a user-friendly name for the process of the given |cmd_line|.""" 303 if not cmd_line: 304 # TODO(tonyg): Eventually we should make all of these known and add an 305 # assertion. 306 return 'unknown' 307 if 'nacl_helper_bootstrap' in cmd_line: 308 return 'nacl_helper_bootstrap' 309 if ':sandboxed_process' in cmd_line: 310 return 'renderer' 311 m = re.match(r'.* --type=([^\s]*) .*', cmd_line) 312 if not m: 313 return 'browser' 314 return m.group(1) 315 316 def Close(self): 317 if self._tracing_backend: 318 self._tracing_backend.Close() 319 self._tracing_backend = None 320 321 @property 322 def supports_system_info(self): 323 return self.GetSystemInfo() != None 324 325 def GetSystemInfo(self): 326 if self._system_info_backend is None: 327 self._system_info_backend = system_info_backend.SystemInfoBackend( 328 self._port) 329 return self._system_info_backend.GetSystemInfo() 330 331 def _SetBranchNumber(self, version): 332 assert version 333 self._chrome_branch_number = re.search(r'\d+\.\d+\.(\d+)\.\d+', 334 version).group(1) 335