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.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
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 ']
20 def __init__(self, browser_type, browser_options, cri, is_guest,
22 super(CrOSBrowserBackend, self).__init__(
23 is_content_shell=False, supports_extensions=not is_guest,
24 browser_options=browser_options,
25 output_profile_path=None, extensions_to_load=extensions_to_load)
27 from telemetry.core.backends.chrome import chrome_browser_options
28 assert isinstance(browser_options,
29 chrome_browser_options.CrosBrowserOptions)
31 # Initialize fields so that an explosion during init doesn't break in Close.
32 self._browser_type = browser_type
34 self._is_guest = is_guest
36 self.wpr_http_port_pair.remote_port = self._cri.GetRemotePort()
37 self.wpr_https_port_pair.remote_port = self._cri.GetRemotePort()
38 self._remote_debugging_port = self._cri.GetRemotePort()
39 self._port = self._remote_debugging_port
40 self._forwarder = None
42 self._SetBranchNumber(self._GetChromeVersion())
44 self._login_ext_dir = None
45 if not self._use_oobe_login_for_testing:
46 self._login_ext_dir = os.path.join(os.path.dirname(__file__),
49 # Push a dummy login extension to the device.
50 # This extension automatically logs in test user specified by
51 # self.browser_options.username.
52 # Note that we also perform this copy locally to ensure that
53 # the owner of the extensions is set to chronos.
54 logging.info('Copying dummy login extension to the device')
55 cri.PushFile(self._login_ext_dir, '/tmp/')
56 self._login_ext_dir = '/tmp/chromeos_login_ext'
57 cri.Chown(self._login_ext_dir)
59 # Copy extensions to temp directories on the device.
60 # Note that we also perform this copy locally to ensure that
61 # the owner of the extensions is set to chronos.
62 for e in extensions_to_load:
63 output = cri.RunCmdOnDevice(['mktemp', '-d', '/tmp/extension_XXXXX'])
64 extension_dir = output[0].rstrip()
65 cri.PushFile(e.path, extension_dir)
66 cri.Chown(extension_dir)
67 e.local_path = os.path.join(extension_dir, os.path.basename(e.path))
69 # Ensure the UI is running and logged out.
71 util.WaitFor(self.IsBrowserRunning, 20)
73 # Delete test user's cryptohome vault (user data directory).
74 if not self.browser_options.dont_override_profile:
75 self._cri.RunCmdOnDevice(['cryptohome', '--action=remove', '--force',
76 '--user=%s' % self.browser_options.username])
77 if self.browser_options.profile_dir:
78 cri.RmRF(self.profile_directory)
79 cri.PushFile(self.browser_options.profile_dir + '/Default',
80 self.profile_directory)
81 cri.Chown(self.profile_directory)
83 def GetBrowserStartupArgs(self):
84 args = super(CrOSBrowserBackend, self).GetBrowserStartupArgs()
86 '--enable-smooth-scrolling',
87 '--enable-threaded-compositing',
88 '--enable-per-tile-painting',
89 '--force-compositing-mode',
90 # Disables the start page, as well as other external apps that can
91 # steal focus or make measurements inconsistent.
92 '--disable-default-apps',
93 # Skip user image selection screen, and post login screens.
94 '--oobe-skip-postlogin',
95 # Allow devtools to connect to chrome.
96 '--remote-debugging-port=%i' % self._remote_debugging_port,
97 # Open a maximized window.
99 # TODO(achuith): Re-enable this flag again before multi-profiles
100 # will become enabled by default to have telemetry mileage on it.
101 # '--multi-profiles',
102 # Debug logging for login flake (crbug.com/263527).
103 '--vmodule=*/browser/automation/*=2,*/chromeos/net/*=2,'
104 '*/chromeos/login/*=2,*/extensions/*=2,'
105 '*/device_policy_decoder_chromeos.cc=2'])
109 # Jump to the login screen, skipping network selection, eula, etc.
110 '--login-screen=login',
111 # Skip hwid check, for VMs and pre-MP lab devices.
114 elif not self._use_oobe_login_for_testing:
115 # This extension bypasses gaia and logs us in.
116 logging.info('Using --auth-ext-path=%s to login', self._login_ext_dir)
117 args.append('--auth-ext-path=%s' % self._login_ext_dir)
121 def _GetSessionManagerPid(self, procs):
122 """Returns the pid of the session_manager process, given the list of
124 for pid, process, _, _ in procs:
125 if process.startswith('/sbin/session_manager '):
129 def _GetChromeProcess(self):
130 """Locates the the main chrome browser process.
132 Chrome on cros is usually in /opt/google/chrome, but could be in
133 /usr/local/ for developer workflows - debug chrome is too large to fit on
136 Chrome spawns multiple processes for renderers. pids wrap around after they
137 are exhausted so looking for the smallest pid is not always correct. We
138 locate the session_manager's pid, and look for the chrome process that's an
139 immediate child. This is the main browser process.
141 procs = self._cri.ListProcesses()
142 session_manager_pid = self._GetSessionManagerPid(procs)
143 if not session_manager_pid:
146 # Find the chrome process that is the child of the session_manager.
147 for pid, process, ppid, _ in procs:
148 if ppid != session_manager_pid:
150 for path in self.CHROME_PATHS:
151 if process.startswith(path):
152 return {'pid': pid, 'path': path, 'args': process}
155 def _GetChromeVersion(self):
156 result = util.WaitFor(self._GetChromeProcess, timeout=30)
157 assert result and result['path']
158 (version, _) = self._cri.RunCmdOnDevice([result['path'], '--version'])
164 result = self._GetChromeProcess()
165 if result and 'pid' in result:
170 def browser_directory(self):
171 result = self._GetChromeProcess()
172 if result and 'path' in result:
173 return os.path.dirname(result['path'])
177 def profile_directory(self):
178 return '/home/chronos/Default'
182 return self._cri.RunCmdOnDevice(['/usr/bin/crossystem', 'hwid'])[0]
185 def _use_oobe_login_for_testing(self):
186 """Oobe.LoginForTesting was introduced after branch 1599."""
187 return self.chrome_branch_number > 1599
189 def GetRemotePort(self, _):
190 return self._cri.GetRemotePort()
196 # Escape all commas in the startup arguments we pass to Chrome
197 # because dbus-send delimits array elements by commas
198 startup_args = [a.replace(',', '\\,') for a in self.GetBrowserStartupArgs()]
200 # Restart Chrome with the login extension and remote debugging.
201 logging.info('Restarting Chrome with flags and login')
202 args = ['dbus-send', '--system', '--type=method_call',
203 '--dest=org.chromium.SessionManager',
204 '/org/chromium/SessionManager',
205 'org.chromium.SessionManagerInterface.EnableChromeTesting',
207 'array:string:"%s"' % ','.join(startup_args)]
208 self._cri.RunCmdOnDevice(args)
210 if not self._cri.local:
211 # Find a free local port.
212 self._port = util.GetUnreservedAvailableLocalPort()
214 # Forward the remote debugging port.
215 logging.info('Forwarding remote debugging port %d to local port %d',
216 self._remote_debugging_port, self._port)
217 self._forwarder = SSHForwarder(
219 util.PortPair(self._port, self._remote_debugging_port))
222 self._WaitForBrowserToComeUp(wait_for_extensions=False)
223 self._PostBrowserStartupInitialization()
226 traceback.print_exc()
230 # chrome_branch_number is set in _PostBrowserStartupInitialization.
231 # Without --skip-hwid-check (introduced in crrev.com/203397), devices/VMs
232 # will be stuck on the bad hwid screen.
233 if self.chrome_branch_number <= 1500 and not self.hwid:
234 raise exceptions.LoginException(
235 'Hardware id not set on device/VM. --skip-hwid-check not supported '
236 'with chrome branches 1500 or earlier.')
238 util.WaitFor(lambda: self.oobe_exists, 10)
240 if self.browser_options.auto_login:
243 self._NavigateGuestLogin()
244 # Guest browsing shuts down the current browser and launches an
245 # incognito browser in a separate process, which we need to wait for.
246 util.WaitFor(lambda: pid != self.pid, 10)
247 self._WaitForBrowserToComeUp()
249 self._NavigateLogin()
251 logging.info('Browser is up!')
254 super(CrOSBrowserBackend, self).Close()
256 self._RestartUI() # Logs out.
258 if not self._cri.local:
260 self._forwarder.Close()
261 self._forwarder = None
263 if self._login_ext_dir:
264 self._cri.RmRF(self._login_ext_dir)
265 self._login_ext_dir = None
267 for e in self._extensions_to_load:
268 self._cri.RmRF(os.path.dirname(e.local_path))
272 def IsBrowserRunning(self):
273 return bool(self.pid)
275 def GetStandardOutput(self):
276 return 'Cannot get standard output on CrOS'
278 def GetStackTrace(self):
279 return 'Cannot get stack trace on CrOS'
281 def CreateForwarder(self, *port_pairs):
283 return (browser_backend.DoNothingForwarder(*port_pairs) if self._cri.local
284 else SSHForwarder(self._cri, 'R', *port_pairs))
286 def _RestartUI(self):
288 logging.info('(Re)starting the ui (logs the user out)')
289 if self._cri.IsServiceRunning('ui'):
290 self._cri.RunCmdOnDevice(['restart', 'ui'])
292 self._cri.RunCmdOnDevice(['start', 'ui'])
296 return self.misc_web_contents_backend.GetOobe()
299 def oobe_exists(self):
300 return self.misc_web_contents_backend.oobe_exists
302 def _SigninUIState(self):
303 """Returns the signin ui state of the oobe. HIDDEN: 0, GAIA_SIGNIN: 1,
304 ACCOUNT_PICKER: 2, WRONG_HWID_WARNING: 3, MANAGED_USER_CREATION_FLOW: 4.
306 chrome/browser/resources/chromeos/login/display_manager.js
308 return self.oobe.EvaluateJavaScript('''
309 loginHeader = document.getElementById('login-header-bar')
311 loginHeader.signinUIState_;
315 def _CryptohomePath(self, user):
316 (path, _) = self._cri.RunCmdOnDevice(['cryptohome-path', 'user',
320 def _IsCryptohomeMounted(self):
321 """Returns True if a cryptohome vault at the user mount point."""
322 profile_path = self._CryptohomePath(self.browser_options.username)
323 mount = self._cri.FilesystemMountedAt(profile_path)
324 return mount and mount.startswith('/home/.shadow/')
326 def _HandleUserImageSelectionScreen(self):
327 """If we're stuck on the user image selection screen, we click the ok
333 oobe.EvaluateJavaScript("""
334 var ok = document.getElementById("ok-button");
339 except (exceptions.TabCrashException):
342 def _IsLoggedIn(self):
343 """Returns True if we're logged in (cryptohome has mounted), and the oobe
344 has been dismissed."""
345 if self.chrome_branch_number <= 1547:
346 self._HandleUserImageSelectionScreen()
347 return self._IsCryptohomeMounted() and not self.oobe_exists
349 def _StartupWindow(self):
350 """Closes the startup window, which is an extension on official builds,
351 and a webpage on chromiumos"""
352 startup_window_ext_id = 'honijodknafkokifofgiaalefdiedpko'
353 return (self.extension_dict_backend[startup_window_ext_id]
354 if startup_window_ext_id in self.extension_dict_backend
355 else self.tab_list_backend.Get(0, None))
357 def _WaitForSigninScreen(self):
358 """Waits for oobe to be on the signin or account picker screen."""
359 def OnAccountPickerScreen():
360 signin_state = self._SigninUIState()
361 # GAIA_SIGNIN or ACCOUNT_PICKER screens.
362 return signin_state == 1 or signin_state == 2
364 util.WaitFor(OnAccountPickerScreen, 60)
365 except util.TimeoutException:
366 self._cri.TakeScreenShot('guest-screen')
367 raise exceptions.LoginException('Timed out waiting for signin screen, '
368 'signin state %d' % self._SigninUIState())
370 def _ClickBrowseAsGuest(self):
371 """Click the Browse As Guest button on the account picker screen. This will
372 restart the browser, and we could have a tab crash or a browser crash."""
374 self.oobe.EvaluateJavaScript("""
375 var guest = document.getElementById("guest-user-button");
380 except (exceptions.TabCrashException,
381 exceptions.BrowserConnectionGoneException):
384 def _WaitForGuestFsMounted(self):
385 """Waits for the guest user to be mounted as guestfs"""
386 guest_path = self._CryptohomePath('$guest')
387 util.WaitFor(lambda: (self._cri.FilesystemMountedAt(guest_path) ==
390 def _NavigateGuestLogin(self):
391 """Navigates through oobe login screen as guest"""
392 assert self.oobe_exists
393 self._WaitForSigninScreen()
394 self._ClickBrowseAsGuest()
395 self._WaitForGuestFsMounted()
397 def _NavigateLogin(self):
398 """Navigates through oobe login screen"""
399 if self._use_oobe_login_for_testing:
400 logging.info('Invoking Oobe.loginForTesting')
401 assert self.oobe_exists
403 util.WaitFor(lambda: oobe.EvaluateJavaScript(
404 'typeof Oobe !== \'undefined\''), 10)
406 if oobe.EvaluateJavaScript(
407 'typeof Oobe.loginForTesting == \'undefined\''):
408 raise exceptions.LoginException('Oobe.loginForTesting js api missing')
410 oobe.ExecuteJavaScript(
411 'Oobe.loginForTesting(\'%s\', \'%s\');'
412 % (self.browser_options.username, self.browser_options.password))
415 util.WaitFor(self._IsLoggedIn, 60)
416 except util.TimeoutException:
417 self._cri.TakeScreenShot('login-screen')
418 raise exceptions.LoginException('Timed out going through login screen')
420 # Wait for extensions to load.
422 self._WaitForBrowserToComeUp()
423 except util.TimeoutException:
424 logging.error('Chrome args: %s' % self._GetChromeProcess()['args'])
425 self._cri.TakeScreenShot('extension-timeout')
428 if self.chrome_branch_number < 1500:
429 # Wait for the startup window, then close it. Startup window doesn't exist
430 # post-M27. crrev.com/197900
431 util.WaitFor(self._StartupWindow, 20).Close()
433 # Workaround for crbug.com/329271, crbug.com/334726.
435 while not len(self.tab_list_backend):
437 # Open a new window/tab.
438 tab = self.tab_list_backend.New(timeout=30)
439 tab.Navigate('about:blank', timeout=10)
440 except (exceptions.TabCrashException, util.TimeoutException):
442 logging.warn('TabCrashException/TimeoutException in '
443 'new tab creation/navigation, '
444 'remaining retries %d' % retries)
448 class SSHForwarder(object):
449 def __init__(self, cri, forwarding_flag, *port_pairs):
452 if forwarding_flag == 'R':
453 self._host_port = port_pairs[0].remote_port
454 command_line = ['-%s%i:localhost:%i' % (forwarding_flag,
455 port_pair.remote_port,
456 port_pair.local_port)
457 for port_pair in port_pairs]
459 self._host_port = port_pairs[0].local_port
460 command_line = ['-%s%i:localhost:%i' % (forwarding_flag,
461 port_pair.local_port,
462 port_pair.remote_port)
463 for port_pair in port_pairs]
465 self._device_port = port_pairs[0].remote_port
467 self._proc = subprocess.Popen(
468 cri.FormSSHCommandLine(['sleep', '999999999'], command_line),
469 stdout=subprocess.PIPE,
470 stderr=subprocess.PIPE,
471 stdin=subprocess.PIPE,
474 util.WaitFor(lambda: cri.IsHTTPServerRunningOnPort(self._device_port), 60)
475 logging.debug('ssh forwarder created: %s', command_line)
480 return 'http://localhost:%i' % self._host_port