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.
4
5import logging
6import os
7
8from telemetry import decorators
9from telemetry.core import exceptions
10from telemetry.core import forwarders
11from telemetry.core import util
12from telemetry.core.backends.chrome import chrome_browser_backend
13from telemetry.core.backends.chrome import misc_web_contents_backend
14from telemetry.core.forwarders import cros_forwarder
15
16
17class CrOSBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
18  def __init__(self, browser_options, cri, is_guest, extensions_to_load):
19    super(CrOSBrowserBackend, self).__init__(
20        supports_tab_control=True, supports_extensions=not is_guest,
21        browser_options=browser_options,
22        output_profile_path=None, extensions_to_load=extensions_to_load)
23
24    # Initialize fields so that an explosion during init doesn't break in Close.
25    self._cri = cri
26    self._is_guest = is_guest
27    self._forwarder = None
28
29    from telemetry.core.backends.chrome import chrome_browser_options
30    assert isinstance(browser_options,
31                      chrome_browser_options.CrosBrowserOptions)
32
33    self.wpr_port_pairs = forwarders.PortPairs(
34        http=forwarders.PortPair(self.wpr_port_pairs.http.local_port,
35                                 self.GetRemotePort(
36                                     self.wpr_port_pairs.http.local_port)),
37        https=forwarders.PortPair(self.wpr_port_pairs.https.local_port,
38                                  self.GetRemotePort(
39                                      self.wpr_port_pairs.http.local_port)),
40        dns=None)
41    self._remote_debugging_port = self._cri.GetRemotePort()
42    self._port = self._remote_debugging_port
43
44    # Copy extensions to temp directories on the device.
45    # Note that we also perform this copy locally to ensure that
46    # the owner of the extensions is set to chronos.
47    for e in extensions_to_load:
48      extension_dir = cri.RunCmdOnDevice(
49          ['mktemp', '-d', '/tmp/extension_XXXXX'])[0].rstrip()
50      cri.PushFile(e.path, extension_dir)
51      cri.Chown(extension_dir)
52      e.local_path = os.path.join(extension_dir, os.path.basename(e.path))
53
54    self._cri.RestartUI(self.browser_options.clear_enterprise_policy)
55    util.WaitFor(self.IsBrowserRunning, 20)
56
57    # Delete test user's cryptohome vault (user data directory).
58    if not self.browser_options.dont_override_profile:
59      self._cri.RunCmdOnDevice(['cryptohome', '--action=remove', '--force',
60                                '--user=%s' % self._username])
61    if self.browser_options.profile_dir:
62      cri.RmRF(self.profile_directory)
63      cri.PushFile(self.browser_options.profile_dir + '/Default',
64                   self.profile_directory)
65      cri.Chown(self.profile_directory)
66
67  def GetBrowserStartupArgs(self):
68    args = super(CrOSBrowserBackend, self).GetBrowserStartupArgs()
69    args.extend([
70            '--enable-smooth-scrolling',
71            '--enable-threaded-compositing',
72            '--enable-per-tile-painting',
73            # Disables the start page, as well as other external apps that can
74            # steal focus or make measurements inconsistent.
75            '--disable-default-apps',
76            # Allow devtools to connect to chrome.
77            '--remote-debugging-port=%i' % self._remote_debugging_port,
78            # Open a maximized window.
79            '--start-maximized',
80            # Skip user image selection screen, and post login screens.
81            '--oobe-skip-postlogin',
82            # Debug logging.
83            '--vmodule=*/chromeos/net/*=2,*/chromeos/login/*=2'])
84
85    # Disable GAIA services unless we're using GAIA login, or if there's an
86    # explicit request for it.
87    if (self.browser_options.disable_gaia_services and
88        not self.browser_options.gaia_login):
89      args.append('--disable-gaia-services')
90
91    return args
92
93  @property
94  def pid(self):
95    return self._cri.GetChromePid()
96
97  @property
98  def browser_directory(self):
99    result = self._cri.GetChromeProcess()
100    if result and 'path' in result:
101      return os.path.dirname(result['path'])
102    return None
103
104  @property
105  def profile_directory(self):
106    return '/home/chronos/Default'
107
108  def GetRemotePort(self, port):
109    if self._cri.local:
110      return port
111    return self._cri.GetRemotePort()
112
113  def __del__(self):
114    self.Close()
115
116  def Start(self):
117    # Escape all commas in the startup arguments we pass to Chrome
118    # because dbus-send delimits array elements by commas
119    startup_args = [a.replace(',', '\\,') for a in self.GetBrowserStartupArgs()]
120
121    # Restart Chrome with the login extension and remote debugging.
122    logging.info('Restarting Chrome with flags and login')
123    args = ['dbus-send', '--system', '--type=method_call',
124            '--dest=org.chromium.SessionManager',
125            '/org/chromium/SessionManager',
126            'org.chromium.SessionManagerInterface.EnableChromeTesting',
127            'boolean:true',
128            'array:string:"%s"' % ','.join(startup_args)]
129    self._cri.RunCmdOnDevice(args)
130
131    if not self._cri.local:
132      self._port = util.GetUnreservedAvailableLocalPort()
133      self._forwarder = self.forwarder_factory.Create(
134          forwarders.PortPairs(
135              http=forwarders.PortPair(self._port, self._remote_debugging_port),
136              https=None,
137              dns=None), forwarding_flag='L')
138
139    # Wait for oobe.
140    self._WaitForBrowserToComeUp(wait_for_extensions=False)
141    util.WaitFor(lambda: self.oobe_exists, 10)
142
143    if self.browser_options.auto_login:
144      try:
145        if self._is_guest:
146          pid = self.pid
147          self.oobe.NavigateGuestLogin()
148          # Guest browsing shuts down the current browser and launches an
149          # incognito browser in a separate process, which we need to wait for.
150          util.WaitFor(lambda: pid != self.pid, 10)
151        elif self.browser_options.gaia_login:
152          self.oobe.NavigateGaiaLogin(self._username, self._password)
153        else:
154          self.oobe.NavigateFakeLogin(self._username, self._password)
155        self._WaitForLogin()
156      except util.TimeoutException:
157        self._cri.TakeScreenShot('login-screen')
158        raise exceptions.LoginException('Timed out going through login screen')
159
160    logging.info('Browser is up!')
161
162  def Close(self):
163    super(CrOSBrowserBackend, self).Close()
164
165    if self._cri:
166      self._cri.RestartUI(False) # Logs out.
167      self._cri.CloseConnection()
168
169    util.WaitFor(lambda: not self._IsCryptohomeMounted(), 30)
170
171    if self._forwarder:
172      self._forwarder.Close()
173      self._forwarder = None
174
175    if self._cri:
176      for e in self._extensions_to_load:
177        self._cri.RmRF(os.path.dirname(e.local_path))
178
179    self._cri = None
180
181  @property
182  @decorators.Cache
183  def forwarder_factory(self):
184    return cros_forwarder.CrOsForwarderFactory(self._cri)
185
186  def IsBrowserRunning(self):
187    return bool(self.pid)
188
189  def GetStandardOutput(self):
190    return 'Cannot get standard output on CrOS'
191
192  def GetStackTrace(self):
193    return 'Cannot get stack trace on CrOS'
194
195  @property
196  @decorators.Cache
197  def misc_web_contents_backend(self):
198    """Access to chrome://oobe/login page."""
199    return misc_web_contents_backend.MiscWebContentsBackend(self)
200
201  @property
202  def oobe(self):
203    return self.misc_web_contents_backend.GetOobe()
204
205  @property
206  def oobe_exists(self):
207    return self.misc_web_contents_backend.oobe_exists
208
209  @property
210  def _username(self):
211    return self.browser_options.username
212
213  @property
214  def _password(self):
215    return self.browser_options.password
216
217  def _IsCryptohomeMounted(self):
218    username = '$guest' if self._is_guest else self._username
219    return self._cri.IsCryptohomeMounted(username, self._is_guest)
220
221  def _IsLoggedIn(self):
222    """Returns True if cryptohome has mounted, the browser is
223    responsive to devtools requests, and the oobe has been dismissed."""
224    return (self._IsCryptohomeMounted() and
225            self.HasBrowserFinishedLaunching() and
226            not self.oobe_exists)
227
228  def _WaitForLogin(self):
229    # Wait for cryptohome to mount.
230    util.WaitFor(self._IsLoggedIn, 60)
231
232    # Wait for extensions to load.
233    self._WaitForBrowserToComeUp()
234
235    # Workaround for crbug.com/374462 - the bug doesn't manifest in the guest
236    # session, which also starts with an open browser tab.
237    retries = 3
238    while not self._is_guest and not self.browser_options.gaia_login:
239      try:
240        # Open a new window/tab.
241        tab = self.tab_list_backend.New(timeout=30)
242        tab.Navigate('about:blank', timeout=10)
243        break
244      except (exceptions.TabCrashException, util.TimeoutException,
245              IndexError):
246        retries -= 1
247        logging.warning('TabCrashException/TimeoutException in '
248                        'new tab creation/navigation, '
249                        'remaining retries %d', retries)
250        if not retries:
251          raise
252