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.
8 from telemetry import decorators
10 from telemetry.core import exceptions
11 from telemetry.core import forwarders
12 from telemetry.core import util
13 from telemetry.core.backends.chrome import chrome_browser_backend
14 from telemetry.core.forwarders import cros_forwarder
17 class CrOSBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
18 # Some developers' workflow includes running the Chrome process from
19 # /usr/local/... instead of the default location. We have to check for both
20 # paths in order to support this workflow.
21 CHROME_PATHS = ['/opt/google/chrome/chrome ',
22 '/usr/local/opt/google/chrome/chrome ']
24 def __init__(self, browser_type, browser_options, cri, is_guest,
26 super(CrOSBrowserBackend, self).__init__(
27 is_content_shell=False, supports_extensions=not is_guest,
28 browser_options=browser_options,
29 output_profile_path=None, extensions_to_load=extensions_to_load)
31 from telemetry.core.backends.chrome import chrome_browser_options
32 assert isinstance(browser_options,
33 chrome_browser_options.CrosBrowserOptions)
35 # Initialize fields so that an explosion during init doesn't break in Close.
36 self._browser_type = browser_type
38 self._is_guest = is_guest
39 self._forwarder = None
41 self.wpr_port_pairs = forwarders.PortPairs(
42 http=forwarders.PortPair(self.wpr_port_pairs.http.local_port,
43 self._cri.GetRemotePort()),
44 https=forwarders.PortPair(self.wpr_port_pairs.https.local_port,
45 self._cri.GetRemotePort()),
47 self._remote_debugging_port = self._cri.GetRemotePort()
48 self._port = self._remote_debugging_port
50 self._SetBranchNumber(self._GetChromeVersion())
52 self._login_ext_dir = None
53 if not self._use_oobe_login_for_testing:
54 self._login_ext_dir = os.path.join(os.path.dirname(__file__),
57 # Push a dummy login extension to the device.
58 # This extension automatically logs in test user specified by
59 # self.browser_options.username.
60 # Note that we also perform this copy locally to ensure that
61 # the owner of the extensions is set to chronos.
62 logging.info('Copying dummy login extension to the device')
63 cri.PushFile(self._login_ext_dir, '/tmp/')
64 self._login_ext_dir = '/tmp/chromeos_login_ext'
65 cri.Chown(self._login_ext_dir)
67 # Copy extensions to temp directories on the device.
68 # Note that we also perform this copy locally to ensure that
69 # the owner of the extensions is set to chronos.
70 for e in extensions_to_load:
71 output = cri.RunCmdOnDevice(['mktemp', '-d', '/tmp/extension_XXXXX'])
72 extension_dir = output[0].rstrip()
73 cri.PushFile(e.path, extension_dir)
74 cri.Chown(extension_dir)
75 e.local_path = os.path.join(extension_dir, os.path.basename(e.path))
77 # Ensure the UI is running and logged out.
79 util.WaitFor(self.IsBrowserRunning, 20)
81 # Delete test user's cryptohome vault (user data directory).
82 if not self.browser_options.dont_override_profile:
83 self._cri.RunCmdOnDevice(['cryptohome', '--action=remove', '--force',
84 '--user=%s' % self.browser_options.username])
85 if self.browser_options.profile_dir:
86 cri.RmRF(self.profile_directory)
87 cri.PushFile(self.browser_options.profile_dir + '/Default',
88 self.profile_directory)
89 cri.Chown(self.profile_directory)
91 def GetBrowserStartupArgs(self):
92 args = super(CrOSBrowserBackend, self).GetBrowserStartupArgs()
94 '--enable-smooth-scrolling',
95 '--enable-threaded-compositing',
96 '--enable-per-tile-painting',
97 '--force-compositing-mode',
98 # Disables the start page, as well as other external apps that can
99 # steal focus or make measurements inconsistent.
100 '--disable-default-apps',
101 # Skip user image selection screen, and post login screens.
102 '--oobe-skip-postlogin',
103 # Allow devtools to connect to chrome.
104 '--remote-debugging-port=%i' % self._remote_debugging_port,
105 # Open a maximized window.
107 # TODO(achuith): Re-enable this flag again before multi-profiles
108 # will become enabled by default to have telemetry mileage on it.
109 # '--multi-profiles',
110 # Debug logging for login flake (crbug.com/263527).
111 '--vmodule=*/browser/automation/*=2,*/chromeos/net/*=2,'
112 '*/chromeos/login/*=2,*/extensions/*=2,'
113 '*/device_policy_decoder_chromeos.cc=2'])
117 # Jump to the login screen, skipping network selection, eula, etc.
118 '--login-screen=login',
119 # Skip hwid check, for VMs and pre-MP lab devices.
122 elif not self._use_oobe_login_for_testing:
123 # This extension bypasses gaia and logs us in.
124 logging.info('Using --auth-ext-path=%s to login', self._login_ext_dir)
125 args.append('--auth-ext-path=%s' % self._login_ext_dir)
129 def _GetSessionManagerPid(self, procs):
130 """Returns the pid of the session_manager process, given the list of
132 for pid, process, _, _ in procs:
133 if process.startswith('/sbin/session_manager '):
137 def _GetChromeProcess(self):
138 """Locates the the main chrome browser process.
140 Chrome on cros is usually in /opt/google/chrome, but could be in
141 /usr/local/ for developer workflows - debug chrome is too large to fit on
144 Chrome spawns multiple processes for renderers. pids wrap around after they
145 are exhausted so looking for the smallest pid is not always correct. We
146 locate the session_manager's pid, and look for the chrome process that's an
147 immediate child. This is the main browser process.
149 procs = self._cri.ListProcesses()
150 session_manager_pid = self._GetSessionManagerPid(procs)
151 if not session_manager_pid:
154 # Find the chrome process that is the child of the session_manager.
155 for pid, process, ppid, _ in procs:
156 if ppid != session_manager_pid:
158 for path in self.CHROME_PATHS:
159 if process.startswith(path):
160 return {'pid': pid, 'path': path, 'args': process}
163 def _GetChromeVersion(self):
164 result = util.WaitFor(self._GetChromeProcess, timeout=30)
165 assert result and result['path']
166 (version, _) = self._cri.RunCmdOnDevice([result['path'], '--version'])
172 result = self._GetChromeProcess()
173 if result and 'pid' in result:
178 def browser_directory(self):
179 result = self._GetChromeProcess()
180 if result and 'path' in result:
181 return os.path.dirname(result['path'])
185 def profile_directory(self):
186 return '/home/chronos/Default'
190 return self._cri.RunCmdOnDevice(['/usr/bin/crossystem', 'hwid'])[0]
193 def _use_oobe_login_for_testing(self):
194 """Oobe.LoginForTesting was introduced after branch 1599."""
195 return self.chrome_branch_number > 1599
197 def GetRemotePort(self, port):
200 return self._cri.GetRemotePort()
206 # Escape all commas in the startup arguments we pass to Chrome
207 # because dbus-send delimits array elements by commas
208 startup_args = [a.replace(',', '\\,') for a in self.GetBrowserStartupArgs()]
210 # Restart Chrome with the login extension and remote debugging.
211 logging.info('Restarting Chrome with flags and login')
212 args = ['dbus-send', '--system', '--type=method_call',
213 '--dest=org.chromium.SessionManager',
214 '/org/chromium/SessionManager',
215 'org.chromium.SessionManagerInterface.EnableChromeTesting',
217 'array:string:"%s"' % ','.join(startup_args)]
218 self._cri.RunCmdOnDevice(args)
220 if not self._cri.local:
221 self._port = util.GetUnreservedAvailableLocalPort()
222 self._forwarder = self.forwarder_factory.Create(
223 forwarders.PortPairs(
224 http=forwarders.PortPair(self._port, self._remote_debugging_port),
226 dns=None), forwarding_flag='L')
229 self._WaitForBrowserToComeUp(wait_for_extensions=False)
230 self._PostBrowserStartupInitialization()
233 traceback.print_exc()
237 # chrome_branch_number is set in _PostBrowserStartupInitialization.
238 # Without --skip-hwid-check (introduced in crrev.com/203397), devices/VMs
239 # will be stuck on the bad hwid screen.
240 if self.chrome_branch_number <= 1500 and not self.hwid:
241 raise exceptions.LoginException(
242 'Hardware id not set on device/VM. --skip-hwid-check not supported '
243 'with chrome branches 1500 or earlier.')
245 util.WaitFor(lambda: self.oobe_exists, 10)
247 if self.browser_options.auto_login:
250 self._NavigateGuestLogin()
251 # Guest browsing shuts down the current browser and launches an
252 # incognito browser in a separate process, which we need to wait for.
253 util.WaitFor(lambda: pid != self.pid, 10)
254 self._WaitForBrowserToComeUp()
256 self._NavigateLogin()
258 logging.info('Browser is up!')
261 super(CrOSBrowserBackend, self).Close()
263 self._RestartUI() # Logs out.
266 self._forwarder.Close()
267 self._forwarder = None
269 if self._login_ext_dir:
270 self._cri.RmRF(self._login_ext_dir)
271 self._login_ext_dir = None
273 for e in self._extensions_to_load:
274 self._cri.RmRF(os.path.dirname(e.local_path))
280 def forwarder_factory(self):
281 return cros_forwarder.CrOsForwarderFactory(self._cri)
283 def IsBrowserRunning(self):
284 return bool(self.pid)
286 def GetStandardOutput(self):
287 return 'Cannot get standard output on CrOS'
289 def GetStackTrace(self):
290 return 'Cannot get stack trace on CrOS'
292 def _RestartUI(self):
294 logging.info('(Re)starting the ui (logs the user out)')
295 if self._cri.IsServiceRunning('ui'):
296 self._cri.RunCmdOnDevice(['restart', 'ui'])
298 self._cri.RunCmdOnDevice(['start', 'ui'])
302 return self.misc_web_contents_backend.GetOobe()
305 def oobe_exists(self):
306 return self.misc_web_contents_backend.oobe_exists
308 def _SigninUIState(self):
309 """Returns the signin ui state of the oobe. HIDDEN: 0, GAIA_SIGNIN: 1,
310 ACCOUNT_PICKER: 2, WRONG_HWID_WARNING: 3, MANAGED_USER_CREATION_FLOW: 4.
312 chrome/browser/resources/chromeos/login/display_manager.js
314 return self.oobe.EvaluateJavaScript('''
315 loginHeader = document.getElementById('login-header-bar')
317 loginHeader.signinUIState_;
321 def _CryptohomePath(self, user):
322 (path, _) = self._cri.RunCmdOnDevice(['cryptohome-path', 'user',
326 def _IsCryptohomeMounted(self):
327 """Returns True if a cryptohome vault at the user mount point."""
328 profile_path = self._CryptohomePath(self.browser_options.username)
329 mount = self._cri.FilesystemMountedAt(profile_path)
330 return mount and mount.startswith('/home/.shadow/')
332 def _HandleUserImageSelectionScreen(self):
333 """If we're stuck on the user image selection screen, we click the ok
339 oobe.EvaluateJavaScript("""
340 var ok = document.getElementById("ok-button");
345 except (exceptions.TabCrashException):
348 def _IsLoggedIn(self):
349 """Returns True if we're logged in (cryptohome has mounted), and the oobe
350 has been dismissed."""
351 if self.chrome_branch_number <= 1547:
352 self._HandleUserImageSelectionScreen()
353 return self._IsCryptohomeMounted() and not self.oobe_exists
355 def _StartupWindow(self):
356 """Closes the startup window, which is an extension on official builds,
357 and a webpage on chromiumos"""
358 startup_window_ext_id = 'honijodknafkokifofgiaalefdiedpko'
359 return (self.extension_dict_backend[startup_window_ext_id]
360 if startup_window_ext_id in self.extension_dict_backend
361 else self.tab_list_backend.Get(0, None))
363 def _WaitForSigninScreen(self):
364 """Waits for oobe to be on the signin or account picker screen."""
365 def OnAccountPickerScreen():
366 signin_state = self._SigninUIState()
367 # GAIA_SIGNIN or ACCOUNT_PICKER screens.
368 return signin_state == 1 or signin_state == 2
370 util.WaitFor(OnAccountPickerScreen, 60)
371 except util.TimeoutException:
372 self._cri.TakeScreenShot('guest-screen')
373 raise exceptions.LoginException('Timed out waiting for signin screen, '
374 'signin state %d' % self._SigninUIState())
376 def _ClickBrowseAsGuest(self):
377 """Click the Browse As Guest button on the account picker screen. This will
378 restart the browser, and we could have a tab crash or a browser crash."""
380 self.oobe.EvaluateJavaScript("""
381 var guest = document.getElementById("guest-user-button");
386 except (exceptions.TabCrashException,
387 exceptions.BrowserConnectionGoneException):
390 def _WaitForGuestFsMounted(self):
391 """Waits for the guest user to be mounted as guestfs"""
392 guest_path = self._CryptohomePath('$guest')
393 util.WaitFor(lambda: (self._cri.FilesystemMountedAt(guest_path) ==
396 def _NavigateGuestLogin(self):
397 """Navigates through oobe login screen as guest"""
398 if not self.oobe_exists:
399 raise exceptions.LoginException('Oobe missing')
400 self._WaitForSigninScreen()
401 self._ClickBrowseAsGuest()
402 self._WaitForGuestFsMounted()
404 def _NavigateLogin(self):
405 """Navigates through oobe login screen"""
406 if self._use_oobe_login_for_testing:
407 logging.info('Invoking Oobe.loginForTesting')
408 if not self.oobe_exists:
409 raise exceptions.LoginException('Oobe missing')
411 util.WaitFor(lambda: oobe.EvaluateJavaScript(
412 'typeof Oobe !== \'undefined\''), 10)
414 if oobe.EvaluateJavaScript(
415 'typeof Oobe.loginForTesting == \'undefined\''):
416 raise exceptions.LoginException('Oobe.loginForTesting js api missing')
418 oobe.ExecuteJavaScript(
419 'Oobe.loginForTesting(\'%s\', \'%s\');'
420 % (self.browser_options.username, self.browser_options.password))
423 util.WaitFor(self._IsLoggedIn, 60)
424 except util.TimeoutException:
425 self._cri.TakeScreenShot('login-screen')
426 raise exceptions.LoginException('Timed out going through login screen')
428 # Wait for extensions to load.
430 self._WaitForBrowserToComeUp()
431 except util.TimeoutException:
432 logging.error('Chrome args: %s' % self._GetChromeProcess()['args'])
433 self._cri.TakeScreenShot('extension-timeout')
436 if self.chrome_branch_number < 1500:
437 # Wait for the startup window, then close it. Startup window doesn't exist
438 # post-M27. crrev.com/197900
439 util.WaitFor(self._StartupWindow, 20).Close()
441 # Workaround for crbug.com/329271, crbug.com/334726.
443 while not len(self.tab_list_backend):
445 # Open a new window/tab.
446 tab = self.tab_list_backend.New(timeout=30)
447 tab.Navigate('about:blank', timeout=10)
448 except (exceptions.TabCrashException, util.TimeoutException):
450 logging.warn('TabCrashException/TimeoutException in '
451 'new tab creation/navigation, '
452 'remaining retries %d' % retries)