- add sources.
[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 subprocess
8 import sys
9 import time
10
11 from telemetry.core import exceptions
12 from telemetry.core import util
13 from telemetry.core.backends import adb_commands
14 from telemetry.core.backends import android_rndis
15 from telemetry.core.backends import browser_backend
16 from telemetry.core.backends.chrome import chrome_browser_backend
17
18
19 class AndroidBrowserBackendSettings(object):
20   def __init__(self, adb, activity, cmdline_file, package, pseudo_exec_name,
21                supports_tab_control):
22     self.adb = adb
23     self.activity = activity
24     self.cmdline_file = cmdline_file
25     self.package = package
26     self.pseudo_exec_name = pseudo_exec_name
27     self.supports_tab_control = supports_tab_control
28
29   def GetDevtoolsRemotePort(self):
30     raise NotImplementedError()
31
32   def RemoveProfile(self):
33     self.adb.RunShellCommand(
34         'su -c rm -r "%s"' % self.profile_dir)
35
36   def PushProfile(self, _):
37     logging.critical('Profiles cannot be overriden with current configuration')
38     sys.exit(1)
39
40   @property
41   def is_content_shell(self):
42     return False
43
44   @property
45   def profile_dir(self):
46     raise NotImplementedError()
47
48
49 class ChromeBackendSettings(AndroidBrowserBackendSettings):
50   # Stores a default Preferences file, re-used to speed up "--page-repeat".
51   _default_preferences_file = None
52
53   def __init__(self, adb, package):
54     super(ChromeBackendSettings, self).__init__(
55         adb=adb,
56         activity='com.google.android.apps.chrome.Main',
57         cmdline_file='/data/local/chrome-command-line',
58         package=package,
59         pseudo_exec_name='chrome',
60         supports_tab_control=True)
61
62   def GetDevtoolsRemotePort(self):
63     return 'localabstract:chrome_devtools_remote'
64
65   def PushProfile(self, new_profile_dir):
66     self.adb.Push(new_profile_dir, self.profile_dir)
67
68   @property
69   def profile_dir(self):
70     return '/data/data/%s/app_chrome/' % self.package
71
72
73 class ContentShellBackendSettings(AndroidBrowserBackendSettings):
74   def __init__(self, adb, package):
75     super(ContentShellBackendSettings, self).__init__(
76         adb=adb,
77         activity='org.chromium.content_shell_apk.ContentShellActivity',
78         cmdline_file='/data/local/tmp/content-shell-command-line',
79         package=package,
80         pseudo_exec_name='content_shell',
81         supports_tab_control=False)
82
83   def GetDevtoolsRemotePort(self):
84     return 'localabstract:content_shell_devtools_remote'
85
86   @property
87   def is_content_shell(self):
88     return True
89
90   @property
91   def profile_dir(self):
92     return '/data/data/%s/app_content_shell/' % self.package
93
94
95 class ChromiumTestShellBackendSettings(AndroidBrowserBackendSettings):
96   def __init__(self, adb, package):
97     super(ChromiumTestShellBackendSettings, self).__init__(
98           adb=adb,
99           activity='org.chromium.chrome.testshell.ChromiumTestShellActivity',
100           cmdline_file='/data/local/tmp/chromium-testshell-command-line',
101           package=package,
102           pseudo_exec_name='chromium_testshell',
103           supports_tab_control=False)
104
105   def GetDevtoolsRemotePort(self):
106     return 'localabstract:chromium_testshell_devtools_remote'
107
108   @property
109   def is_content_shell(self):
110     return True
111
112   @property
113   def profile_dir(self):
114     return '/data/data/%s/app_chromiumtestshell/' % self.package
115
116
117 class WebviewBackendSettings(AndroidBrowserBackendSettings):
118   def __init__(self, adb, package):
119     super(WebviewBackendSettings, self).__init__(
120         adb=adb,
121         activity='com.android.webview.chromium.shell.TelemetryActivity',
122         cmdline_file='/data/local/tmp/webview-command-line',
123         package=package,
124         pseudo_exec_name='webview',
125         supports_tab_control=False)
126
127   def GetDevtoolsRemotePort(self):
128     # The DevTools socket name for WebView depends on the activity PID's.
129     retries = 0
130     timeout = 1
131     pid = None
132     while True:
133       pids = self.adb.ExtractPid(self.package)
134       if (len(pids) > 0):
135         pid = pids[-1]
136         break
137       time.sleep(timeout)
138       retries += 1
139       timeout *= 2
140       if retries == 4:
141         logging.critical('android_browser_backend: Timeout while waiting for '
142                          'activity %s:%s to come up',
143                          self.package,
144                          self.activity)
145         raise exceptions.BrowserGoneException('Timeout waiting for PID.')
146     return 'localabstract:webview_devtools_remote_%s' % str(pid)
147
148   @property
149   def profile_dir(self):
150     return '/data/data/%s/app_webview/' % self.package
151
152
153 class AndroidBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
154   """The backend for controlling a browser instance running on Android.
155   """
156   def __init__(self, browser_options, backend_settings, rndis,
157                output_profile_path, extensions_to_load):
158     super(AndroidBrowserBackend, self).__init__(
159         is_content_shell=backend_settings.is_content_shell,
160         supports_extensions=False, browser_options=browser_options,
161         output_profile_path=output_profile_path,
162         extensions_to_load=extensions_to_load)
163     if len(extensions_to_load) > 0:
164       raise browser_backend.ExtensionsNotSupportedException(
165           'Android browser does not support extensions.')
166
167     # Initialize fields so that an explosion during init doesn't break in Close.
168     self._adb = backend_settings.adb
169     self._backend_settings = backend_settings
170     self._saved_cmdline = None
171     if not self.browser_options.keep_test_server_ports:
172       adb_commands.ResetTestServerPortAllocation()
173     self._port = adb_commands.AllocateTestServerPort()
174
175     # Kill old browser.
176     self._adb.CloseApplication(self._backend_settings.package)
177
178     if self._adb.Adb().CanAccessProtectedFileContents():
179       if not self.browser_options.dont_override_profile:
180         self._backend_settings.RemoveProfile()
181       if self.browser_options.profile_dir:
182         self._backend_settings.PushProfile(self.browser_options.profile_dir)
183
184     # Pre-configure RNDIS forwarding.
185     self._rndis_forwarder = None
186     if rndis:
187       self._rndis_forwarder = android_rndis.RndisForwarderWithRoot(self._adb)
188       self.WEBPAGEREPLAY_HOST = self._rndis_forwarder.host_ip
189     # TODO(szym): only override DNS if WPR has privileges to proxy on port 25.
190     self._override_dns = False
191
192     self._SetUpCommandLine()
193
194   def _SetUpCommandLine(self):
195     def QuoteIfNeeded(arg):
196       # Escape 'key=valueA valueB' to 'key="valueA valueB"'
197       # Already quoted values, or values without space are left untouched.
198       # This is required so CommandLine.java can parse valueB correctly rather
199       # than as a separate switch.
200       params = arg.split('=')
201       if len(params) != 2:
202         return arg
203       key, values = params
204       if ' ' not in values:
205         return arg
206       if values[0] in '"\'' and values[-1] == values[0]:
207         return arg
208       return '%s="%s"' % (key, values)
209
210     args = [self._backend_settings.pseudo_exec_name]
211     args.extend(self.GetBrowserStartupArgs())
212     args = ' '.join(map(QuoteIfNeeded, args))
213
214     self._SetCommandLineFile(args)
215
216   def _SetCommandLineFile(self, file_contents):
217     def IsProtectedFile(name):
218       if self._adb.Adb().FileExistsOnDevice(name):
219         return not self._adb.Adb().IsFileWritableOnDevice(name)
220       else:
221         parent_name = os.path.dirname(name)
222         if parent_name != '':
223           return IsProtectedFile(parent_name)
224         else:
225           return True
226
227     if IsProtectedFile(self._backend_settings.cmdline_file):
228       if not self._adb.Adb().CanAccessProtectedFileContents():
229         logging.critical('Cannot set Chrome command line. '
230                          'Fix this by flashing to a userdebug build.')
231         sys.exit(1)
232       self._saved_cmdline = ''.join(self._adb.Adb().GetProtectedFileContents(
233           self._backend_settings.cmdline_file) or [])
234       self._adb.Adb().SetProtectedFileContents(
235           self._backend_settings.cmdline_file, file_contents)
236     else:
237       self._saved_cmdline = ''.join(self._adb.Adb().GetFileContents(
238           self._backend_settings.cmdline_file) or [])
239       self._adb.Adb().SetFileContents(self._backend_settings.cmdline_file,
240                                       file_contents)
241
242   def Start(self):
243     self._adb.RunShellCommand('logcat -c')
244     if self.browser_options.startup_url:
245       url = self.browser_options.startup_url
246     else:
247       url = 'about:blank'
248     self._adb.StartActivity(self._backend_settings.package,
249                             self._backend_settings.activity,
250                             True,
251                             None,
252                             None,
253                             url)
254
255     self._adb.Forward('tcp:%d' % self._port,
256                       self._backend_settings.GetDevtoolsRemotePort())
257
258     try:
259       self._WaitForBrowserToComeUp()
260       self._PostBrowserStartupInitialization()
261     except exceptions.BrowserGoneException:
262       logging.critical('Failed to connect to browser.')
263       if not self._adb.Adb().CanAccessProtectedFileContents():
264         logging.critical(
265           'Resolve this by either: '
266           '(1) Flashing to a userdebug build OR '
267           '(2) Manually enabling web debugging in Chrome at '
268           'Settings > Developer tools > Enable USB Web debugging.')
269       sys.exit(1)
270     except:
271       import traceback
272       traceback.print_exc()
273       self.Close()
274       raise
275     finally:
276       self._SetCommandLineFile(self._saved_cmdline or '')
277
278   def GetBrowserStartupArgs(self):
279     args = super(AndroidBrowserBackend, self).GetBrowserStartupArgs()
280     if self._override_dns:
281       args = [arg for arg in args
282               if not arg.startswith('--host-resolver-rules')]
283     args.append('--enable-remote-debugging')
284     args.append('--no-restore-state')
285     args.append('--disable-fre')
286     return args
287
288   @property
289   def adb(self):
290     return self._adb
291
292   @property
293   def pid(self):
294     pids = self._adb.ExtractPid(self._backend_settings.package)
295     if not pids:
296       raise exceptions.BrowserGoneException(self.GetStackTrace())
297     return int(pids[0])
298
299   @property
300   def browser_directory(self):
301     return None
302
303   @property
304   def profile_directory(self):
305     return self._backend_settings.profile_dir
306
307   @property
308   def package(self):
309     return self._backend_settings.package
310
311   @property
312   def activity(self):
313     return self._backend_settings.activity
314
315   @property
316   def supports_tab_control(self):
317     return self._backend_settings.supports_tab_control
318
319   def __del__(self):
320     self.Close()
321
322   def Close(self):
323     super(AndroidBrowserBackend, self).Close()
324     self._adb.CloseApplication(self._backend_settings.package)
325
326     if self._output_profile_path:
327       logging.info("Pulling profile directory from device: '%s'->'%s'.",
328                    self._backend_settings.profile_dir,
329                    self._output_profile_path)
330       # To minimize bandwidth it might be good to look at whether all the data
331       # pulled down is really needed e.g. .pak files.
332       self._adb.Pull(self._backend_settings.profile_dir,
333                      self._output_profile_path)
334
335   def IsBrowserRunning(self):
336     pids = self._adb.ExtractPid(self._backend_settings.package)
337     return len(pids) != 0
338
339   def GetRemotePort(self, local_port):
340     return local_port
341
342   def GetStandardOutput(self):
343     return '\n'.join(self._adb.RunShellCommand('logcat -d -t 500'))
344
345   def GetStackTrace(self):
346     def Decorate(title, content):
347       return title + '\n' + content + '\n' + '*' * 80 + '\n'
348     # Get the last lines of logcat (large enough to contain stacktrace)
349     logcat = self.GetStandardOutput()
350     ret = Decorate('Logcat', logcat)
351     stack = os.path.join(util.GetChromiumSrcDir(), 'third_party',
352                          'android_platform', 'development', 'scripts', 'stack')
353     # Try to symbolize logcat.
354     if os.path.exists(stack):
355       p = subprocess.Popen([stack], stdin=subprocess.PIPE,
356                            stdout=subprocess.PIPE)
357       ret += Decorate('Stack from Logcat', p.communicate(input=logcat)[0])
358
359     # Try to get tombstones.
360     tombstones = os.path.join(util.GetChromiumSrcDir(), 'build', 'android',
361                               'tombstones.py')
362     if os.path.exists(tombstones):
363       ret += Decorate('Tombstones',
364                       subprocess.Popen([tombstones, '-w', '--device',
365                                         self._adb.device()],
366                                        stdout=subprocess.PIPE).communicate()[0])
367     return ret
368
369   def AddReplayServerOptions(self, extra_wpr_args):
370     """Override. Only add --no-dns_forwarding if not using RNDIS."""
371     if not self._override_dns:
372       extra_wpr_args.append('--no-dns_forwarding')
373
374   def CreateForwarder(self, *port_pairs):
375     if self._rndis_forwarder:
376       forwarder = self._rndis_forwarder
377       forwarder.SetPorts(*port_pairs)
378       assert self.WEBPAGEREPLAY_HOST == forwarder.host_ip, (
379         'Host IP address on the RNDIS interface changed. Must restart browser!')
380       if self._override_dns:
381         forwarder.OverrideDns()
382       return forwarder
383     assert not self._override_dns, ('The user-space forwarder does not support '
384                                     'DNS override!')
385     logging.warning('Using the user-space forwarder.\n')
386     return adb_commands.Forwarder(self._adb, *port_pairs)