1 # Copyright (c) 2012 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 import logging 5 import os 6 import subprocess 7 8 from telemetry.core import exceptions 9 from telemetry.core import util 10 from telemetry.core.backends import browser_backend 11 from telemetry.core.backends.chrome import chrome_browser_backend 12 13 class CrOSBrowserBackend(chrome_browser_backend.ChromeBrowserBackend): 14 # Some developers' workflow includes running the Chrome process from 15 # /usr/local/... instead of the default location. We have to check for both 16 # paths in order to support this workflow. 17 CHROME_PATHS = ['/opt/google/chrome/chrome ', 18 '/usr/local/opt/google/chrome/chrome '] 19 20 def __init__(self, browser_type, options, cri, is_guest): 21 super(CrOSBrowserBackend, self).__init__( 22 is_content_shell=False, supports_extensions=not is_guest, 23 options=options) 24 # Initialize fields so that an explosion during init doesn't break in Close. 25 self._browser_type = browser_type 26 self._options = options 27 self._cri = cri 28 self._is_guest = is_guest 29 30 self._remote_debugging_port = self._cri.GetRemotePort() 31 self._port = self._remote_debugging_port 32 self._forwarder = None 33 34 self._login_ext_dir = os.path.join(os.path.dirname(__file__), 35 'chromeos_login_ext') 36 37 # Push a dummy login extension to the device. 38 # This extension automatically logs in as test (at] test.test 39 # Note that we also perform this copy locally to ensure that 40 # the owner of the extensions is set to chronos. 41 logging.info('Copying dummy login extension to the device') 42 cri.PushFile(self._login_ext_dir, '/tmp/') 43 self._login_ext_dir = '/tmp/chromeos_login_ext' 44 cri.RunCmdOnDevice(['chown', '-R', 'chronos:chronos', 45 self._login_ext_dir]) 46 47 # Copy extensions to temp directories on the device. 48 # Note that we also perform this copy locally to ensure that 49 # the owner of the extensions is set to chronos. 50 for e in options.extensions_to_load: 51 output = cri.RunCmdOnDevice(['mktemp', '-d', '/tmp/extension_XXXXX']) 52 extension_dir = output[0].rstrip() 53 cri.PushFile(e.path, extension_dir) 54 cri.RunCmdOnDevice(['chown', '-R', 'chronos:chronos', extension_dir]) 55 e.local_path = os.path.join(extension_dir, os.path.basename(e.path)) 56 57 # Ensure the UI is running and logged out. 58 self._RestartUI() 59 util.WaitFor(lambda: self.IsBrowserRunning(), 20) # pylint: disable=W0108 60 61 # Delete test (at] test.test's cryptohome vault (user data directory). 62 if not options.dont_override_profile: 63 logging.info('Deleting user\'s cryptohome vault (the user data dir)') 64 self._cri.RunCmdOnDevice( 65 ['cryptohome', '--action=remove', '--force', '--user=test (at] test.test']) 66 if options.profile_dir: 67 profile_dir = '/home/chronos/Default' 68 cri.RunCmdOnDevice(['rm', '-rf', profile_dir]) 69 cri.PushFile(options.profile_dir + '/Default', profile_dir) 70 cri.RunCmdOnDevice(['chown', '-R', 'chronos:chronos', profile_dir]) 71 72 def GetBrowserStartupArgs(self): 73 self.webpagereplay_remote_http_port = self._cri.GetRemotePort() 74 self.webpagereplay_remote_https_port = self._cri.GetRemotePort() 75 76 args = super(CrOSBrowserBackend, self).GetBrowserStartupArgs() 77 78 args.extend([ 79 '--enable-smooth-scrolling', 80 '--enable-threaded-compositing', 81 '--enable-per-tile-painting', 82 '--force-compositing-mode', 83 # Disables the start page, as well as other external apps that can 84 # steal focus or make measurements inconsistent. 85 '--disable-default-apps', 86 # Jump to the login screen, skipping network selection, eula, etc. 87 '--login-screen=login', 88 # Skip user image selection screen, and post login screens. 89 '--oobe-skip-postlogin', 90 # Skip hwid check, for VMs and pre-MP lab devices. 91 '--skip-hwid-check', 92 # Allow devtools to connect to chrome. 93 '--remote-debugging-port=%i' % self._remote_debugging_port, 94 # Open a maximized window. 95 '--start-maximized', 96 # Debug logging for login flake (crbug.com/263527). 97 '--vmodule=*/browser/automation/*=2,*/chromeos/net/*=2,' + 98 '*/chromeos/login/*=2']) 99 100 if not self._is_guest: 101 # This extension bypasses gaia and logs us in. 102 args.append('--auth-ext-path=%s' % self._login_ext_dir) 103 104 return args 105 106 def _GetSessionManagerPid(self, procs): 107 """Returns the pid of the session_manager process, given the list of 108 processes.""" 109 for pid, process, _ in procs: 110 if process.startswith('/sbin/session_manager '): 111 return pid 112 return None 113 114 def _GetChromeProcess(self): 115 """Locates the the main chrome browser process. 116 117 Chrome on cros is usually in /opt/google/chrome, but could be in 118 /usr/local/ for developer workflows - debug chrome is too large to fit on 119 rootfs. 120 121 Chrome spawns multiple processes for renderers. pids wrap around after they 122 are exhausted so looking for the smallest pid is not always correct. We 123 locate the session_manager's pid, and look for the chrome process that's an 124 immediate child. This is the main browser process. 125 """ 126 procs = self._cri.ListProcesses() 127 session_manager_pid = self._GetSessionManagerPid(procs) 128 if not session_manager_pid: 129 return None 130 131 # Find the chrome process that is the child of the session_manager. 132 for pid, process, ppid in procs: 133 if ppid != session_manager_pid: 134 continue 135 for path in self.CHROME_PATHS: 136 if process.startswith(path): 137 return {'pid': pid, 'path': path} 138 return None 139 140 @property 141 def pid(self): 142 result = self._GetChromeProcess() 143 if result and 'pid' in result: 144 return result['pid'] 145 return None 146 147 @property 148 def browser_directory(self): 149 result = self._GetChromeProcess() 150 if result and 'path' in result: 151 return result['path'] 152 return None 153 154 @property 155 def profile_directory(self): 156 return '/home/chronos/Default' 157 158 @property 159 def hwid(self): 160 return self._cri.RunCmdOnDevice(['/usr/bin/crossystem', 'hwid'])[0] 161 162 def GetRemotePort(self, _): 163 return self._cri.GetRemotePort() 164 165 def __del__(self): 166 self.Close() 167 168 def Start(self): 169 # Escape all commas in the startup arguments we pass to Chrome 170 # because dbus-send delimits array elements by commas 171 startup_args = [a.replace(',', '\\,') for a in self.GetBrowserStartupArgs()] 172 173 # Restart Chrome with the login extension and remote debugging. 174 logging.info('Restarting Chrome with flags and login') 175 args = ['dbus-send', '--system', '--type=method_call', 176 '--dest=org.chromium.SessionManager', 177 '/org/chromium/SessionManager', 178 'org.chromium.SessionManagerInterface.EnableChromeTesting', 179 'boolean:true', 180 'array:string:"%s"' % ','.join(startup_args)] 181 self._cri.RunCmdOnDevice(args) 182 183 if not self._cri.local: 184 # Find a free local port. 185 self._port = util.GetAvailableLocalPort() 186 187 # Forward the remote debugging port. 188 logging.info('Forwarding remote debugging port') 189 self._forwarder = SSHForwarder( 190 self._cri, 'L', 191 util.PortPair(self._port, self._remote_debugging_port)) 192 193 # Wait for the browser to come up. 194 logging.info('Waiting for browser to be ready') 195 try: 196 self._WaitForBrowserToComeUp() 197 self._PostBrowserStartupInitialization() 198 except: 199 import traceback 200 traceback.print_exc() 201 self.Close() 202 raise 203 204 # chrome_branch_number is set in _PostBrowserStartupInitialization. 205 # Without --skip-hwid-check (introduced in crrev.com/203397), devices/VMs 206 # will be stuck on the bad hwid screen. 207 if self.chrome_branch_number <= 1500 and not self.hwid: 208 raise exceptions.LoginException( 209 'Hardware id not set on device/VM. --skip-hwid-check not supported ' 210 'with chrome branches 1500 or earlier.') 211 212 if self._is_guest: 213 pid = self.pid 214 self._NavigateGuestLogin() 215 # Guest browsing shuts down the current browser and launches an incognito 216 # browser in a separate process, which we need to wait for. 217 util.WaitFor(lambda: pid != self.pid, 10) 218 self._WaitForBrowserToComeUp() 219 else: 220 self._NavigateLogin() 221 222 logging.info('Browser is up!') 223 224 def Close(self): 225 super(CrOSBrowserBackend, self).Close() 226 227 self._RestartUI() # Logs out. 228 229 if not self._cri.local: 230 if self._forwarder: 231 self._forwarder.Close() 232 self._forwarder = None 233 234 if self._login_ext_dir: 235 self._cri.RmRF(self._login_ext_dir) 236 self._login_ext_dir = None 237 238 for e in self._options.extensions_to_load: 239 self._cri.RmRF(os.path.dirname(e.local_path)) 240 241 self._cri = None 242 243 def IsBrowserRunning(self): 244 return bool(self.pid) 245 246 def GetStandardOutput(self): 247 return 'Cannot get standard output on CrOS' 248 249 def GetStackTrace(self): 250 return 'Cannot get stack trace on CrOS' 251 252 def CreateForwarder(self, *port_pairs): 253 assert self._cri 254 return (browser_backend.DoNothingForwarder(*port_pairs) if self._cri.local 255 else SSHForwarder(self._cri, 'R', *port_pairs)) 256 257 def _RestartUI(self): 258 if self._cri: 259 logging.info('(Re)starting the ui (logs the user out)') 260 if self._cri.IsServiceRunning('ui'): 261 self._cri.RunCmdOnDevice(['restart', 'ui']) 262 else: 263 self._cri.RunCmdOnDevice(['start', 'ui']) 264 265 @property 266 def oobe(self): 267 return self.misc_web_contents_backend.GetOobe() 268 269 def _SigninUIState(self): 270 """Returns the signin ui state of the oobe. HIDDEN: 0, GAIA_SIGNIN: 1, 271 ACCOUNT_PICKER: 2, WRONG_HWID_WARNING: 3, MANAGED_USER_CREATION_FLOW: 4. 272 These values are in 273 chrome/browser/resources/chromeos/login/display_manager.js 274 """ 275 return self.oobe.EvaluateJavaScript(''' 276 loginHeader = document.getElementById('login-header-bar') 277 if (loginHeader) { 278 loginHeader.signinUIState_; 279 } 280 ''') 281 282 def _IsCryptohomeMounted(self): 283 """Returns True if a cryptohome vault is mounted at /home/chronos/user.""" 284 return self._cri.FilesystemMountedAt('/home/chronos/user').startswith( 285 '/home/.shadow/') 286 287 def _HandleUserImageSelectionScreen(self): 288 """If we're stuck on the user image selection screen, we click the ok 289 button. 290 """ 291 oobe = self.oobe 292 if oobe: 293 try: 294 oobe.EvaluateJavaScript(""" 295 var ok = document.getElementById("ok-button"); 296 if (ok) { 297 ok.click(); 298 } 299 """) 300 except (exceptions.TabCrashException): 301 pass 302 303 def _IsLoggedIn(self): 304 """Returns True if we're logged in (cryptohome has mounted), and the oobe 305 has been dismissed.""" 306 if self.chrome_branch_number <= 1547: 307 self._HandleUserImageSelectionScreen() 308 return self._IsCryptohomeMounted() and not self.oobe 309 310 def _StartupWindow(self): 311 """Closes the startup window, which is an extension on official builds, 312 and a webpage on chromiumos""" 313 startup_window_ext_id = 'honijodknafkokifofgiaalefdiedpko' 314 return (self.extension_dict_backend[startup_window_ext_id] 315 if startup_window_ext_id in self.extension_dict_backend 316 else self.tab_list_backend.Get(0, None)) 317 318 def _WaitForAccountPicker(self): 319 """Waits for the oobe screen to be in the account picker state.""" 320 util.WaitFor(lambda: self._SigninUIState() == 2, 20) 321 322 def _ClickBrowseAsGuest(self): 323 """Click the Browse As Guest button on the account picker screen. This will 324 restart the browser, and we could have a tab crash or a browser crash.""" 325 try: 326 self.oobe.EvaluateJavaScript(""" 327 var guest = document.getElementById("guest-user-button"); 328 if (guest) { 329 guest.click(); 330 } 331 """) 332 except (exceptions.TabCrashException, 333 exceptions.BrowserConnectionGoneException): 334 pass 335 336 def _WaitForGuestFsMounted(self): 337 """Waits for /home/chronos/user to be mounted as guestfs""" 338 util.WaitFor(lambda: (self._cri.FilesystemMountedAt('/home/chronos/user') == 339 'guestfs'), 20) 340 341 def _NavigateGuestLogin(self): 342 """Navigates through oobe login screen as guest""" 343 assert self.oobe 344 self._WaitForAccountPicker() 345 self._ClickBrowseAsGuest() 346 self._WaitForGuestFsMounted() 347 348 def _NavigateLogin(self): 349 """Navigates through oobe login screen""" 350 # Dismiss the user image selection screen. 351 try: 352 util.WaitFor(lambda: self._IsLoggedIn(), 60) # pylint: disable=W0108 353 except util.TimeoutException: 354 self._cri.TakeScreenShot('login-screen') 355 raise exceptions.LoginException( 356 'Timed out going through oobe screen. Make sure the custom auth ' 357 'extension passed through --auth-ext-path is valid and belongs ' 358 'to user "chronos".') 359 360 if self.chrome_branch_number < 1500: 361 # Wait for the startup window, then close it. Startup window doesn't exist 362 # post-M27. crrev.com/197900 363 util.WaitFor(lambda: self._StartupWindow() is not None, 20) 364 self._StartupWindow().Close() 365 else: 366 # Open a new window/tab. 367 self.tab_list_backend.New(15) 368 369 370 class SSHForwarder(object): 371 def __init__(self, cri, forwarding_flag, *port_pairs): 372 self._proc = None 373 374 if forwarding_flag == 'R': 375 self._host_port = port_pairs[0].remote_port 376 command_line = ['-%s%i:localhost:%i' % (forwarding_flag, 377 port_pair.remote_port, 378 port_pair.local_port) 379 for port_pair in port_pairs] 380 else: 381 self._host_port = port_pairs[0].local_port 382 command_line = ['-%s%i:localhost:%i' % (forwarding_flag, 383 port_pair.local_port, 384 port_pair.remote_port) 385 for port_pair in port_pairs] 386 387 self._device_port = port_pairs[0].remote_port 388 389 self._proc = subprocess.Popen( 390 cri.FormSSHCommandLine(['sleep', '999999999'], command_line), 391 stdout=subprocess.PIPE, 392 stderr=subprocess.PIPE, 393 stdin=subprocess.PIPE, 394 shell=False) 395 396 util.WaitFor(lambda: cri.IsHTTPServerRunningOnPort(self._device_port), 60) 397 398 @property 399 def url(self): 400 assert self._proc 401 return 'http://localhost:%i' % self._host_port 402 403 def Close(self): 404 if self._proc: 405 self._proc.kill() 406 self._proc = None 407 408