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