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.
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
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
26 class AndroidBrowserBackendSettings(object):
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
37 def GetCommandLineFile(self, is_user_debug_build): # pylint: disable=W0613
38 return self._cmdline_file
40 def GetDevtoolsRemotePort(self, adb):
41 raise NotImplementedError()
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)
50 def PushProfile(self, _new_profile_dir, _adb):
51 logging.critical('Profiles cannot be overriden with current configuration')
55 def profile_dir(self):
56 return '/data/data/%s/' % self.package
59 class ChromeBackendSettings(AndroidBrowserBackendSettings):
60 # Stores a default Preferences file, re-used to speed up "--page-repeat".
61 _default_preferences_file = None
63 def GetCommandLineFile(self, is_user_debug_build):
64 if is_user_debug_build:
65 return '/data/local/tmp/chrome-command-line'
67 return '/data/local/chrome-command-line'
69 def __init__(self, package):
70 super(ChromeBackendSettings, self).__init__(
71 activity='com.google.android.apps.chrome.Main',
74 pseudo_exec_name='chrome',
75 supports_tab_control=True)
77 def GetDevtoolsRemotePort(self, adb):
78 return 'localabstract:chrome_devtools_remote'
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.
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.
89 profile_base = os.path.basename(profile_parent)
91 saved_profile_location = '/sdcard/profile/%s' % profile_base
92 adb.device().PushChangedFiles(new_profile_dir, saved_profile_location)
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)
103 paths = ['%s/%s' % (self.profile_dir, f) for f in files]
105 extended_path = '%s %s/* %s/*/* %s/*/*/*' % (path, path, path, path)
106 adb.device().RunShellCommand(
107 'chown %s.%s %s' % (uid, uid, extended_path))
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',
115 pseudo_exec_name='content_shell',
116 supports_tab_control=False)
118 def GetDevtoolsRemotePort(self, adb):
119 return 'localabstract:content_shell_devtools_remote'
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',
128 pseudo_exec_name='chrome_shell',
129 supports_tab_control=False)
131 def GetDevtoolsRemotePort(self, adb):
132 return 'localabstract:chrome_shell_devtools_remote'
134 class WebviewBackendSettings(AndroidBrowserBackendSettings):
135 def __init__(self, package,
136 activity='org.chromium.telemetry_shell.TelemetryActivity'):
137 super(WebviewBackendSettings, self).__init__(
139 cmdline_file='/data/local/tmp/webview-command-line',
141 pseudo_exec_name='webview',
142 supports_tab_control=False)
144 def GetDevtoolsRemotePort(self, adb):
145 # The DevTools socket name for WebView depends on the activity PID's.
150 pids = adb.ExtractPid(self.package)
158 logging.critical('android_browser_backend: Timeout while waiting for '
159 'activity %s:%s to come up',
162 raise exceptions.BrowserGoneException(self.browser,
163 'Timeout waiting for PID.')
164 return 'localabstract:webview_devtools_remote_%s' % str(pid)
166 class WebviewShellBackendSettings(WebviewBackendSettings):
167 def __init__(self, package):
168 super(WebviewShellBackendSettings, self).__init__(
169 activity='org.chromium.android_webview.shell.AwShellActivity',
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.')
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 = ''
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()
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')
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,
211 elif not self.browser_options.dont_override_profile:
212 self._backend_settings.RemoveProfile(self._adb)
214 self._forwarder_factory = android_forwarder.AndroidForwarderFactory(
215 self._adb, use_rndis_forwarder)
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))
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)
232 return self._android_platform_backend.adb
234 def _KillBrowser(self):
235 # We use KillAll rather than ForceStop for efficiency reasons.
237 self._adb.device().KillAll(self._backend_settings.package, retries=0)
238 except device_errors.CommandFailedError:
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)
251 if ' ' not in values:
253 if values[0] in '"\'' and values[-1] == values[0]:
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()
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
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.')
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,
285 self._SetUpCommandLine()
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:
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
296 # Dismiss any error dialogs. Limit the number in case we have an error loop
297 # or we are failing to dismiss.
299 if not self._adb.device().old_interface.DismissCrashDialogIfNeeded():
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),
307 self._adb.Forward('tcp:%d' % self._port,
308 self._backend_settings.GetDevtoolsRemotePort(self._adb))
311 self._WaitForBrowserToComeUp()
312 except exceptions.BrowserGoneException:
313 logging.critical('Failed to connect to browser.')
314 if not self._adb.device().old_interface.CanAccessProtectedFileContents():
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.')
323 traceback.print_exc()
327 self._RestoreCommandLine()
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')
340 def forwarder_factory(self):
341 return self._forwarder_factory
349 pids = self._adb.ExtractPid(self._backend_settings.package)
351 raise exceptions.BrowserGoneException(self.browser)
355 def browser_directory(self):
359 def profile_directory(self):
360 return self._backend_settings.profile_dir
364 return self._backend_settings.package
368 return self._backend_settings.activity
374 super(AndroidBrowserBackend, self).Close()
377 # Restore android.net SSL check
378 if self._backend_settings.relax_ssl_check:
379 self._adb.device().SetProp('socket.relaxsslcheck', self._saved_sslflag)
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)
392 # Don't pull lib, since it is created by the installer.
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)
403 def IsBrowserRunning(self):
404 pids = self._adb.ExtractPid(self._backend_settings.package)
405 return len(pids) != 0
407 def GetRemotePort(self, local_port):
410 def GetStandardOutput(self):
411 return '\n'.join(self._adb.device().RunShellCommand('logcat -d -t 500'))
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):
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])
429 # Try to get tombstones.
430 tombstones = os.path.join(util.GetChromiumSrcDir(), 'build', 'android',
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])
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)