Home | History | Annotate | Download | only in chrome
      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