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.
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
21 class AndroidBrowserBackendSettings(object):
23 def __init__(self, adb, activity, cmdline_file, package, pseudo_exec_name,
24 supports_tab_control):
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
32 def GetDevtoolsRemotePort(self):
33 raise NotImplementedError()
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))
41 def PushProfile(self, _):
42 logging.critical('Profiles cannot be overriden with current configuration')
46 def is_content_shell(self):
50 def profile_dir(self):
51 return '/data/data/%s/' % self.package
54 class ChromeBackendSettings(AndroidBrowserBackendSettings):
55 # Stores a default Preferences file, re-used to speed up "--page-repeat".
56 _default_preferences_file = None
59 def _GetCommandLineFile(adb):
61 return '/data/local/tmp/chrome-command-line'
63 return '/data/local/chrome-command-line'
65 def __init__(self, adb, package):
66 super(ChromeBackendSettings, self).__init__(
68 activity='com.google.android.apps.chrome.Main',
69 cmdline_file=ChromeBackendSettings._GetCommandLineFile(adb),
71 pseudo_exec_name='chrome',
72 supports_tab_control=True)
74 def GetDevtoolsRemotePort(self):
75 return 'localabstract:chrome_devtools_remote'
77 def PushProfile(self, new_profile_dir):
78 # Clear the old profile first, since copying won't delete files.
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.
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 self.adb.Adb().PushIfNeeded(new_profile_dir, saved_profile_location)
93 self.adb.RunShellCommand('cp -r %s/* %s' % (saved_profile_location,
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)
103 paths = ['%s/%s' % (self.profile_dir, f) for f in files]
105 extended_path = '%s %s/* %s/*/* %s/*/*/*' % (path, path, path, path)
106 self.adb.RunShellCommand('chown %s.%s %s' %
107 (uid, uid, extended_path))
110 class ContentShellBackendSettings(AndroidBrowserBackendSettings):
111 def __init__(self, adb, package):
112 super(ContentShellBackendSettings, self).__init__(
114 activity='org.chromium.content_shell_apk.ContentShellActivity',
115 cmdline_file='/data/local/tmp/content-shell-command-line',
117 pseudo_exec_name='content_shell',
118 supports_tab_control=False)
120 def GetDevtoolsRemotePort(self):
121 return 'localabstract:content_shell_devtools_remote'
124 def is_content_shell(self):
128 class ChromiumTestShellBackendSettings(AndroidBrowserBackendSettings):
129 def __init__(self, adb, package):
130 super(ChromiumTestShellBackendSettings, self).__init__(
132 activity='org.chromium.chrome.testshell.ChromiumTestShellActivity',
133 cmdline_file='/data/local/tmp/chromium-testshell-command-line',
135 pseudo_exec_name='chromium_testshell',
136 supports_tab_control=False)
138 def GetDevtoolsRemotePort(self):
139 return 'localabstract:chromium_testshell_devtools_remote'
142 def is_content_shell(self):
146 class WebviewBackendSettings(AndroidBrowserBackendSettings):
147 def __init__(self, adb, package):
148 super(WebviewBackendSettings, self).__init__(
150 activity='com.android.webview.chromium.shell.TelemetryActivity',
151 cmdline_file='/data/local/tmp/webview-command-line',
153 pseudo_exec_name='webview',
154 supports_tab_control=False)
156 def GetDevtoolsRemotePort(self):
157 # The DevTools socket name for WebView depends on the activity PID's.
162 pids = self.adb.ExtractPid(self.package)
170 logging.critical('android_browser_backend: Timeout while waiting for '
171 'activity %s:%s to come up',
174 raise exceptions.BrowserGoneException('Timeout waiting for PID.')
175 return 'localabstract:webview_devtools_remote_%s' % str(pid)
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.')
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 = ''
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()
201 self._adb.CloseApplication(self._backend_settings.package)
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)
209 self._forwarder_factory = android_forwarder.AndroidForwarderFactory(
210 self._adb, use_rndis_forwarder)
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))
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)
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)
235 if ' ' not in values:
237 if values[0] in '"\'' and values[-1] == values[0]:
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))
244 self._SetCommandLineFile(args)
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)
252 parent_name = os.path.dirname(name)
253 if parent_name != '':
254 return IsProtectedFile(parent_name)
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.')
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)
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,
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:
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
284 self._adb.Adb().DismissCrashDialogIfNeeded()
285 self._adb.StartActivity(self._backend_settings.package,
286 self._backend_settings.activity,
292 self._adb.Forward('tcp:%d' % self._port,
293 self._backend_settings.GetDevtoolsRemotePort())
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():
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.')
309 traceback.print_exc()
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(
319 if '--host-resolver-rules' not in self._saved_cmdline else '')
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')
332 def forwarder_factory(self):
333 return self._forwarder_factory
341 pids = self._adb.ExtractPid(self._backend_settings.package)
343 raise exceptions.BrowserGoneException(self.GetStackTrace())
347 def browser_directory(self):
351 def profile_directory(self):
352 return self._backend_settings.profile_dir
356 return self._backend_settings.package
360 return self._backend_settings.activity
363 def supports_tab_control(self):
364 return self._backend_settings.supports_tab_control
370 super(AndroidBrowserBackend, self).Close()
371 self._adb.CloseApplication(self._backend_settings.package)
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)
384 # Don't pull lib, since it is created by the installer.
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)
394 def IsBrowserRunning(self):
395 pids = self._adb.ExtractPid(self._backend_settings.package)
396 return len(pids) != 0
398 def GetRemotePort(self, local_port):
401 def GetStandardOutput(self):
402 return '\n'.join(self._adb.RunShellCommand('logcat -d -t 500'))
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])
418 # Try to get tombstones.
419 tombstones = os.path.join(util.GetChromiumSrcDir(), 'build', 'android',
421 if os.path.exists(tombstones):
422 ret += Decorate('Tombstones',
423 subprocess.Popen([tombstones, '-w', '--device',
425 stdout=subprocess.PIPE).communicate()[0])
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)