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 extensions_to_load): 19 super(CrOSBrowserBackend, self).__init__( 20 cros_platform_backend, supports_tab_control=True, 21 supports_extensions=not is_guest, 22 browser_options=browser_options, 23 output_profile_path=None, extensions_to_load=extensions_to_load) 24 assert browser_options.IsCrosBrowserOptions() 25 # Initialize fields so that an explosion during init doesn't break in Close. 26 self._cri = cri 27 self._is_guest = is_guest 28 self._forwarder = None 29 self._remote_debugging_port = self._cri.GetRemotePort() 30 self._port = self._remote_debugging_port 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 args.extend([ 57 '--enable-smooth-scrolling', 58 '--enable-threaded-compositing', 59 # Allow devtools to connect to chrome. 60 '--remote-debugging-port=%i' % self._remote_debugging_port, 61 # Open a maximized window. 62 '--start-maximized', 63 # Disable system startup sound. 64 '--ash-disable-system-sounds', 65 # Ignore DMServer errors for policy fetches. 66 '--allow-failed-policy-fetch-for-test', 67 # Skip user image selection screen, and post login screens. 68 '--oobe-skip-postlogin', 69 # Debug logging. 70 '--vmodule=*/chromeos/net/*=2,*/chromeos/login/*=2']) 71 72 # Disable GAIA services unless we're using GAIA login, or if there's an 73 # explicit request for it. 74 if (self.browser_options.disable_gaia_services and 75 not self.browser_options.gaia_login): 76 args.append('--disable-gaia-services') 77 78 return args 79 80 @property 81 def pid(self): 82 return self._cri.GetChromePid() 83 84 @property 85 def browser_directory(self): 86 result = self._cri.GetChromeProcess() 87 if result and 'path' in result: 88 return os.path.dirname(result['path']) 89 return None 90 91 @property 92 def profile_directory(self): 93 return '/home/chronos/Default' 94 95 def __del__(self): 96 self.Close() 97 98 def Start(self): 99 # Escape all commas in the startup arguments we pass to Chrome 100 # because dbus-send delimits array elements by commas 101 startup_args = [a.replace(',', '\\,') for a in self.GetBrowserStartupArgs()] 102 103 # Restart Chrome with the login extension and remote debugging. 104 logging.info('Restarting Chrome with flags and login') 105 args = ['dbus-send', '--system', '--type=method_call', 106 '--dest=org.chromium.SessionManager', 107 '/org/chromium/SessionManager', 108 'org.chromium.SessionManagerInterface.EnableChromeTesting', 109 'boolean:true', 110 'array:string:"%s"' % ','.join(startup_args)] 111 self._cri.RunCmdOnDevice(args) 112 113 if not self._cri.local: 114 # TODO(crbug.com/404771): Move port forwarding to network_controller. 115 self._port = util.GetUnreservedAvailableLocalPort() 116 self._forwarder = self._platform_backend.forwarder_factory.Create( 117 forwarders.PortPairs( 118 http=forwarders.PortPair(self._port, self._remote_debugging_port), 119 https=None, 120 dns=None), use_remote_port_forwarding=False) 121 122 # Wait for oobe. 123 self._WaitForBrowserToComeUp() 124 self._InitDevtoolsClientBackend( 125 remote_devtools_port=self._remote_debugging_port) 126 util.WaitFor(lambda: self.oobe_exists, 10) 127 128 if self.browser_options.auto_login: 129 try: 130 if self._is_guest: 131 pid = self.pid 132 self.oobe.NavigateGuestLogin() 133 # Guest browsing shuts down the current browser and launches an 134 # incognito browser in a separate process, which we need to wait for. 135 util.WaitFor(lambda: pid != self.pid, 10) 136 elif self.browser_options.gaia_login: 137 self.oobe.NavigateGaiaLogin(self._username, self._password) 138 else: 139 self.oobe.NavigateFakeLogin(self._username, self._password, 140 self._gaia_id) 141 142 self._WaitForLogin() 143 except exceptions.TimeoutException: 144 self._cri.TakeScreenShot('login-screen') 145 raise exceptions.LoginException('Timed out going through login screen. ' 146 + self._GetLoginStatus()) 147 148 logging.info('Browser is up!') 149 150 def Close(self): 151 super(CrOSBrowserBackend, self).Close() 152 153 if self._cri: 154 self._cri.RestartUI(False) # Logs out. 155 self._cri.CloseConnection() 156 157 util.WaitFor(lambda: not self._IsCryptohomeMounted(), 180) 158 159 if self._forwarder: 160 self._forwarder.Close() 161 self._forwarder = None 162 163 if self._cri: 164 for e in self._extensions_to_load: 165 self._cri.RmRF(os.path.dirname(e.local_path)) 166 167 self._cri = None 168 169 def IsBrowserRunning(self): 170 return bool(self.pid) 171 172 def GetStandardOutput(self): 173 return 'Cannot get standard output on CrOS' 174 175 def GetStackTrace(self): 176 return 'Cannot get stack trace on CrOS' 177 178 @property 179 @decorators.Cache 180 def misc_web_contents_backend(self): 181 """Access to chrome://oobe/login page.""" 182 return misc_web_contents_backend.MiscWebContentsBackend(self) 183 184 @property 185 def oobe(self): 186 return self.misc_web_contents_backend.GetOobe() 187 188 @property 189 def oobe_exists(self): 190 return self.misc_web_contents_backend.oobe_exists 191 192 @property 193 def _username(self): 194 return self.browser_options.username 195 196 @property 197 def _password(self): 198 return self.browser_options.password 199 200 @property 201 def _gaia_id(self): 202 return self.browser_options.gaia_id 203 204 def _IsCryptohomeMounted(self): 205 username = '$guest' if self._is_guest else self._username 206 return self._cri.IsCryptohomeMounted(username, self._is_guest) 207 208 def _GetLoginStatus(self): 209 """Returns login status. If logged in, empty string is returned.""" 210 status = '' 211 if not self._IsCryptohomeMounted(): 212 status += 'Cryptohome not mounted. ' 213 if not self.HasBrowserFinishedLaunching(): 214 status += 'Browser didn\'t launch. ' 215 if self.oobe_exists: 216 status += 'OOBE not dismissed.' 217 return status 218 219 def _IsLoggedIn(self): 220 """Returns True if cryptohome has mounted, the browser is 221 responsive to devtools requests, and the oobe has been dismissed.""" 222 return not self._GetLoginStatus() 223 224 def _WaitForLogin(self): 225 # Wait for cryptohome to mount. 226 util.WaitFor(self._IsLoggedIn, 60) 227 228 # For incognito mode, the session manager actually relaunches chrome with 229 # new arguments, so we have to wait for the browser to come up. 230 self._WaitForBrowserToComeUp() 231 232 # Wait for extensions to load. 233 if self._supports_extensions: 234 self._WaitForExtensionsToLoad() 235