ca802fb10ffbe9a398e62bf8383ee0e056b98aa4
[platform/framework/web/crosswalk.git] / src / tools / telemetry / telemetry / core / backends / chrome / cros_browser_backend.py
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
5 import logging
6 import os
7
8 from telemetry import decorators
9
10 from telemetry.core import exceptions
11 from telemetry.core import forwarders
12 from telemetry.core import util
13 from telemetry.core.backends.chrome import chrome_browser_backend
14 from telemetry.core.forwarders import cros_forwarder
15
16
17 class CrOSBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
18   # Some developers' workflow includes running the Chrome process from
19   # /usr/local/... instead of the default location. We have to check for both
20   # paths in order to support this workflow.
21   CHROME_PATHS = ['/opt/google/chrome/chrome ',
22                   '/usr/local/opt/google/chrome/chrome ']
23
24   def __init__(self, browser_type, browser_options, cri, is_guest,
25                extensions_to_load):
26     super(CrOSBrowserBackend, self).__init__(
27         is_content_shell=False, supports_extensions=not is_guest,
28         browser_options=browser_options,
29         output_profile_path=None, extensions_to_load=extensions_to_load)
30
31     from telemetry.core.backends.chrome import chrome_browser_options
32     assert isinstance(browser_options,
33                       chrome_browser_options.CrosBrowserOptions)
34
35     # Initialize fields so that an explosion during init doesn't break in Close.
36     self._browser_type = browser_type
37     self._cri = cri
38     self._is_guest = is_guest
39     self._forwarder = None
40
41     self.wpr_port_pairs = forwarders.PortPairs(
42         http=forwarders.PortPair(self.wpr_port_pairs.http.local_port,
43                                  self._cri.GetRemotePort()),
44         https=forwarders.PortPair(self.wpr_port_pairs.https.local_port,
45                                   self._cri.GetRemotePort()),
46         dns=None)
47     self._remote_debugging_port = self._cri.GetRemotePort()
48     self._port = self._remote_debugging_port
49
50     self._SetBranchNumber(self._GetChromeVersion())
51
52     self._login_ext_dir = None
53     if not self._use_oobe_login_for_testing:
54       self._login_ext_dir = os.path.join(os.path.dirname(__file__),
55                                          'chromeos_login_ext')
56
57       # Push a dummy login extension to the device.
58       # This extension automatically logs in test user specified by
59       # self.browser_options.username.
60       # Note that we also perform this copy locally to ensure that
61       # the owner of the extensions is set to chronos.
62       logging.info('Copying dummy login extension to the device')
63       cri.PushFile(self._login_ext_dir, '/tmp/')
64       self._login_ext_dir = '/tmp/chromeos_login_ext'
65       cri.Chown(self._login_ext_dir)
66
67     # Copy extensions to temp directories on the device.
68     # Note that we also perform this copy locally to ensure that
69     # the owner of the extensions is set to chronos.
70     for e in extensions_to_load:
71       output = cri.RunCmdOnDevice(['mktemp', '-d', '/tmp/extension_XXXXX'])
72       extension_dir = output[0].rstrip()
73       cri.PushFile(e.path, extension_dir)
74       cri.Chown(extension_dir)
75       e.local_path = os.path.join(extension_dir, os.path.basename(e.path))
76
77     # Ensure the UI is running and logged out.
78     self._RestartUI()
79     util.WaitFor(self.IsBrowserRunning, 20)
80
81     # Delete test user's cryptohome vault (user data directory).
82     if not self.browser_options.dont_override_profile:
83       self._cri.RunCmdOnDevice(['cryptohome', '--action=remove', '--force',
84                                 '--user=%s' % self.browser_options.username])
85     if self.browser_options.profile_dir:
86       cri.RmRF(self.profile_directory)
87       cri.PushFile(self.browser_options.profile_dir + '/Default',
88                    self.profile_directory)
89       cri.Chown(self.profile_directory)
90
91   def GetBrowserStartupArgs(self):
92     args = super(CrOSBrowserBackend, self).GetBrowserStartupArgs()
93     args.extend([
94             '--enable-smooth-scrolling',
95             '--enable-threaded-compositing',
96             '--enable-per-tile-painting',
97             '--force-compositing-mode',
98             # Disables the start page, as well as other external apps that can
99             # steal focus or make measurements inconsistent.
100             '--disable-default-apps',
101             # Skip user image selection screen, and post login screens.
102             '--oobe-skip-postlogin',
103             # Allow devtools to connect to chrome.
104             '--remote-debugging-port=%i' % self._remote_debugging_port,
105             # Open a maximized window.
106             '--start-maximized',
107             # TODO(achuith): Re-enable this flag again before multi-profiles
108             # will become enabled by default to have telemetry mileage on it.
109             # '--multi-profiles',
110             # Debug logging for login flake (crbug.com/263527).
111             '--vmodule=*/browser/automation/*=2,*/chromeos/net/*=2,'
112                 '*/chromeos/login/*=2,*/extensions/*=2,'
113                 '*/device_policy_decoder_chromeos.cc=2'])
114
115     if self._is_guest:
116       args.extend([
117           # Jump to the login screen, skipping network selection, eula, etc.
118           '--login-screen=login',
119           # Skip hwid check, for VMs and pre-MP lab devices.
120           '--skip-hwid-check'
121       ])
122     elif not self._use_oobe_login_for_testing:
123       # This extension bypasses gaia and logs us in.
124       logging.info('Using --auth-ext-path=%s to login', self._login_ext_dir)
125       args.append('--auth-ext-path=%s' % self._login_ext_dir)
126
127     return args
128
129   def _GetSessionManagerPid(self, procs):
130     """Returns the pid of the session_manager process, given the list of
131     processes."""
132     for pid, process, _, _ in procs:
133       if process.startswith('/sbin/session_manager '):
134         return pid
135     return None
136
137   def _GetChromeProcess(self):
138     """Locates the the main chrome browser process.
139
140     Chrome on cros is usually in /opt/google/chrome, but could be in
141     /usr/local/ for developer workflows - debug chrome is too large to fit on
142     rootfs.
143
144     Chrome spawns multiple processes for renderers. pids wrap around after they
145     are exhausted so looking for the smallest pid is not always correct. We
146     locate the session_manager's pid, and look for the chrome process that's an
147     immediate child. This is the main browser process.
148     """
149     procs = self._cri.ListProcesses()
150     session_manager_pid = self._GetSessionManagerPid(procs)
151     if not session_manager_pid:
152       return None
153
154     # Find the chrome process that is the child of the session_manager.
155     for pid, process, ppid, _ in procs:
156       if ppid != session_manager_pid:
157         continue
158       for path in self.CHROME_PATHS:
159         if process.startswith(path):
160           return {'pid': pid, 'path': path, 'args': process}
161     return None
162
163   def _GetChromeVersion(self):
164     result = util.WaitFor(self._GetChromeProcess, timeout=30)
165     assert result and result['path']
166     (version, _) = self._cri.RunCmdOnDevice([result['path'], '--version'])
167     assert version
168     return version
169
170   @property
171   def pid(self):
172     result = self._GetChromeProcess()
173     if result and 'pid' in result:
174       return result['pid']
175     return None
176
177   @property
178   def browser_directory(self):
179     result = self._GetChromeProcess()
180     if result and 'path' in result:
181       return os.path.dirname(result['path'])
182     return None
183
184   @property
185   def profile_directory(self):
186     return '/home/chronos/Default'
187
188   @property
189   def hwid(self):
190     return self._cri.RunCmdOnDevice(['/usr/bin/crossystem', 'hwid'])[0]
191
192   @property
193   def _use_oobe_login_for_testing(self):
194     """Oobe.LoginForTesting was introduced after branch 1599."""
195     return self.chrome_branch_number > 1599
196
197   def GetRemotePort(self, port):
198     if self._cri.local:
199       return port
200     return self._cri.GetRemotePort()
201
202   def __del__(self):
203     self.Close()
204
205   def Start(self):
206     # Escape all commas in the startup arguments we pass to Chrome
207     # because dbus-send delimits array elements by commas
208     startup_args = [a.replace(',', '\\,') for a in self.GetBrowserStartupArgs()]
209
210     # Restart Chrome with the login extension and remote debugging.
211     logging.info('Restarting Chrome with flags and login')
212     args = ['dbus-send', '--system', '--type=method_call',
213             '--dest=org.chromium.SessionManager',
214             '/org/chromium/SessionManager',
215             'org.chromium.SessionManagerInterface.EnableChromeTesting',
216             'boolean:true',
217             'array:string:"%s"' % ','.join(startup_args)]
218     self._cri.RunCmdOnDevice(args)
219
220     if not self._cri.local:
221       self._port = util.GetUnreservedAvailableLocalPort()
222       self._forwarder = self.forwarder_factory.Create(
223           forwarders.PortPairs(
224               http=forwarders.PortPair(self._port, self._remote_debugging_port),
225               https=None,
226               dns=None), forwarding_flag='L')
227
228     try:
229       self._WaitForBrowserToComeUp(wait_for_extensions=False)
230       self._PostBrowserStartupInitialization()
231     except:
232       import traceback
233       traceback.print_exc()
234       self.Close()
235       raise
236
237     # chrome_branch_number is set in _PostBrowserStartupInitialization.
238     # Without --skip-hwid-check (introduced in crrev.com/203397), devices/VMs
239     # will be stuck on the bad hwid screen.
240     if self.chrome_branch_number <= 1500 and not self.hwid:
241       raise exceptions.LoginException(
242           'Hardware id not set on device/VM. --skip-hwid-check not supported '
243           'with chrome branches 1500 or earlier.')
244
245     util.WaitFor(lambda: self.oobe_exists, 10)
246
247     if self.browser_options.auto_login:
248       if self._is_guest:
249         pid = self.pid
250         self._NavigateGuestLogin()
251         # Guest browsing shuts down the current browser and launches an
252         # incognito browser in a separate process, which we need to wait for.
253         util.WaitFor(lambda: pid != self.pid, 10)
254         self._WaitForBrowserToComeUp()
255       else:
256         self._NavigateLogin()
257
258     logging.info('Browser is up!')
259
260   def Close(self):
261     super(CrOSBrowserBackend, self).Close()
262
263     self._RestartUI() # Logs out.
264
265     if self._forwarder:
266       self._forwarder.Close()
267       self._forwarder = None
268
269     if self._login_ext_dir:
270       self._cri.RmRF(self._login_ext_dir)
271       self._login_ext_dir = None
272
273     for e in self._extensions_to_load:
274       self._cri.RmRF(os.path.dirname(e.local_path))
275
276     self._cri = None
277
278   @property
279   @decorators.Cache
280   def forwarder_factory(self):
281     return cros_forwarder.CrOsForwarderFactory(self._cri)
282
283   def IsBrowserRunning(self):
284     return bool(self.pid)
285
286   def GetStandardOutput(self):
287     return 'Cannot get standard output on CrOS'
288
289   def GetStackTrace(self):
290     return 'Cannot get stack trace on CrOS'
291
292   def _RestartUI(self):
293     if self._cri:
294       logging.info('(Re)starting the ui (logs the user out)')
295       if self._cri.IsServiceRunning('ui'):
296         self._cri.RunCmdOnDevice(['restart', 'ui'])
297       else:
298         self._cri.RunCmdOnDevice(['start', 'ui'])
299
300   @property
301   def oobe(self):
302     return self.misc_web_contents_backend.GetOobe()
303
304   @property
305   def oobe_exists(self):
306     return self.misc_web_contents_backend.oobe_exists
307
308   def _SigninUIState(self):
309     """Returns the signin ui state of the oobe. HIDDEN: 0, GAIA_SIGNIN: 1,
310     ACCOUNT_PICKER: 2, WRONG_HWID_WARNING: 3, MANAGED_USER_CREATION_FLOW: 4.
311     These values are in
312     chrome/browser/resources/chromeos/login/display_manager.js
313     """
314     return self.oobe.EvaluateJavaScript('''
315       loginHeader = document.getElementById('login-header-bar')
316       if (loginHeader) {
317         loginHeader.signinUIState_;
318       }
319     ''')
320
321   def _CryptohomePath(self, user):
322     (path, _) = self._cri.RunCmdOnDevice(['cryptohome-path', 'user',
323                                           "'%s'" % user])
324     return path
325
326   def _IsCryptohomeMounted(self):
327     """Returns True if a cryptohome vault at the user mount point."""
328     profile_path = self._CryptohomePath(self.browser_options.username)
329     mount = self._cri.FilesystemMountedAt(profile_path)
330     return mount and mount.startswith('/home/.shadow/')
331
332   def _HandleUserImageSelectionScreen(self):
333     """If we're stuck on the user image selection screen, we click the ok
334     button.
335     """
336     oobe = self.oobe
337     if oobe:
338       try:
339         oobe.EvaluateJavaScript("""
340             var ok = document.getElementById("ok-button");
341             if (ok) {
342               ok.click();
343             }
344         """)
345       except (exceptions.TabCrashException):
346         pass
347
348   def _IsLoggedIn(self):
349     """Returns True if we're logged in (cryptohome has mounted), and the oobe
350     has been dismissed."""
351     if self.chrome_branch_number <= 1547:
352       self._HandleUserImageSelectionScreen()
353     return self._IsCryptohomeMounted() and not self.oobe_exists
354
355   def _StartupWindow(self):
356     """Closes the startup window, which is an extension on official builds,
357     and a webpage on chromiumos"""
358     startup_window_ext_id = 'honijodknafkokifofgiaalefdiedpko'
359     return (self.extension_dict_backend[startup_window_ext_id]
360         if startup_window_ext_id in self.extension_dict_backend
361         else self.tab_list_backend.Get(0, None))
362
363   def _WaitForSigninScreen(self):
364     """Waits for oobe to be on the signin or account picker screen."""
365     def OnAccountPickerScreen():
366       signin_state = self._SigninUIState()
367       # GAIA_SIGNIN or ACCOUNT_PICKER screens.
368       return signin_state == 1 or signin_state == 2
369     try:
370       util.WaitFor(OnAccountPickerScreen, 60)
371     except util.TimeoutException:
372       self._cri.TakeScreenShot('guest-screen')
373       raise exceptions.LoginException('Timed out waiting for signin screen, '
374                                       'signin state %d' % self._SigninUIState())
375
376   def _ClickBrowseAsGuest(self):
377     """Click the Browse As Guest button on the account picker screen. This will
378     restart the browser, and we could have a tab crash or a browser crash."""
379     try:
380       self.oobe.EvaluateJavaScript("""
381           var guest = document.getElementById("guest-user-button");
382           if (guest) {
383             guest.click();
384           }
385       """)
386     except (exceptions.TabCrashException,
387             exceptions.BrowserConnectionGoneException):
388       pass
389
390   def _WaitForGuestFsMounted(self):
391     """Waits for the guest user to be mounted as guestfs"""
392     guest_path = self._CryptohomePath('$guest')
393     util.WaitFor(lambda: (self._cri.FilesystemMountedAt(guest_path) ==
394                           'guestfs'), 20)
395
396   def _NavigateGuestLogin(self):
397     """Navigates through oobe login screen as guest"""
398     if not self.oobe_exists:
399       raise exceptions.LoginException('Oobe missing')
400     self._WaitForSigninScreen()
401     self._ClickBrowseAsGuest()
402     self._WaitForGuestFsMounted()
403
404   def _NavigateLogin(self):
405     """Navigates through oobe login screen"""
406     if self._use_oobe_login_for_testing:
407       logging.info('Invoking Oobe.loginForTesting')
408       if not self.oobe_exists:
409         raise exceptions.LoginException('Oobe missing')
410       oobe = self.oobe
411       util.WaitFor(lambda: oobe.EvaluateJavaScript(
412           'typeof Oobe !== \'undefined\''), 10)
413
414       if oobe.EvaluateJavaScript(
415           'typeof Oobe.loginForTesting == \'undefined\''):
416         raise exceptions.LoginException('Oobe.loginForTesting js api missing')
417
418       oobe.ExecuteJavaScript(
419           'Oobe.loginForTesting(\'%s\', \'%s\');'
420               % (self.browser_options.username, self.browser_options.password))
421
422     try:
423       util.WaitFor(self._IsLoggedIn, 60)
424     except util.TimeoutException:
425       self._cri.TakeScreenShot('login-screen')
426       raise exceptions.LoginException('Timed out going through login screen')
427
428     # Wait for extensions to load.
429     try:
430       self._WaitForBrowserToComeUp()
431     except util.TimeoutException:
432       logging.error('Chrome args: %s' % self._GetChromeProcess()['args'])
433       self._cri.TakeScreenShot('extension-timeout')
434       raise
435
436     if self.chrome_branch_number < 1500:
437       # Wait for the startup window, then close it. Startup window doesn't exist
438       # post-M27. crrev.com/197900
439       util.WaitFor(self._StartupWindow, 20).Close()
440     else:
441       # Workaround for crbug.com/329271, crbug.com/334726.
442       retries = 3
443       while not len(self.tab_list_backend):
444         try:
445           # Open a new window/tab.
446           tab = self.tab_list_backend.New(timeout=30)
447           tab.Navigate('about:blank', timeout=10)
448         except (exceptions.TabCrashException, util.TimeoutException):
449           retries -= 1
450           logging.warn('TabCrashException/TimeoutException in '
451                        'new tab creation/navigation, '
452                        'remaining retries %d' % retries)
453           if not retries:
454             raise