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 7 8from telemetry.core import exceptions 9from telemetry.core import util 10from telemetry import decorators 11from telemetry.internal.backends.chrome import chrome_browser_backend 12from telemetry.internal.backends.chrome import misc_web_contents_backend 13from telemetry.internal import forwarders 14 15 16class CrOSBrowserBackend(chrome_browser_backend.ChromeBrowserBackend): 17 def __init__(self, cros_platform_backend, browser_options, cri, is_guest): 18 super(CrOSBrowserBackend, self).__init__( 19 cros_platform_backend, supports_tab_control=True, 20 supports_extensions=not is_guest, 21 browser_options=browser_options) 22 assert browser_options.IsCrosBrowserOptions() 23 # Initialize fields so that an explosion during init doesn't break in Close. 24 self._cri = cri 25 self._is_guest = is_guest 26 self._forwarder = None 27 self._remote_debugging_port = self._cri.GetRemotePort() 28 self._port = self._remote_debugging_port 29 30 extensions_to_load = browser_options.extensions_to_load 31 32 # Copy extensions to temp directories on the device. 33 # Note that we also perform this copy locally to ensure that 34 # the owner of the extensions is set to chronos. 35 for e in extensions_to_load: 36 extension_dir = cri.RunCmdOnDevice( 37 ['mktemp', '-d', '/tmp/extension_XXXXX'])[0].rstrip() 38 e.local_path = os.path.join(extension_dir, os.path.basename(e.path)) 39 cri.PushFile(e.path, extension_dir) 40 cri.Chown(extension_dir) 41 42 self._cri.RestartUI(self.browser_options.clear_enterprise_policy) 43 util.WaitFor(self.IsBrowserRunning, 20) 44 45 # Delete test user's cryptohome vault (user data directory). 46 if not self.browser_options.dont_override_profile: 47 self._cri.RunCmdOnDevice(['cryptohome', '--action=remove', '--force', 48 '--user=%s' % self._username]) 49 50 @property 51 def log_file_path(self): 52 return None 53 54 def GetBrowserStartupArgs(self): 55 args = super(CrOSBrowserBackend, self).GetBrowserStartupArgs() 56 57 logging_patterns = ['*/chromeos/net/*', 58 '*/chromeos/login/*', 59 '*/dbus/*', 60 'application_lifetime', 61 'chrome_browser_main_posix'] 62 vmodule = '--vmodule=' 63 for pattern in logging_patterns: 64 vmodule += '%s=2,' % pattern 65 vmodule = vmodule.rstrip(',') 66 67 args.extend([ 68 '--enable-smooth-scrolling', 69 '--enable-threaded-compositing', 70 # Allow devtools to connect to chrome. 71 '--remote-debugging-port=%i' % self._remote_debugging_port, 72 # Open a maximized window. 73 '--start-maximized', 74 # Disable system startup sound. 75 '--ash-disable-system-sounds', 76 # Ignore DMServer errors for policy fetches. 77 '--allow-failed-policy-fetch-for-test', 78 # Skip user image selection screen, and post login screens. 79 '--oobe-skip-postlogin', 80 # Debug logging. 81 vmodule]) 82 83 # Disable GAIA services unless we're using GAIA login, or if there's an 84 # explicit request for it. 85 if (self.browser_options.disable_gaia_services and 86 not self.browser_options.gaia_login): 87 args.append('--disable-gaia-services') 88 89 trace_config_file = (self.platform_backend.tracing_controller_backend 90 .GetChromeTraceConfigFile()) 91 if trace_config_file: 92 args.append('--trace-config-file=%s' % trace_config_file) 93 94 return args 95 96 @property 97 def pid(self): 98 return self._cri.GetChromePid() 99 100 @property 101 def browser_directory(self): 102 result = self._cri.GetChromeProcess() 103 if result and 'path' in result: 104 return os.path.dirname(result['path']) 105 return None 106 107 @property 108 def profile_directory(self): 109 return '/home/chronos/Default' 110 111 def __del__(self): 112 self.Close() 113 114 def Start(self): 115 # Escape all commas in the startup arguments we pass to Chrome 116 # because dbus-send delimits array elements by commas 117 startup_args = [a.replace(',', '\\,') for a in self.GetBrowserStartupArgs()] 118 119 # Restart Chrome with the login extension and remote debugging. 120 pid = self.pid 121 logging.info('Restarting Chrome (pid=%d) with remote port', pid) 122 args = ['dbus-send', '--system', '--type=method_call', 123 '--dest=org.chromium.SessionManager', 124 '/org/chromium/SessionManager', 125 'org.chromium.SessionManagerInterface.EnableChromeTesting', 126 'boolean:true', 127 'array:string:"%s"' % ','.join(startup_args)] 128 logging.info('Starting Chrome %s', args) 129 self._cri.RunCmdOnDevice(args) 130 131 if not self._cri.local: 132 # TODO(crbug.com/404771): Move port forwarding to network_controller. 133 self._port = util.GetUnreservedAvailableLocalPort() 134 self._forwarder = self._platform_backend.forwarder_factory.Create( 135 forwarders.PortPair(self._port, self._remote_debugging_port), 136 use_remote_port_forwarding=False) 137 138 # Wait for new chrome and oobe. 139 util.WaitFor(lambda: pid != self.pid, 15) 140 self._WaitForBrowserToComeUp() 141 self._InitDevtoolsClientBackend( 142 remote_devtools_port=self._remote_debugging_port) 143 util.WaitFor(lambda: self.oobe_exists, 30) 144 145 if self.browser_options.auto_login: 146 if self._is_guest: 147 pid = self.pid 148 self.oobe.NavigateGuestLogin() 149 # Guest browsing shuts down the current browser and launches an 150 # incognito browser in a separate process, which we need to wait for. 151 try: 152 # TODO(achuith): Reduce this timeout to 15 sec after crbug.com/631640 153 # is resolved. 154 util.WaitFor(lambda: pid != self.pid, 60) 155 except exceptions.TimeoutException: 156 self._RaiseOnLoginFailure( 157 'Failed to restart browser in guest mode (pid %d).' % pid) 158 159 elif self.browser_options.gaia_login: 160 self.oobe.NavigateGaiaLogin(self._username, self._password) 161 else: 162 self.oobe.NavigateFakeLogin(self._username, self._password, 163 self._gaia_id, not self.browser_options.disable_gaia_services) 164 165 try: 166 self._WaitForLogin() 167 except exceptions.TimeoutException: 168 self._RaiseOnLoginFailure('Timed out going through login screen. ' 169 + self._GetLoginStatus()) 170 171 logging.info('Browser is up!') 172 173 def Close(self): 174 super(CrOSBrowserBackend, self).Close() 175 176 if self._cri: 177 self._cri.RestartUI(False) # Logs out. 178 self._cri.CloseConnection() 179 180 util.WaitFor(lambda: not self._IsCryptohomeMounted(), 180) 181 182 if self._forwarder: 183 self._forwarder.Close() 184 self._forwarder = None 185 186 if self._cri: 187 for e in self._extensions_to_load: 188 self._cri.RmRF(os.path.dirname(e.local_path)) 189 190 self._cri = None 191 192 def IsBrowserRunning(self): 193 return bool(self.pid) 194 195 def GetStandardOutput(self): 196 return 'Cannot get standard output on CrOS' 197 198 def GetStackTrace(self): 199 return (False, 'Cannot get stack trace on CrOS') 200 201 def GetMostRecentMinidumpPath(self): 202 return None 203 204 def GetAllMinidumpPaths(self): 205 return None 206 207 def GetAllUnsymbolizedMinidumpPaths(self): 208 return None 209 210 def SymbolizeMinidump(self, minidump_path): 211 return None 212 213 @property 214 @decorators.Cache 215 def misc_web_contents_backend(self): 216 """Access to chrome://oobe/login page.""" 217 return misc_web_contents_backend.MiscWebContentsBackend(self) 218 219 @property 220 def oobe(self): 221 return self.misc_web_contents_backend.GetOobe() 222 223 @property 224 def oobe_exists(self): 225 return self.misc_web_contents_backend.oobe_exists 226 227 @property 228 def _username(self): 229 return self.browser_options.username 230 231 @property 232 def _password(self): 233 return self.browser_options.password 234 235 @property 236 def _gaia_id(self): 237 return self.browser_options.gaia_id 238 239 def _IsCryptohomeMounted(self): 240 username = '$guest' if self._is_guest else self._username 241 return self._cri.IsCryptohomeMounted(username, self._is_guest) 242 243 def _GetLoginStatus(self): 244 """Returns login status. If logged in, empty string is returned.""" 245 status = '' 246 if not self._IsCryptohomeMounted(): 247 status += 'Cryptohome not mounted. ' 248 if not self.HasBrowserFinishedLaunching(): 249 status += 'Browser didn\'t launch. ' 250 if self.oobe_exists: 251 status += 'OOBE not dismissed.' 252 return status 253 254 def _IsLoggedIn(self): 255 """Returns True if cryptohome has mounted, the browser is 256 responsive to devtools requests, and the oobe has been dismissed.""" 257 return not self._GetLoginStatus() 258 259 def _WaitForLogin(self): 260 # Wait for cryptohome to mount. 261 util.WaitFor(self._IsLoggedIn, 60) 262 263 # For incognito mode, the session manager actually relaunches chrome with 264 # new arguments, so we have to wait for the browser to come up. 265 self._WaitForBrowserToComeUp() 266 267 # Wait for extensions to load. 268 if self._supports_extensions: 269 self._WaitForExtensionsToLoad() 270 271 def _RaiseOnLoginFailure(self, error): 272 if self._platform_backend.CanTakeScreenshot(): 273 self._cri.TakeScreenshotWithPrefix('login-screen') 274 raise exceptions.LoginException(error) 275