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