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