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    super(CrOSBrowserBackend, self).__init__(
19        cros_platform_backend, supports_tab_control=True,
20        supports_extensions=not is_guest,
21        browser_options=browser_options)
22    assert browser_options.IsCrosBrowserOptions()
23    # Initialize fields so that an explosion during init doesn't break in Close.
24    self._cri = cri
25    self._is_guest = is_guest
26    self._forwarder = None
27    self._remote_debugging_port = self._cri.GetRemotePort()
28    self._port = self._remote_debugging_port
29
30    extensions_to_load = browser_options.extensions_to_load
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
57    logging_patterns = ['*/chromeos/net/*',
58                        '*/chromeos/login/*',
59                        '*/dbus/*',
60                        'application_lifetime',
61                        'chrome_browser_main_posix']
62    vmodule = '--vmodule='
63    for pattern in logging_patterns:
64      vmodule += '%s=2,' % pattern
65    vmodule = vmodule.rstrip(',')
66
67    args.extend([
68            '--enable-smooth-scrolling',
69            '--enable-threaded-compositing',
70            # Allow devtools to connect to chrome.
71            '--remote-debugging-port=%i' % self._remote_debugging_port,
72            # Open a maximized window.
73            '--start-maximized',
74            # Disable system startup sound.
75            '--ash-disable-system-sounds',
76            # Ignore DMServer errors for policy fetches.
77            '--allow-failed-policy-fetch-for-test',
78            # Skip user image selection screen, and post login screens.
79            '--oobe-skip-postlogin',
80            # Debug logging.
81            vmodule])
82
83    # Disable GAIA services unless we're using GAIA login, or if there's an
84    # explicit request for it.
85    if (self.browser_options.disable_gaia_services and
86        not self.browser_options.gaia_login):
87      args.append('--disable-gaia-services')
88
89    trace_config_file = (self.platform_backend.tracing_controller_backend
90                         .GetChromeTraceConfigFile())
91    if trace_config_file:
92      args.append('--trace-config-file=%s' % trace_config_file)
93
94    return args
95
96  @property
97  def pid(self):
98    return self._cri.GetChromePid()
99
100  @property
101  def browser_directory(self):
102    result = self._cri.GetChromeProcess()
103    if result and 'path' in result:
104      return os.path.dirname(result['path'])
105    return None
106
107  @property
108  def profile_directory(self):
109    return '/home/chronos/Default'
110
111  def __del__(self):
112    self.Close()
113
114  def Start(self):
115    # Escape all commas in the startup arguments we pass to Chrome
116    # because dbus-send delimits array elements by commas
117    startup_args = [a.replace(',', '\\,') for a in self.GetBrowserStartupArgs()]
118
119    # Restart Chrome with the login extension and remote debugging.
120    pid = self.pid
121    logging.info('Restarting Chrome (pid=%d) with remote port', pid)
122    args = ['dbus-send', '--system', '--type=method_call',
123            '--dest=org.chromium.SessionManager',
124            '/org/chromium/SessionManager',
125            'org.chromium.SessionManagerInterface.EnableChromeTesting',
126            'boolean:true',
127            'array:string:"%s"' % ','.join(startup_args)]
128    logging.info('Starting Chrome %s', args)
129    self._cri.RunCmdOnDevice(args)
130
131    if not self._cri.local:
132      # TODO(crbug.com/404771): Move port forwarding to network_controller.
133      self._port = util.GetUnreservedAvailableLocalPort()
134      self._forwarder = self._platform_backend.forwarder_factory.Create(
135          forwarders.PortPair(self._port, self._remote_debugging_port),
136          use_remote_port_forwarding=False)
137
138    # Wait for new chrome and oobe.
139    util.WaitFor(lambda: pid != self.pid, 15)
140    self._WaitForBrowserToComeUp()
141    self._InitDevtoolsClientBackend(
142        remote_devtools_port=self._remote_debugging_port)
143    util.WaitFor(lambda: self.oobe_exists, 30)
144
145    if self.browser_options.auto_login:
146      if self._is_guest:
147        pid = self.pid
148        self.oobe.NavigateGuestLogin()
149        # Guest browsing shuts down the current browser and launches an
150        # incognito browser in a separate process, which we need to wait for.
151        try:
152          # TODO(achuith): Reduce this timeout to 15 sec after crbug.com/631640
153          # is resolved.
154          util.WaitFor(lambda: pid != self.pid, 60)
155        except exceptions.TimeoutException:
156          self._RaiseOnLoginFailure(
157              'Failed to restart browser in guest mode (pid %d).' % pid)
158
159      elif self.browser_options.gaia_login:
160        self.oobe.NavigateGaiaLogin(self._username, self._password)
161      else:
162        self.oobe.NavigateFakeLogin(self._username, self._password,
163            self._gaia_id, not self.browser_options.disable_gaia_services)
164
165      try:
166        self._WaitForLogin()
167      except exceptions.TimeoutException:
168        self._RaiseOnLoginFailure('Timed out going through login screen. '
169                                  + self._GetLoginStatus())
170
171    logging.info('Browser is up!')
172
173  def Close(self):
174    super(CrOSBrowserBackend, self).Close()
175
176    if self._cri:
177      self._cri.RestartUI(False) # Logs out.
178      self._cri.CloseConnection()
179
180    util.WaitFor(lambda: not self._IsCryptohomeMounted(), 180)
181
182    if self._forwarder:
183      self._forwarder.Close()
184      self._forwarder = None
185
186    if self._cri:
187      for e in self._extensions_to_load:
188        self._cri.RmRF(os.path.dirname(e.local_path))
189
190    self._cri = None
191
192  def IsBrowserRunning(self):
193    return bool(self.pid)
194
195  def GetStandardOutput(self):
196    return 'Cannot get standard output on CrOS'
197
198  def GetStackTrace(self):
199    return (False, 'Cannot get stack trace on CrOS')
200
201  def GetMostRecentMinidumpPath(self):
202    return None
203
204  def GetAllMinidumpPaths(self):
205    return None
206
207  def GetAllUnsymbolizedMinidumpPaths(self):
208    return None
209
210  def SymbolizeMinidump(self, minidump_path):
211    return None
212
213  @property
214  @decorators.Cache
215  def misc_web_contents_backend(self):
216    """Access to chrome://oobe/login page."""
217    return misc_web_contents_backend.MiscWebContentsBackend(self)
218
219  @property
220  def oobe(self):
221    return self.misc_web_contents_backend.GetOobe()
222
223  @property
224  def oobe_exists(self):
225    return self.misc_web_contents_backend.oobe_exists
226
227  @property
228  def _username(self):
229    return self.browser_options.username
230
231  @property
232  def _password(self):
233    return self.browser_options.password
234
235  @property
236  def _gaia_id(self):
237    return self.browser_options.gaia_id
238
239  def _IsCryptohomeMounted(self):
240    username = '$guest' if self._is_guest else self._username
241    return self._cri.IsCryptohomeMounted(username, self._is_guest)
242
243  def _GetLoginStatus(self):
244    """Returns login status. If logged in, empty string is returned."""
245    status = ''
246    if not self._IsCryptohomeMounted():
247      status += 'Cryptohome not mounted. '
248    if not self.HasBrowserFinishedLaunching():
249      status += 'Browser didn\'t launch. '
250    if self.oobe_exists:
251      status += 'OOBE not dismissed.'
252    return status
253
254  def _IsLoggedIn(self):
255    """Returns True if cryptohome has mounted, the browser is
256    responsive to devtools requests, and the oobe has been dismissed."""
257    return not self._GetLoginStatus()
258
259  def _WaitForLogin(self):
260    # Wait for cryptohome to mount.
261    util.WaitFor(self._IsLoggedIn, 60)
262
263    # For incognito mode, the session manager actually relaunches chrome with
264    # new arguments, so we have to wait for the browser to come up.
265    self._WaitForBrowserToComeUp()
266
267    # Wait for extensions to load.
268    if self._supports_extensions:
269      self._WaitForExtensionsToLoad()
270
271  def _RaiseOnLoginFailure(self, error):
272    if self._platform_backend.CanTakeScreenshot():
273      self._cri.TakeScreenshotWithPrefix('login-screen')
274    raise exceptions.LoginException(error)
275