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 5import logging 6import os 7import pipes 8import re 9import subprocess 10import sys 11import time 12 13from telemetry.core import exceptions 14from telemetry.core import forwarders 15from telemetry.core import util 16from telemetry.core.backends import adb_commands 17from telemetry.core.backends import browser_backend 18from telemetry.core.backends.chrome import chrome_browser_backend 19from telemetry.core.forwarders import android_forwarder 20 21util.AddDirToPythonPath(util.GetChromiumSrcDir(), 'build', 'android') 22from pylib.device import device_errors # pylint: disable=F0401 23from pylib.device import intent # pylint: disable=F0401 24 25 26class AndroidBrowserBackendSettings(object): 27 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 36 37 def GetCommandLineFile(self, is_user_debug_build): # pylint: disable=W0613 38 return self._cmdline_file 39 40 def GetDevtoolsRemotePort(self, adb): 41 raise NotImplementedError() 42 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) 49 50 def PushProfile(self, _new_profile_dir, _adb): 51 logging.critical('Profiles cannot be overriden with current configuration') 52 sys.exit(1) 53 54 @property 55 def profile_dir(self): 56 return '/data/data/%s/' % self.package 57 58 59class ChromeBackendSettings(AndroidBrowserBackendSettings): 60 # Stores a default Preferences file, re-used to speed up "--page-repeat". 61 _default_preferences_file = None 62 63 def GetCommandLineFile(self, is_user_debug_build): 64 if is_user_debug_build: 65 return '/data/local/tmp/chrome-command-line' 66 else: 67 return '/data/local/chrome-command-line' 68 69 def __init__(self, package): 70 super(ChromeBackendSettings, self).__init__( 71 activity='com.google.android.apps.chrome.Main', 72 cmdline_file=None, 73 package=package, 74 pseudo_exec_name='chrome', 75 supports_tab_control=True) 76 77 def GetDevtoolsRemotePort(self, adb): 78 return 'localabstract:chrome_devtools_remote' 79 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. 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 adb.device().PushChangedFiles(new_profile_dir, saved_profile_location) 93 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) 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 adb.device().RunShellCommand( 107 'chown %s.%s %s' % (uid, uid, extended_path)) 108 109class 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', 114 package=package, 115 pseudo_exec_name='content_shell', 116 supports_tab_control=False) 117 118 def GetDevtoolsRemotePort(self, adb): 119 return 'localabstract:content_shell_devtools_remote' 120 121 122class 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', 127 package=package, 128 pseudo_exec_name='chrome_shell', 129 supports_tab_control=False) 130 131 def GetDevtoolsRemotePort(self, adb): 132 return 'localabstract:chrome_shell_devtools_remote' 133 134class WebviewBackendSettings(AndroidBrowserBackendSettings): 135 def __init__(self, package, 136 activity='org.chromium.telemetry_shell.TelemetryActivity'): 137 super(WebviewBackendSettings, self).__init__( 138 activity=activity, 139 cmdline_file='/data/local/tmp/webview-command-line', 140 package=package, 141 pseudo_exec_name='webview', 142 supports_tab_control=False) 143 144 def GetDevtoolsRemotePort(self, adb): 145 # The DevTools socket name for WebView depends on the activity PID's. 146 retries = 0 147 timeout = 1 148 pid = None 149 while True: 150 pids = adb.ExtractPid(self.package) 151 if (len(pids) > 0): 152 pid = pids[-1] 153 break 154 time.sleep(timeout) 155 retries += 1 156 timeout *= 2 157 if retries == 4: 158 logging.critical('android_browser_backend: Timeout while waiting for ' 159 'activity %s:%s to come up', 160 self.package, 161 self.activity) 162 raise exceptions.BrowserGoneException(self.browser, 163 'Timeout waiting for PID.') 164 return 'localabstract:webview_devtools_remote_%s' % str(pid) 165 166class WebviewShellBackendSettings(WebviewBackendSettings): 167 def __init__(self, package): 168 super(WebviewShellBackendSettings, self).__init__( 169 activity='org.chromium.android_webview.shell.AwShellActivity', 170 package=package) 171 172class 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.') 185 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 = '' 192 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() 196 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') 203 204 # Kill old browser. 205 self._KillBrowser() 206 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, 210 self._adb) 211 elif not self.browser_options.dont_override_profile: 212 self._backend_settings.RemoveProfile(self._adb) 213 214 self._forwarder_factory = android_forwarder.AndroidForwarderFactory( 215 self._adb, use_rndis_forwarder) 216 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)) 223 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) 229 230 @property 231 def _adb(self): 232 return self._android_platform_backend.adb 233 234 def _KillBrowser(self): 235 # We use KillAll rather than ForceStop for efficiency reasons. 236 try: 237 self._adb.device().KillAll(self._backend_settings.package, retries=0) 238 except device_errors.CommandFailedError: 239 pass 240 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) 248 if len(params) != 2: 249 return arg 250 key, values = params 251 if ' ' not in values: 252 return arg 253 if values[0] in '"\'' and values[-1] == values[0]: 254 return arg 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() 262 263 try: 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 267 # with Telemetry. 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.') 275 sys.exit(1) 276 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, 282 as_root=as_root) 283 284 def Start(self): 285 self._SetUpCommandLine() 286 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: 291 url = None 292 else: 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 295 url = 'about:blank' 296 # Dismiss any error dialogs. Limit the number in case we have an error loop 297 # or we are failing to dismiss. 298 for _ in xrange(10): 299 if not self._adb.device().old_interface.DismissCrashDialogIfNeeded(): 300 break 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), 305 blocking=True) 306 307 self._adb.Forward('tcp:%d' % self._port, 308 self._backend_settings.GetDevtoolsRemotePort(self._adb)) 309 310 try: 311 self._WaitForBrowserToComeUp() 312 except exceptions.BrowserGoneException: 313 logging.critical('Failed to connect to browser.') 314 if not self._adb.device().old_interface.CanAccessProtectedFileContents(): 315 logging.critical( 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.') 320 sys.exit(1) 321 except: 322 import traceback 323 traceback.print_exc() 324 self.Close() 325 raise 326 finally: 327 self._RestoreCommandLine() 328 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') 337 return args 338 339 @property 340 def forwarder_factory(self): 341 return self._forwarder_factory 342 343 @property 344 def adb(self): 345 return self._adb 346 347 @property 348 def pid(self): 349 pids = self._adb.ExtractPid(self._backend_settings.package) 350 if not pids: 351 raise exceptions.BrowserGoneException(self.browser) 352 return int(pids[0]) 353 354 @property 355 def browser_directory(self): 356 return None 357 358 @property 359 def profile_directory(self): 360 return self._backend_settings.profile_dir 361 362 @property 363 def package(self): 364 return self._backend_settings.package 365 366 @property 367 def activity(self): 368 return self._backend_settings.activity 369 370 def __del__(self): 371 self.Close() 372 373 def Close(self): 374 super(AndroidBrowserBackend, self).Close() 375 self._KillBrowser() 376 377 # Restore android.net SSL check 378 if self._backend_settings.relax_ssl_check: 379 self._adb.device().SetProp('socket.relaxsslcheck', self._saved_sslflag) 380 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) 391 for f in files: 392 # Don't pull lib, since it is created by the installer. 393 if f != 'lib': 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) 402 403 def IsBrowserRunning(self): 404 pids = self._adb.ExtractPid(self._backend_settings.package) 405 return len(pids) != 0 406 407 def GetRemotePort(self, local_port): 408 return local_port 409 410 def GetStandardOutput(self): 411 return '\n'.join(self._adb.device().RunShellCommand('logcat -d -t 500')) 412 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): 423 cmd = [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]) 428 429 # Try to get tombstones. 430 tombstones = os.path.join(util.GetChromiumSrcDir(), 'build', 'android', 431 'tombstones.py') 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]) 437 return ret 438 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) 444