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