blob: 8b6449434a832bb22d940b5f4aee7ec23f083445 [file] [log] [blame]
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import os
from telemetry import decorators
from telemetry.core import exceptions
from telemetry.core import forwarders
from telemetry.core import util
from telemetry.core.backends.chrome import chrome_browser_backend
from telemetry.core.backends.chrome import misc_web_contents_backend
from telemetry.core.forwarders import cros_forwarder
class CrOSBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
# Some developers' workflow includes running the Chrome process from
# /usr/local/... instead of the default location. We have to check for both
# paths in order to support this workflow.
CHROME_PATHS = ['/opt/google/chrome/chrome ',
'/usr/local/opt/google/chrome/chrome ']
def __init__(self, browser_type, browser_options, cri, is_guest,
extensions_to_load):
super(CrOSBrowserBackend, self).__init__(
is_content_shell=False, supports_extensions=not is_guest,
browser_options=browser_options,
output_profile_path=None, extensions_to_load=extensions_to_load)
# Initialize fields so that an explosion during init doesn't break in Close.
self._browser_type = browser_type
self._cri = cri
self._is_guest = is_guest
self._forwarder = None
from telemetry.core.backends.chrome import chrome_browser_options
assert isinstance(browser_options,
chrome_browser_options.CrosBrowserOptions)
self.wpr_port_pairs = forwarders.PortPairs(
http=forwarders.PortPair(self.wpr_port_pairs.http.local_port,
self._cri.GetRemotePort()),
https=forwarders.PortPair(self.wpr_port_pairs.https.local_port,
self._cri.GetRemotePort()),
dns=None)
self._remote_debugging_port = self._cri.GetRemotePort()
self._port = self._remote_debugging_port
# Copy extensions to temp directories on the device.
# Note that we also perform this copy locally to ensure that
# the owner of the extensions is set to chronos.
for e in extensions_to_load:
output = cri.RunCmdOnDevice(['mktemp', '-d', '/tmp/extension_XXXXX'])
extension_dir = output[0].rstrip()
cri.PushFile(e.path, extension_dir)
cri.Chown(extension_dir)
e.local_path = os.path.join(extension_dir, os.path.basename(e.path))
self._cri.RunCmdOnDevice(['stop', 'ui'])
if self.browser_options.clear_enterprise_policy:
self._cri.RmRF('/var/lib/whitelist/*')
self._cri.RmRF('/home/chronos/Local\ State')
# Delete test user's cryptohome vault (user data directory).
if not self.browser_options.dont_override_profile:
self._cri.RunCmdOnDevice(['cryptohome', '--action=remove', '--force',
'--user=%s' % self.browser_options.username])
if self.browser_options.profile_dir:
cri.RmRF(self.profile_directory)
cri.PushFile(self.browser_options.profile_dir + '/Default',
self.profile_directory)
cri.Chown(self.profile_directory)
self._cri.RunCmdOnDevice(['start', 'ui'])
util.WaitFor(self.IsBrowserRunning, 20)
self._SetBranchNumber(self._GetChromeVersion())
def GetBrowserStartupArgs(self):
args = super(CrOSBrowserBackend, self).GetBrowserStartupArgs()
args.extend([
'--enable-smooth-scrolling',
'--enable-threaded-compositing',
'--enable-per-tile-painting',
'--force-compositing-mode',
# Disables the start page, as well as other external apps that can
# steal focus or make measurements inconsistent.
'--disable-default-apps',
# Skip user image selection screen, and post login screens.
'--oobe-skip-postlogin',
# Allow devtools to connect to chrome.
'--remote-debugging-port=%i' % self._remote_debugging_port,
# Open a maximized window.
'--start-maximized',
# Debug logging.
'--vmodule=*/chromeos/net/*=2,*/chromeos/login/*=2'])
return args
def _GetSessionManagerPid(self, procs):
"""Returns the pid of the session_manager process, given the list of
processes."""
for pid, process, _, _ in procs:
if process.startswith('/sbin/session_manager '):
return pid
return None
def _GetChromeProcess(self):
"""Locates the the main chrome browser process.
Chrome on cros is usually in /opt/google/chrome, but could be in
/usr/local/ for developer workflows - debug chrome is too large to fit on
rootfs.
Chrome spawns multiple processes for renderers. pids wrap around after they
are exhausted so looking for the smallest pid is not always correct. We
locate the session_manager's pid, and look for the chrome process that's an
immediate child. This is the main browser process.
"""
procs = self._cri.ListProcesses()
session_manager_pid = self._GetSessionManagerPid(procs)
if not session_manager_pid:
return None
# Find the chrome process that is the child of the session_manager.
for pid, process, ppid, _ in procs:
if ppid != session_manager_pid:
continue
for path in self.CHROME_PATHS:
if process.startswith(path):
return {'pid': pid, 'path': path, 'args': process}
return None
def _GetChromeVersion(self):
result = util.WaitFor(self._GetChromeProcess, timeout=30)
assert result and result['path']
(version, _) = self._cri.RunCmdOnDevice([result['path'], '--version'])
assert version
return version
@property
def pid(self):
result = self._GetChromeProcess()
if result and 'pid' in result:
return result['pid']
return None
@property
def browser_directory(self):
result = self._GetChromeProcess()
if result and 'path' in result:
return os.path.dirname(result['path'])
return None
@property
def profile_directory(self):
return '/home/chronos/Default'
def GetRemotePort(self, port):
if self._cri.local:
return port
return self._cri.GetRemotePort()
def __del__(self):
self.Close()
def Start(self):
# Escape all commas in the startup arguments we pass to Chrome
# because dbus-send delimits array elements by commas
startup_args = [a.replace(',', '\\,') for a in self.GetBrowserStartupArgs()]
# Restart Chrome with the login extension and remote debugging.
logging.info('Restarting Chrome with flags and login')
args = ['dbus-send', '--system', '--type=method_call',
'--dest=org.chromium.SessionManager',
'/org/chromium/SessionManager',
'org.chromium.SessionManagerInterface.EnableChromeTesting',
'boolean:true',
'array:string:"%s"' % ','.join(startup_args)]
self._cri.RunCmdOnDevice(args)
if not self._cri.local:
self._port = util.GetUnreservedAvailableLocalPort()
self._forwarder = self.forwarder_factory.Create(
forwarders.PortPairs(
http=forwarders.PortPair(self._port, self._remote_debugging_port),
https=None,
dns=None), forwarding_flag='L')
try:
self._WaitForBrowserToComeUp(wait_for_extensions=False)
self._PostBrowserStartupInitialization()
except:
import traceback
traceback.print_exc()
self.Close()
raise
util.WaitFor(lambda: self.oobe_exists, 10)
if self.browser_options.auto_login:
if self._is_guest:
pid = self.pid
self.oobe.NavigateGuestLogin()
# Guest browsing shuts down the current browser and launches an
# incognito browser in a separate process, which we need to wait for.
util.WaitFor(lambda: pid != self.pid, 10)
self._WaitForBrowserToComeUp()
elif self.browser_options.gaia_login:
self.oobe.NavigateGaiaLogin(self.browser_options.username,
self.browser_options.password)
else:
self.oobe.NavigateFakeLogin(self.browser_options.username,
self.browser_options.password)
logging.info('Browser is up!')
def Close(self):
super(CrOSBrowserBackend, self).Close()
self._RestartUI() # Logs out.
util.WaitFor(lambda: not self._IsCryptohomeMounted(), 30)
if self._forwarder:
self._forwarder.Close()
self._forwarder = None
if self._cri:
for e in self._extensions_to_load:
self._cri.RmRF(os.path.dirname(e.local_path))
self._cri = None
@property
@decorators.Cache
def forwarder_factory(self):
return cros_forwarder.CrOsForwarderFactory(self._cri)
def IsBrowserRunning(self):
return bool(self.pid)
def GetStandardOutput(self):
return 'Cannot get standard output on CrOS'
def GetStackTrace(self):
return 'Cannot get stack trace on CrOS'
def _RestartUI(self):
if self._cri:
logging.info('(Re)starting the ui (logs the user out)')
if self._cri.IsServiceRunning('ui'):
self._cri.RunCmdOnDevice(['restart', 'ui'])
else:
self._cri.RunCmdOnDevice(['start', 'ui'])
def TakeScreenShot(self, screenshot_prefix):
self._cri.TakeScreenShot(screenshot_prefix)
@property
@decorators.Cache
def misc_web_contents_backend(self):
"""Access to chrome://oobe/login page."""
return misc_web_contents_backend.MiscWebContentsBackend(self)
@property
def oobe(self):
return self.misc_web_contents_backend.GetOobe()
@property
def oobe_exists(self):
return self.misc_web_contents_backend.oobe_exists
def _IsCryptohomeMounted(self):
username = '$guest' if self._is_guest else self.browser_options.username
return self._cri.IsCryptohomeMounted(username, self._is_guest)
def _IsLoggedIn(self):
"""Returns True if cryptohome has mounted, the browser is
responsive to devtools requests, and the oobe has been dismissed."""
return (self._IsCryptohomeMounted() and
self.HasBrowserFinishedLaunching() and
not self.oobe_exists)
def WaitForLogin(self):
if self._is_guest:
util.WaitFor(self._IsCryptohomeMounted, 30)
return
try:
util.WaitFor(self._IsLoggedIn, 60)
except util.TimeoutException:
self.TakeScreenShot('login-screen')
raise exceptions.LoginException('Timed out going through login screen')
# Wait for extensions to load.
try:
self._WaitForBrowserToComeUp()
except util.TimeoutException:
logging.error('Chrome args: %s' % self._GetChromeProcess()['args'])
self.TakeScreenShot('extension-timeout')
raise
# Workaround for crbug.com/329271, crbug.com/334726.
retries = 3
while True:
try:
# Open a new window/tab.
tab = self.tab_list_backend.New(timeout=30)
tab.Navigate('about:blank', timeout=10)
break
except (exceptions.TabCrashException, util.TimeoutException,
IndexError):
retries -= 1
logging.warn('TabCrashException/TimeoutException in '
'new tab creation/navigation, '
'remaining retries %d' % retries)
if not retries:
raise