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