Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / tools / telemetry / telemetry / core / backends / chrome / android_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 import pipes
8 import re
9 import subprocess
10 import sys
11 import time
12
13 from telemetry.core import exceptions
14 from telemetry.core import forwarders
15 from telemetry.core import util
16 from telemetry.core.backends import adb_commands
17 from telemetry.core.backends import browser_backend
18 from telemetry.core.backends.chrome import chrome_browser_backend
19 from telemetry.core.forwarders import android_forwarder
20
21 util.AddDirToPythonPath(util.GetChromiumSrcDir(), 'build', 'android')
22 from pylib.device import device_errors  # pylint: disable=F0401
23 from pylib.device import intent  # pylint: disable=F0401
24
25
26 class AndroidBrowserBackendSettings(object):
27
28   def __init__(self, activity, cmdline_file, package, pseudo_exec_name,
29                supports_tab_control, relax_ssl_check=False):
30     self.activity = activity
31     self._cmdline_file = cmdline_file
32     self.package = package
33     self.pseudo_exec_name = pseudo_exec_name
34     self.supports_tab_control = supports_tab_control
35     self.relax_ssl_check = relax_ssl_check
36
37   def GetCommandLineFile(self, is_user_debug_build):  # pylint: disable=W0613
38     return self._cmdline_file
39
40   def GetDevtoolsRemotePort(self, adb):
41     raise NotImplementedError()
42
43   def RemoveProfile(self, adb):
44     files = adb.device().RunShellCommand(
45         'ls "%s"' % self.profile_dir, as_root=True)
46     # Don't delete lib, since it is created by the installer.
47     paths = ['"%s/%s"' % (self.profile_dir, f) for f in files if f != 'lib']
48     adb.device().RunShellCommand('rm -r %s' % ' '.join(paths), as_root=True)
49
50   def PushProfile(self, _new_profile_dir, _adb):
51     logging.critical('Profiles cannot be overriden with current configuration')
52     sys.exit(1)
53
54   @property
55   def profile_dir(self):
56     return '/data/data/%s/' % self.package
57
58
59 class ChromeBackendSettings(AndroidBrowserBackendSettings):
60   # Stores a default Preferences file, re-used to speed up "--page-repeat".
61   _default_preferences_file = None
62
63   def GetCommandLineFile(self, is_user_debug_build):
64     if is_user_debug_build:
65       return '/data/local/tmp/chrome-command-line'
66     else:
67       return '/data/local/chrome-command-line'
68
69   def __init__(self, package):
70     super(ChromeBackendSettings, self).__init__(
71         activity='com.google.android.apps.chrome.Main',
72         cmdline_file=None,
73         package=package,
74         pseudo_exec_name='chrome',
75         supports_tab_control=True)
76
77   def GetDevtoolsRemotePort(self, adb):
78     return 'localabstract:chrome_devtools_remote'
79
80   def PushProfile(self, new_profile_dir, adb):
81     # Pushing the profile is slow, so we don't want to do it every time.
82     # Avoid this by pushing to a safe location using PushChangedFiles, and
83     # then copying into the correct location on each test run.
84
85     (profile_parent, profile_base) = os.path.split(new_profile_dir)
86     # If the path ends with a '/' python split will return an empty string for
87     # the base name; so we now need to get the base name from the directory.
88     if not profile_base:
89       profile_base = os.path.basename(profile_parent)
90
91     saved_profile_location = '/sdcard/profile/%s' % profile_base
92     adb.device().PushChangedFiles(new_profile_dir, saved_profile_location)
93
94     adb.device().old_interface.EfficientDeviceDirectoryCopy(
95         saved_profile_location, self.profile_dir)
96     dumpsys = adb.device().RunShellCommand(
97         'dumpsys package %s' % self.package)
98     id_line = next(line for line in dumpsys if 'userId=' in line)
99     uid = re.search('\d+', id_line).group()
100     files = adb.device().RunShellCommand(
101         'ls "%s"' % self.profile_dir, as_root=True)
102     files.remove('lib')
103     paths = ['%s/%s' % (self.profile_dir, f) for f in files]
104     for path in paths:
105       extended_path = '%s %s/* %s/*/* %s/*/*/*' % (path, path, path, path)
106       adb.device().RunShellCommand(
107           'chown %s.%s %s' % (uid, uid, extended_path))
108
109 class ContentShellBackendSettings(AndroidBrowserBackendSettings):
110   def __init__(self, package):
111     super(ContentShellBackendSettings, self).__init__(
112         activity='org.chromium.content_shell_apk.ContentShellActivity',
113         cmdline_file='/data/local/tmp/content-shell-command-line',
114         package=package,
115         pseudo_exec_name='content_shell',
116         supports_tab_control=False)
117
118   def GetDevtoolsRemotePort(self, adb):
119     return 'localabstract:content_shell_devtools_remote'
120
121
122 class ChromeShellBackendSettings(AndroidBrowserBackendSettings):
123   def __init__(self, package):
124     super(ChromeShellBackendSettings, self).__init__(
125           activity='org.chromium.chrome.shell.ChromeShellActivity',
126           cmdline_file='/data/local/tmp/chrome-shell-command-line',
127           package=package,
128           pseudo_exec_name='chrome_shell',
129           supports_tab_control=False)
130
131   def GetDevtoolsRemotePort(self, adb):
132     return 'localabstract:chrome_shell_devtools_remote'
133
134 class WebviewBackendSettings(AndroidBrowserBackendSettings):
135   def __init__(self, package,
136                activity='org.chromium.telemetry_shell.TelemetryActivity'):
137     super(WebviewBackendSettings, self).__init__(
138         activity=activity,
139         cmdline_file='/data/local/tmp/webview-command-line',
140         package=package,
141         pseudo_exec_name='webview',
142         supports_tab_control=False)
143
144   def GetDevtoolsRemotePort(self, adb):
145     # The DevTools socket name for WebView depends on the activity PID's.
146     retries = 0
147     timeout = 1
148     pid = None
149     while True:
150       pids = adb.ExtractPid(self.package)
151       if (len(pids) > 0):
152         pid = pids[-1]
153         break
154       time.sleep(timeout)
155       retries += 1
156       timeout *= 2
157       if retries == 4:
158         logging.critical('android_browser_backend: Timeout while waiting for '
159                          'activity %s:%s to come up',
160                          self.package,
161                          self.activity)
162         raise exceptions.BrowserGoneException(self.browser,
163                                               'Timeout waiting for PID.')
164     return 'localabstract:webview_devtools_remote_%s' % str(pid)
165
166 class WebviewShellBackendSettings(WebviewBackendSettings):
167   def __init__(self, package):
168     super(WebviewShellBackendSettings, self).__init__(
169         activity='org.chromium.android_webview.shell.AwShellActivity',
170         package=package)
171
172 class AndroidBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
173   """The backend for controlling a browser instance running on Android."""
174   def __init__(self, browser_options, backend_settings, use_rndis_forwarder,
175                output_profile_path, extensions_to_load, target_arch,
176                android_platform_backend):
177     super(AndroidBrowserBackend, self).__init__(
178         supports_tab_control=backend_settings.supports_tab_control,
179         supports_extensions=False, browser_options=browser_options,
180         output_profile_path=output_profile_path,
181         extensions_to_load=extensions_to_load)
182     if len(extensions_to_load) > 0:
183       raise browser_backend.ExtensionsNotSupportedException(
184           'Android browser does not support extensions.')
185
186     # Initialize fields so that an explosion during init doesn't break in Close.
187     self._android_platform_backend = android_platform_backend
188     self._backend_settings = backend_settings
189     self._saved_cmdline = ''
190     self._target_arch = target_arch
191     self._saved_sslflag = ''
192
193     # TODO(tonyg): This is flaky because it doesn't reserve the port that it
194     # allocates. Need to fix this.
195     self._port = adb_commands.AllocateTestServerPort()
196
197     # Disables android.net SSL certificate check.  This is necessary for
198     # applications using the android.net stack to work with proxy HTTPS server
199     # created by telemetry
200     if self._backend_settings.relax_ssl_check:
201       self._saved_sslflag = self._adb.device().GetProp('socket.relaxsslcheck')
202       self._adb.device().SetProp('socket.relaxsslcheck', 'yes')
203
204     # Kill old browser.
205     self._KillBrowser()
206
207     if self._adb.device().old_interface.CanAccessProtectedFileContents():
208       if self.browser_options.profile_dir:
209         self._backend_settings.PushProfile(self.browser_options.profile_dir,
210                                            self._adb)
211       elif not self.browser_options.dont_override_profile:
212         self._backend_settings.RemoveProfile(self._adb)
213
214     self._forwarder_factory = android_forwarder.AndroidForwarderFactory(
215         self._adb, use_rndis_forwarder)
216
217     if self.browser_options.netsim or use_rndis_forwarder:
218       assert use_rndis_forwarder, 'Netsim requires RNDIS forwarding.'
219       self.wpr_port_pairs = forwarders.PortPairs(
220           http=forwarders.PortPair(0, 80),
221           https=forwarders.PortPair(0, 443),
222           dns=forwarders.PortPair(0, 53))
223
224     # Set the debug app if needed.
225     if self._adb.IsUserBuild():
226       logging.debug('User build device, setting debug app')
227       self._adb.device().RunShellCommand(
228           'am set-debug-app --persistent %s' % self._backend_settings.package)
229
230   @property
231   def _adb(self):
232     return self._android_platform_backend.adb
233
234   def _KillBrowser(self):
235     # We use KillAll rather than ForceStop for efficiency reasons.
236     try:
237       self._adb.device().KillAll(self._backend_settings.package, retries=0)
238     except device_errors.CommandFailedError:
239       pass
240
241   def _SetUpCommandLine(self):
242     def QuoteIfNeeded(arg):
243       # Properly escape "key=valueA valueB" to "key='valueA valueB'"
244       # Values without spaces, or that seem to be quoted are left untouched.
245       # This is required so CommandLine.java can parse valueB correctly rather
246       # than as a separate switch.
247       params = arg.split('=', 1)
248       if len(params) != 2:
249         return arg
250       key, values = params
251       if ' ' not in values:
252         return arg
253       if values[0] in '"\'' and values[-1] == values[0]:
254         return arg
255       return '%s=%s' % (key, pipes.quote(values))
256     args = [self._backend_settings.pseudo_exec_name]
257     args.extend(self.GetBrowserStartupArgs())
258     content = ' '.join(QuoteIfNeeded(arg) for arg in args)
259     cmdline_file = self._backend_settings.GetCommandLineFile(
260         self._adb.IsUserBuild())
261     as_root = self._adb.device().old_interface.CanAccessProtectedFileContents()
262
263     try:
264       # Save the current command line to restore later, except if it appears to
265       # be a  Telemetry created one. This is to prevent a common bug where
266       # --host-resolver-rules borks people's browsers if something goes wrong
267       # with Telemetry.
268       self._saved_cmdline = ''.join(self._adb.device().ReadFile(cmdline_file))
269       if '--host-resolver-rules' in self._saved_cmdline:
270         self._saved_cmdline = ''
271       self._adb.device().WriteTextFile(cmdline_file, content, as_root=as_root)
272     except device_errors.CommandFailedError:
273       logging.critical('Cannot set Chrome command line. '
274                        'Fix this by flashing to a userdebug build.')
275       sys.exit(1)
276
277   def _RestoreCommandLine(self):
278     cmdline_file = self._backend_settings.GetCommandLineFile(
279         self._adb.IsUserBuild())
280     as_root = self._adb.device().old_interface.CanAccessProtectedFileContents()
281     self._adb.device().WriteTextFile(cmdline_file, self._saved_cmdline,
282                                      as_root=as_root)
283
284   def Start(self):
285     self._SetUpCommandLine()
286
287     self._adb.device().RunShellCommand('logcat -c')
288     if self.browser_options.startup_url:
289       url = self.browser_options.startup_url
290     elif self.browser_options.profile_dir:
291       url = None
292     else:
293       # If we have no existing tabs start with a blank page since default
294       # startup with the NTP can lead to race conditions with Telemetry
295       url = 'about:blank'
296     # Dismiss any error dialogs. Limit the number in case we have an error loop
297     # or we are failing to dismiss.
298     for _ in xrange(10):
299       if not self._adb.device().old_interface.DismissCrashDialogIfNeeded():
300         break
301     self._adb.device().StartActivity(
302         intent.Intent(package=self._backend_settings.package,
303                       activity=self._backend_settings.activity,
304                       action=None, data=url, category=None),
305         blocking=True)
306
307     self._adb.Forward('tcp:%d' % self._port,
308                       self._backend_settings.GetDevtoolsRemotePort(self._adb))
309
310     try:
311       self._WaitForBrowserToComeUp()
312     except exceptions.BrowserGoneException:
313       logging.critical('Failed to connect to browser.')
314       if not self._adb.device().old_interface.CanAccessProtectedFileContents():
315         logging.critical(
316           'Resolve this by either: '
317           '(1) Flashing to a userdebug build OR '
318           '(2) Manually enabling web debugging in Chrome at '
319           'Settings > Developer tools > Enable USB Web debugging.')
320       sys.exit(1)
321     except:
322       import traceback
323       traceback.print_exc()
324       self.Close()
325       raise
326     finally:
327       self._RestoreCommandLine()
328
329   def GetBrowserStartupArgs(self):
330     args = super(AndroidBrowserBackend, self).GetBrowserStartupArgs()
331     if self.forwarder_factory.does_forwarder_override_dns:
332       args = [arg for arg in args
333               if not arg.startswith('--host-resolver-rules')]
334     args.append('--enable-remote-debugging')
335     args.append('--disable-fre')
336     args.append('--disable-external-intent-requests')
337     return args
338
339   @property
340   def forwarder_factory(self):
341     return self._forwarder_factory
342
343   @property
344   def adb(self):
345     return self._adb
346
347   @property
348   def pid(self):
349     pids = self._adb.ExtractPid(self._backend_settings.package)
350     if not pids:
351       raise exceptions.BrowserGoneException(self.browser)
352     return int(pids[0])
353
354   @property
355   def browser_directory(self):
356     return None
357
358   @property
359   def profile_directory(self):
360     return self._backend_settings.profile_dir
361
362   @property
363   def package(self):
364     return self._backend_settings.package
365
366   @property
367   def activity(self):
368     return self._backend_settings.activity
369
370   def __del__(self):
371     self.Close()
372
373   def Close(self):
374     super(AndroidBrowserBackend, self).Close()
375     self._KillBrowser()
376
377     # Restore android.net SSL check
378     if self._backend_settings.relax_ssl_check:
379       self._adb.device().SetProp('socket.relaxsslcheck', self._saved_sslflag)
380
381     if self._output_profile_path:
382       logging.info("Pulling profile directory from device: '%s'->'%s'.",
383                    self._backend_settings.profile_dir,
384                    self._output_profile_path)
385       # To minimize bandwidth it might be good to look at whether all the data
386       # pulled down is really needed e.g. .pak files.
387       if not os.path.exists(self._output_profile_path):
388         os.makedirs(self._output_profile_pathame)
389       files = self.adb.device().RunShellCommand(
390           'ls "%s"' % self._backend_settings.profile_dir)
391       for f in files:
392         # Don't pull lib, since it is created by the installer.
393         if f != 'lib':
394           source = '%s%s' % (self._backend_settings.profile_dir, f)
395           dest = os.path.join(self._output_profile_path, f)
396           # self._adb.Pull(source, dest) doesn't work because its timeout
397           # is fixed in android's adb_interface at 60 seconds, which may
398           # be too short to pull the cache.
399           cmd = 'pull %s %s' % (source, dest)
400           self._adb.device().old_interface.Adb().SendCommand(
401               cmd, timeout_time=240)
402
403   def IsBrowserRunning(self):
404     pids = self._adb.ExtractPid(self._backend_settings.package)
405     return len(pids) != 0
406
407   def GetRemotePort(self, local_port):
408     return local_port
409
410   def GetStandardOutput(self):
411     return '\n'.join(self._adb.device().RunShellCommand('logcat -d -t 500'))
412
413   def GetStackTrace(self):
414     def Decorate(title, content):
415       return title + '\n' + content + '\n' + '*' * 80 + '\n'
416     # Get the last lines of logcat (large enough to contain stacktrace)
417     logcat = self.GetStandardOutput()
418     ret = Decorate('Logcat', logcat)
419     stack = os.path.join(util.GetChromiumSrcDir(), 'third_party',
420                          'android_platform', 'development', 'scripts', 'stack')
421     # Try to symbolize logcat.
422     if os.path.exists(stack):
423       cmd = [stack]
424       if self._target_arch:
425         cmd.append('--arch=%s' % self._target_arch)
426       p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
427       ret += Decorate('Stack from Logcat', p.communicate(input=logcat)[0])
428
429     # Try to get tombstones.
430     tombstones = os.path.join(util.GetChromiumSrcDir(), 'build', 'android',
431                               'tombstones.py')
432     if os.path.exists(tombstones):
433       ret += Decorate('Tombstones',
434                       subprocess.Popen([tombstones, '-w', '--device',
435                                         self._adb.device_serial()],
436                                        stdout=subprocess.PIPE).communicate()[0])
437     return ret
438
439   def AddReplayServerOptions(self, extra_wpr_args):
440     if not self.forwarder_factory.does_forwarder_override_dns:
441       extra_wpr_args.append('--no-dns_forwarding')
442     if self.browser_options.netsim:
443       extra_wpr_args.append('--net=%s' % self.browser_options.netsim)