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 contextlib 6import httplib 7import json 8import logging 9import pprint 10import re 11import socket 12import sys 13import urllib2 14 15from telemetry import decorators 16from telemetry.core import exceptions 17from telemetry.core import forwarders 18from telemetry.core import user_agent 19from telemetry.core import util 20from telemetry.core import web_contents 21from telemetry.core import wpr_modes 22from telemetry.core import wpr_server 23from telemetry.core.backends import browser_backend 24from telemetry.core.backends.chrome import extension_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.timeline import tracing_timeline_data 29from telemetry.unittest import options_for_unittests 30 31 32class ChromeBrowserBackend(browser_backend.BrowserBackend): 33 """An abstract class for chrome browser backends. Provides basic functionality 34 once a remote-debugger port has been established.""" 35 # It is OK to have abstract methods. pylint: disable=W0223 36 37 def __init__(self, supports_tab_control, supports_extensions, browser_options, 38 output_profile_path, extensions_to_load): 39 super(ChromeBrowserBackend, self).__init__( 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._supports_tab_control = supports_tab_control 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('--enable-net-benchmarking') 87 args.append('--metrics-recording-only') 88 args.append('--no-default-browser-check') 89 args.append('--no-first-run') 90 91 # Turn on GPU benchmarking extension for all runs. The only side effect of 92 # the extension being on is that render stats are tracked. This is believed 93 # to be effectively free. And, by doing so here, it avoids us having to 94 # programmatically inspect a pageset's actions in order to determine if it 95 # might eventually scroll. 96 args.append('--enable-gpu-benchmarking') 97 98 # Set --no-proxy-server to work around some XP issues unless 99 # some other flag indicates a proxy is needed. 100 if not '--enable-spdy-proxy-auth' in args: 101 args.append('--no-proxy-server') 102 103 if self.browser_options.disable_background_networking: 104 args.append('--disable-background-networking') 105 106 if self.browser_options.netsim: 107 args.append('--ignore-certificate-errors') 108 elif self.browser_options.wpr_mode != wpr_modes.WPR_OFF: 109 args.extend(wpr_server.GetChromeFlags(self.forwarder_factory.host_ip, 110 self.wpr_port_pairs)) 111 args.extend(user_agent.GetChromeUserAgentArgumentFromType( 112 self.browser_options.browser_user_agent_type)) 113 114 extensions = [extension.local_path 115 for extension in self._extensions_to_load 116 if not extension.is_component] 117 extension_str = ','.join(extensions) 118 if len(extensions) > 0: 119 args.append('--load-extension=%s' % extension_str) 120 121 component_extensions = [extension.local_path 122 for extension in self._extensions_to_load 123 if extension.is_component] 124 component_extension_str = ','.join(component_extensions) 125 if len(component_extensions) > 0: 126 args.append('--load-component-extension=%s' % component_extension_str) 127 128 if self.browser_options.no_proxy_server: 129 args.append('--no-proxy-server') 130 131 if self.browser_options.disable_component_extensions_with_background_pages: 132 args.append('--disable-component-extensions-with-background-pages') 133 134 return args 135 136 def HasBrowserFinishedLaunching(self): 137 try: 138 self.Request('', timeout=.1) 139 except (exceptions.BrowserGoneException, 140 exceptions.BrowserConnectionGoneException): 141 return False 142 else: 143 return True 144 145 def _WaitForBrowserToComeUp(self, wait_for_extensions=True): 146 try: 147 util.WaitFor(self.HasBrowserFinishedLaunching, timeout=30) 148 except (util.TimeoutException, exceptions.ProcessGoneException) as e: 149 if not self.IsBrowserRunning(): 150 raise exceptions.BrowserGoneException(self.browser, e) 151 raise exceptions.BrowserConnectionGoneException(self.browser, e) 152 153 def AllExtensionsLoaded(): 154 # Extension pages are loaded from an about:blank page, 155 # so we need to check that the document URL is the extension 156 # page in addition to the ready state. 157 extension_ready_js = """ 158 document.URL.lastIndexOf('chrome-extension://%s/', 0) == 0 && 159 (document.readyState == 'complete' || 160 document.readyState == 'interactive') 161 """ 162 for e in self._extensions_to_load: 163 try: 164 extension_objects = self.extension_backend[e.extension_id] 165 except KeyError: 166 return False 167 for extension_object in extension_objects: 168 try: 169 res = extension_object.EvaluateJavaScript( 170 extension_ready_js % e.extension_id) 171 except exceptions.EvaluateException: 172 # If the inspected page is not ready, we will get an error 173 # when we evaluate a JS expression, but we can just keep polling 174 # until the page is ready (crbug.com/251913). 175 res = None 176 177 # TODO(tengs): We don't have full support for getting the Chrome 178 # version before launch, so for now we use a generic workaround to 179 # check for an extension binding bug in old versions of Chrome. 180 # See crbug.com/263162 for details. 181 if res and extension_object.EvaluateJavaScript( 182 'chrome.runtime == null'): 183 extension_object.Reload() 184 if not res: 185 return False 186 return True 187 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 ListInspectableContexts(self): 199 return json.loads(self.Request('')) 200 201 def Request(self, path, timeout=30, throw_network_exception=False): 202 url = 'http://127.0.0.1:%i/json' % self._port 203 if path: 204 url += '/' + path 205 try: 206 proxy_handler = urllib2.ProxyHandler({}) # Bypass any system proxy. 207 opener = urllib2.build_opener(proxy_handler) 208 with contextlib.closing(opener.open(url, timeout=timeout)) as req: 209 return req.read() 210 except (socket.error, httplib.BadStatusLine, urllib2.URLError) as e: 211 if throw_network_exception: 212 raise e 213 if not self.IsBrowserRunning(): 214 raise exceptions.BrowserGoneException(self.browser, e) 215 raise exceptions.BrowserConnectionGoneException(self.browser, e) 216 217 @property 218 def browser_directory(self): 219 raise NotImplementedError() 220 221 @property 222 def profile_directory(self): 223 raise NotImplementedError() 224 225 @property 226 @decorators.Cache 227 def chrome_branch_number(self): 228 # Detect version information. 229 data = self.Request('version') 230 resp = json.loads(data) 231 if 'Protocol-Version' in resp: 232 if 'Browser' in resp: 233 branch_number_match = re.search('Chrome/\d+\.\d+\.(\d+)\.\d+', 234 resp['Browser']) 235 else: 236 branch_number_match = re.search( 237 'Chrome/\d+\.\d+\.(\d+)\.\d+ (Mobile )?Safari', 238 resp['User-Agent']) 239 240 if branch_number_match: 241 branch_number = int(branch_number_match.group(1)) 242 if branch_number: 243 return branch_number 244 245 # Branch number can't be determined, so fail any branch number checks. 246 return 0 247 248 @property 249 def supports_tab_control(self): 250 return self._supports_tab_control 251 252 @property 253 def supports_tracing(self): 254 return True 255 256 def StartTracing(self, trace_options, custom_categories=None, 257 timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT): 258 """ 259 Args: 260 trace_options: An tracing_options.TracingOptions instance. 261 custom_categories: An optional string containing a list of 262 comma separated categories that will be traced 263 instead of the default category set. Example: use 264 "webkit,cc,disabled-by-default-cc.debug" to trace only 265 those three event categories. 266 """ 267 assert trace_options and trace_options.enable_chrome_trace 268 if self._tracing_backend is None: 269 self._tracing_backend = tracing_backend.TracingBackend(self._port, self) 270 return self._tracing_backend.StartTracing( 271 trace_options, custom_categories, timeout) 272 273 @property 274 def is_tracing_running(self): 275 if not self._tracing_backend: 276 return None 277 return self._tracing_backend.is_tracing_running 278 279 def StopTracing(self): 280 """ Stops tracing and returns the result as TimelineData object. """ 281 tab_ids_list = [] 282 for (i, _) in enumerate(self._browser.tabs): 283 tab = self.tab_list_backend.Get(i, None) 284 if tab: 285 success = tab.EvaluateJavaScript( 286 "console.time('" + tab.id + "');" + 287 "console.timeEnd('" + tab.id + "');" + 288 "console.time.toString().indexOf('[native code]') != -1;") 289 if not success: 290 raise Exception('Page stomped on console.time') 291 tab_ids_list.append(tab.id) 292 trace_events = self._tracing_backend.StopTracing() 293 # Augment tab_ids data to trace events. 294 event_data = {'traceEvents' : trace_events, 'tabIds': tab_ids_list} 295 return tracing_timeline_data.TracingTimelineData(event_data) 296 297 def GetProcessName(self, cmd_line): 298 """Returns a user-friendly name for the process of the given |cmd_line|.""" 299 if not cmd_line: 300 # TODO(tonyg): Eventually we should make all of these known and add an 301 # assertion. 302 return 'unknown' 303 if 'nacl_helper_bootstrap' in cmd_line: 304 return 'nacl_helper_bootstrap' 305 if ':sandboxed_process' in cmd_line: 306 return 'renderer' 307 m = re.match(r'.* --type=([^\s]*) .*', cmd_line) 308 if not m: 309 return 'browser' 310 return m.group(1) 311 312 def Close(self): 313 if self._tracing_backend: 314 self._tracing_backend.Close() 315 self._tracing_backend = None 316 317 @property 318 def supports_system_info(self): 319 return self.GetSystemInfo() != None 320 321 def GetSystemInfo(self): 322 if self._system_info_backend is None: 323 self._system_info_backend = system_info_backend.SystemInfoBackend( 324 self._port) 325 return self._system_info_backend.GetSystemInfo() 326