arc.py revision 5c2745d57902e68bbfdd4cfb953740faef645944
1# Copyright 2015 The Chromium OS 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 glob 6import logging 7import os 8import pipes 9import re 10import shutil 11import subprocess 12import sys 13import tempfile 14 15from autotest_lib.client.bin import test, utils 16from autotest_lib.client.common_lib import error 17from autotest_lib.client.common_lib.cros import chrome, arc_common 18 19_ADB_KEYS_PATH = '/tmp/adb_keys' 20_ADB_VENDOR_KEYS = 'ADB_VENDOR_KEYS' 21_ANDROID_CONTAINER_PATH = '/var/run/containers/android_*' 22_SCREENSHOT_DIR_PATH = '/var/log/arc-screenshots' 23_SCREENSHOT_BASENAME = 'arc-screenshot' 24_MAX_SCREENSHOT_NUM = 10 25_SDCARD_PID_PATH = '/var/run/arc/sdcard.pid' 26_ANDROID_ADB_KEYS_PATH = '/data/misc/adb/adb_keys' 27_PROCESS_CHECK_INTERVAL_SECONDS = 1 28_WAIT_FOR_ADB_READY = 60 29_WAIT_FOR_ANDROID_PROCESS_SECONDS = 60 30_VAR_LOGCAT_PATH = '/var/log/logcat' 31 32 33def setup_adb_host(): 34 """Setup ADB host keys. 35 36 This sets up the files and environment variables that wait_for_adb_ready() needs""" 37 if _ADB_VENDOR_KEYS in os.environ: 38 return 39 if not os.path.exists(_ADB_KEYS_PATH): 40 os.mkdir(_ADB_KEYS_PATH) 41 # adb expects $HOME to be writable. 42 os.environ['HOME'] = _ADB_KEYS_PATH 43 44 # Generate and save keys for adb if needed 45 key_path = os.path.join(_ADB_KEYS_PATH, 'test_key') 46 if not os.path.exists(key_path): 47 utils.system('adb keygen ' + pipes.quote(key_path)) 48 os.environ[_ADB_VENDOR_KEYS] = key_path 49 50 51def adb_connect(): 52 """Attempt to connect ADB to the Android container. 53 54 Returns true if successful. Do not call this function directly. Call 55 wait_for_adb_ready() instead.""" 56 if not is_android_booted(): 57 return False 58 if utils.system('adb connect localhost:22', ignore_status=True) != 0: 59 return False 60 return is_adb_connected() 61 62 63def is_adb_connected(): 64 """Return true if adb is connected to the container.""" 65 output = utils.system_output('adb get-state', ignore_status=True) 66 logging.debug('adb get-state: %s', output) 67 return output.strip() == 'device' 68 69 70def wait_for_adb_ready(timeout=_WAIT_FOR_ADB_READY): 71 """Wait for the ADB client to connect to the ARC container. 72 73 @param timeout: Timeout in seconds. 74 """ 75 setup_adb_host() 76 if is_adb_connected(): 77 return 78 79 # Push keys for adb. 80 pubkey_path = os.environ[_ADB_VENDOR_KEYS] + '.pub' 81 with open(pubkey_path, 'r') as f: 82 _write_android_file(_ANDROID_ADB_KEYS_PATH, f.read()) 83 _android_shell('restorecon ' + pipes.quote(_ANDROID_ADB_KEYS_PATH)) 84 85 # This starts adbd. 86 _android_shell('setprop sys.usb.config mtp,adb') 87 88 # Kill existing adb server to ensure that a full reconnect is performed. 89 utils.system('adb kill-server', ignore_status=True) 90 91 exception = error.TestFail('adb is not ready in %d seconds.' % timeout) 92 utils.poll_for_condition(adb_connect, 93 exception, 94 timeout) 95 96 97def grant_permissions(package, permissions): 98 """Grants permissions to a package. 99 100 @param package: Package name. 101 @param permissions: A list of permissions. 102 103 """ 104 for permission in permissions: 105 adb_shell('pm grant %s android.permission.%s' % ( 106 pipes.quote(package), pipes.quote(permission))) 107 108 109def adb_cmd(cmd, **kwargs): 110 """Executed cmd using adb. Must wait for adb ready. 111 112 @param cmd: Command to run. 113 """ 114 wait_for_adb_ready() 115 return utils.system_output('adb %s' % cmd, **kwargs) 116 117 118def adb_shell(cmd, **kwargs): 119 """Executed shell command with adb. 120 121 @param cmd: Command to run. 122 """ 123 output = adb_cmd('shell %s' % pipes.quote(cmd), **kwargs) 124 # Some android commands include a trailing CRLF in their output. 125 if kwargs.pop('strip_trailing_whitespace', True): 126 output = output.rstrip() 127 return output 128 129 130def adb_install(apk): 131 """Install an apk into container. You must connect first. 132 133 @param apk: Package to install. 134 """ 135 return adb_cmd('install -r %s' % apk) 136 137 138def adb_uninstall(apk): 139 """Remove an apk from container. You must connect first. 140 141 @param apk: Package to uninstall. 142 """ 143 return adb_cmd('uninstall %s' % apk) 144 145 146def adb_reboot(): 147 """Reboots the container. You must connect first.""" 148 adb_root() 149 return adb_cmd('reboot', ignore_status=True) 150 151 152def adb_root(): 153 """Restart adbd with root permission.""" 154 adb_cmd('root') 155 156 157def get_container_root(): 158 """Returns path to Android container root directory. 159 160 Raises: 161 TestError if no container root directory is found, or 162 more than one container root directories are found. 163 """ 164 arc_container_roots = glob.glob(_ANDROID_CONTAINER_PATH) 165 if len(arc_container_roots) != 1: 166 raise error.TestError( 167 'Android container not available: %r' % arc_container_roots) 168 return arc_container_roots[0] 169 170 171def get_job_pid(job_name): 172 """Returns the PID of an upstart job.""" 173 status = utils.system_output('status %s' % job_name) 174 match = re.match(r'^%s start/running, process (\d+)$' % job_name, 175 status) 176 if not match: 177 raise error.TestError('Unexpected status: "%s"' % status) 178 return match.group(1) 179 180 181def get_container_pid(): 182 """Returns the PID of the container.""" 183 container_root = get_container_root() 184 pid_path = os.path.join(container_root, 'container.pid') 185 return utils.read_one_line(pid_path) 186 187 188def get_sdcard_pid(): 189 """Returns the PID of the sdcard container.""" 190 return utils.read_one_line(_SDCARD_PID_PATH) 191 192 193def get_dlfs_pid(): 194 """Returns the PID of the arc-downloads-filesystem FUSE daemon.""" 195 job_pid = get_job_pid('arc-downloads-filesystem') 196 # |job_pid| is the minijail process, obtain the PID of the process running 197 # inside the mount namespace. 198 # FUSE process is the only process running as chronos in the process group. 199 return utils.system_output('pgrep -u chronos -g %s' % job_pid) 200 201 202def get_removable_media_pid(): 203 """Returns the PID of the arc-removable-media FUSE daemon.""" 204 job_pid = get_job_pid('arc-removable-media') 205 # |job_pid| is the minijail process, obtain the PID of the process running 206 # inside the mount namespace. 207 # FUSE process is the only process running as chronos in the process group. 208 return utils.system_output('pgrep -u chronos -g %s' % job_pid) 209 210 211def get_obb_mounter_pid(): 212 """Returns the PID of the OBB mounter.""" 213 return utils.system_output('pgrep -f -u root ^/usr/bin/arc-obb-mounter') 214 215 216def is_android_booted(): 217 """Return whether Android has completed booting.""" 218 output = _android_shell('getprop sys.boot_completed', 219 ignore_status=True) 220 return output.strip() == '1' 221 222 223def is_android_process_running(process_name): 224 """Return whether Android has completed booting. 225 226 @param process_name: Process name. 227 """ 228 output = adb_shell('ps %s' % pipes.quote(process_name)) 229 # ps always prints the header. 230 return len(output.splitlines()) == 2 231 232 233def read_android_file(filename): 234 """Reads a file in Android filesystem. 235 236 @param filename: File to read. 237 """ 238 with tempfile.NamedTemporaryFile() as tmpfile: 239 adb_cmd('pull %s %s' % (pipes.quote(filename), pipes.quote(tmpfile.name))) 240 with open(tmpfile.name) as f: 241 return f.read() 242 243 return None 244 245 246def write_android_file(filename, data): 247 """Writes to a file in Android filesystem. 248 249 @param filename: File to write. 250 @param data: Data to write. 251 """ 252 with tempfile.NamedTemporaryFile() as tmpfile: 253 tmpfile.write(data) 254 tmpfile.flush() 255 256 adb_cmd('push %s %s' % (pipes.quote(tmpfile.name), pipes.quote(filename))) 257 258 259def _write_android_file(filename, data): 260 """Writes to a file in Android filesystem. 261 262 This is an internal function used to bootstrap adb. 263 Tests should use write_android_file instead. 264 """ 265 android_cmd = 'cat > %s' % pipes.quote(filename) 266 cros_cmd = 'android-sh -c %s' % pipes.quote(android_cmd) 267 utils.run(cros_cmd, stdin=data) 268 269 270def remove_android_file(filename): 271 """Removes a file in Android filesystem. 272 273 @param filename: File to remove. 274 """ 275 adb_shell('rm -f %s' % pipes.quote(filename)) 276 277 278def wait_for_android_boot(timeout=None): 279 """Sleep until Android has completed booting or timeout occurs. 280 281 @param timeout: Timeout in seconds. 282 """ 283 arc_common.wait_for_android_boot(timeout) 284 285 286def wait_for_android_process(process_name, 287 timeout=_WAIT_FOR_ANDROID_PROCESS_SECONDS): 288 """Sleep until an Android process is running or timeout occurs. 289 290 @param process_name: Process name. 291 @param timeout: Timeout in seconds. 292 """ 293 condition = lambda: is_android_process_running(process_name) 294 utils.poll_for_condition(condition=condition, 295 desc='%s is running' % process_name, 296 timeout=timeout, 297 sleep_interval=_PROCESS_CHECK_INTERVAL_SECONDS) 298 299 300def _android_shell(cmd, **kwargs): 301 """Execute cmd instead the Android container. 302 303 This function is strictly for internal use only, as commands do not run in a 304 fully consistent Android environment. Prefer adb_shell instead. 305 """ 306 return utils.system_output('android-sh -c {}'.format(pipes.quote(cmd)), 307 **kwargs) 308 309 310def is_android_container_alive(): 311 """Check if android container is alive.""" 312 try: 313 container_pid = get_container_pid() 314 except Exception: 315 return False 316 return utils.pid_is_alive(int(container_pid)) 317 318 319def is_package_installed(package): 320 """Check if a package is installed. adb must be ready. 321 322 @param package: Package in request. 323 """ 324 packages = adb_shell('pm list packages').splitlines() 325 package_entry = 'package:{}'.format(package) 326 return package_entry in packages 327 328 329def _before_iteration_hook(obj): 330 """Executed by parent class before every iteration. 331 332 This function resets the run_once_finished flag before every iteration 333 so we can detect failure on every single iteration. 334 335 Args: 336 obj: the test itself 337 """ 338 obj.run_once_finished = False 339 340 341def _after_iteration_hook(obj): 342 """Executed by parent class after every iteration. 343 344 The parent class will handle exceptions and failures in the run and will 345 always call this hook afterwards. Take a screenshot if the run has not 346 been marked as finished (i.e. there was a failure/exception). 347 348 Args: 349 obj: the test itself 350 """ 351 if not obj.run_once_finished: 352 if not os.path.exists(_SCREENSHOT_DIR_PATH): 353 os.mkdir(_SCREENSHOT_DIR_PATH, 0755) 354 obj.num_screenshots += 1 355 if obj.num_screenshots <= _MAX_SCREENSHOT_NUM: 356 logging.warning('Iteration %d failed, taking a screenshot.', 357 obj.iteration) 358 from cros.graphics.drm import crtcScreenshot 359 image = crtcScreenshot() 360 image.save('{}/{}_iter{}.png'.format(_SCREENSHOT_DIR_PATH, 361 _SCREENSHOT_BASENAME, 362 obj.iteration)) 363 else: 364 logging.warning('Too many failures, no screenshot taken') 365 366 367class ArcTest(test.test): 368 """ Base class of ARC Test. 369 370 This class could be used as super class of an ARC test for saving 371 redundant codes for container bringup, autotest-dep package(s) including 372 uiautomator setup if required, and apks install/remove during 373 arc_setup/arc_teardown, respectively. By default arc_setup() is called in 374 initialize() after Android have been brought up. It could also be overridden 375 to perform non-default tasks. For example, a simple ArcHelloWorldTest can be 376 just implemented with print 'HelloWorld' in its run_once() and no other 377 functions are required. We could expect ArcHelloWorldTest would bring up 378 browser and wait for container up, then print 'Hello World', and shutdown 379 browser after. As a precaution, if you overwrite initialize(), arc_setup(), 380 or cleanup() function(s) in ARC test, remember to call the corresponding 381 function(s) in this base class as well. 382 383 """ 384 version = 1 385 _PKG_UIAUTOMATOR = 'uiautomator' 386 _FULL_PKG_NAME_UIAUTOMATOR = 'com.github.uiautomator' 387 388 def __init__(self, *args, **kwargs): 389 """Initialize flag setting.""" 390 super(ArcTest, self).__init__(*args, **kwargs) 391 self.initialized = False 392 # Set the flag run_once_finished to detect if a test is executed 393 # successfully without any exception thrown. Otherwise, generate 394 # a screenshot in /var/log for debugging. 395 self.run_once_finished = False 396 self.logcat_proc = None 397 self.dep_package = None 398 self.apks = None 399 self.full_pkg_names = [] 400 self.uiautomator = False 401 self.email_id = None 402 self.password = None 403 if os.path.exists(_SCREENSHOT_DIR_PATH): 404 shutil.rmtree(_SCREENSHOT_DIR_PATH) 405 self.register_before_iteration_hook(_before_iteration_hook) 406 self.register_after_iteration_hook(_after_iteration_hook) 407 # Keep track of the number of debug screenshots taken and keep the 408 # total number sane to avoid issues. 409 self.num_screenshots = 0 410 411 def initialize(self, extension_path=None, 412 arc_mode=arc_common.ARC_MODE_ENABLED, **chrome_kargs): 413 """Log in to a test account.""" 414 extension_paths = [extension_path] if extension_path else [] 415 self._chrome = chrome.Chrome(extension_paths=extension_paths, 416 is_component=not extension_paths, 417 arc_mode=arc_mode, 418 **chrome_kargs) 419 if extension_path: 420 self._extension = self._chrome.get_extension(extension_path) 421 else: 422 self._extension = None 423 # With ARC enabled, Chrome will wait until container to boot up 424 # before returning here, see chrome.py. 425 self.initialized = True 426 try: 427 if is_android_container_alive(): 428 self.arc_setup() 429 else: 430 logging.error('Container is alive?') 431 except Exception as err: 432 self.cleanup() 433 raise error.TestFail(err) 434 435 def after_run_once(self): 436 """Executed after run_once() only if there were no errors. 437 438 This function marks the run as finished with a flag. If there was a 439 failure the flag won't be set and the failure can then be detected by 440 testing the run_once_finished flag. 441 """ 442 logging.info('After run_once') 443 self.run_once_finished = True 444 445 def cleanup(self): 446 """Log out of Chrome.""" 447 if not self.initialized: 448 logging.info('Skipping ARC cleanup: not initialized') 449 return 450 logging.info('Starting ARC cleanup') 451 try: 452 if is_android_container_alive(): 453 self.arc_teardown() 454 except Exception as err: 455 raise error.TestFail(err) 456 finally: 457 try: 458 self._stop_logcat() 459 finally: 460 self._chrome.close() 461 462 def arc_setup(self, dep_package=None, apks=None, full_pkg_names=[], 463 uiautomator=False, email_id=None, password=None): 464 """ARC test setup: Setup dependencies and install apks. 465 466 This function disables package verification and enables non-market 467 APK installation. Then, it installs specified APK(s) and uiautomator 468 package and path if required in a test. 469 470 @param dep_package: Package name of autotest_deps APK package. 471 @param apks: Array of APK names to be installed in dep_package. 472 @param full_pkg_names: Array of full package names to be removed 473 in teardown. 474 @param uiautomator: uiautomator python package is required or not. 475 476 @param email_id: email id to be attached to the android. Only used 477 when account_util is set to true. 478 @param password: password related to the email_id. 479 """ 480 if not self.initialized: 481 logging.info('Skipping ARC setup: not initialized') 482 return 483 logging.info('Starting ARC setup') 484 self.dep_package = dep_package 485 self.apks = apks 486 self.uiautomator = uiautomator 487 self.email_id = email_id 488 self.password = password 489 # Setup dependent packages if required 490 packages = [] 491 if dep_package: 492 packages.append(dep_package) 493 if self.uiautomator: 494 packages.append(self._PKG_UIAUTOMATOR) 495 if packages: 496 logging.info('Setting up dependent package(s) %s', packages) 497 self.job.setup_dep(packages) 498 499 # TODO(b/29341443): Run logcat on non ArcTest test cases too. 500 with open(_VAR_LOGCAT_PATH, 'w') as f: 501 self.logcat_proc = subprocess.Popen( 502 ['android-sh', '-c', 'logcat -v threadtime'], 503 stdout=f, 504 stderr=subprocess.STDOUT, 505 close_fds=True) 506 507 wait_for_adb_ready() 508 509 # package_verifier_user_consent == -1 means to reject Google's 510 # verification on the server side through Play Store. This suppress a 511 # consent dialog from the system. 512 adb_shell('settings put secure package_verifier_user_consent -1') 513 # TODO(30310952): Remove the workaround below to a Phonesky bug. 514 adb_shell('am broadcast -a com.google.gservices.intent.action.GSERVICES_OVERRIDE ' 515 '-e finsky.platform_anti_malware_enabled false') 516 adb_shell('settings put global package_verifier_enable 0') 517 adb_shell('settings put secure install_non_market_apps 1') 518 519 if self.dep_package: 520 apk_path = os.path.join(self.autodir, 'deps', self.dep_package) 521 if self.apks: 522 for apk in self.apks: 523 logging.info('Installing %s', apk) 524 adb_install('%s/%s' % (apk_path, apk)) 525 # Verify if package(s) are installed correctly 526 if not full_pkg_names: 527 raise error.TestError('Package names of apks expected') 528 for pkg in full_pkg_names: 529 logging.info('Check if %s is installed', pkg) 530 if not is_package_installed(pkg): 531 raise error.TestError('Package %s not found' % pkg) 532 # Make sure full_pkg_names contains installed packages only 533 # so arc_teardown() knows what packages to uninstall. 534 self.full_pkg_names.append(pkg) 535 536 if self.uiautomator: 537 path = os.path.join(self.autodir, 'deps', self._PKG_UIAUTOMATOR) 538 sys.path.append(path) 539 540 def _stop_logcat(self): 541 """Stop the adb logcat process gracefully.""" 542 if not self.logcat_proc: 543 return 544 # Running `adb kill-server` should have killed `adb logcat` 545 # process, but just in case also send termination signal. 546 self.logcat_proc.terminate() 547 548 class TimeoutException(Exception): 549 """Termination timeout timed out.""" 550 551 try: 552 utils.poll_for_condition( 553 condition=lambda: self.logcat_proc.poll() is not None, 554 exception=TimeoutException, 555 timeout=10, 556 sleep_interval=0.1, 557 desc='Waiting for adb logcat to terminate') 558 except TimeoutException: 559 logging.info('Killing adb logcat due to timeout') 560 self.logcat_proc.kill() 561 self.logcat_proc.wait() 562 563 def arc_teardown(self): 564 """ARC test teardown. 565 566 This function removes all installed packages in arc_setup stage 567 first. Then, it restores package verification and disables non-market 568 APK installation. 569 570 """ 571 if self.full_pkg_names: 572 for pkg in self.full_pkg_names: 573 logging.info('Uninstalling %s', pkg) 574 if not is_package_installed(pkg): 575 raise error.TestError('Package %s was not installed' % pkg) 576 adb_uninstall(pkg) 577 if self.uiautomator: 578 logging.info('Uninstalling %s', self._FULL_PKG_NAME_UIAUTOMATOR) 579 adb_uninstall(self._FULL_PKG_NAME_UIAUTOMATOR) 580 adb_shell('settings put secure install_non_market_apps 0') 581 adb_shell('settings put global package_verifier_enable 1') 582 # TODO(30310952): Remove the workaround below to a Phonesky bug. 583 adb_shell('am broadcast -a com.google.gservices.intent.action.GSERVICES_OVERRIDE ' 584 '--esn finsky.platform_anti_malware_enabled') 585 adb_shell('settings put secure package_verifier_user_consent 0') 586 587 remove_android_file(_ANDROID_ADB_KEYS_PATH) 588 utils.system_output('adb kill-server') 589 590 def block_outbound(self): 591 """ Blocks the connection from the container to outer network. 592 593 The iptables settings accept only 192.168.254.2 port 5555 (adb) and 594 localhost port 9008 (uiautomator) 595 """ 596 logging.info('Blocking outbound connection') 597 _android_shell('iptables -I OUTPUT -j REJECT') 598 _android_shell('iptables -I OUTPUT -p tcp -s 192.168.254.2 --sport 5555 -j ACCEPT') 599 _android_shell('iptables -I OUTPUT -p tcp -d localhost --dport 9008 -j ACCEPT') 600 _android_shell('iptables -I OUTPUT -p tcp -s localhost --sport 9008 -j ACCEPT') 601 602 603 def unblock_outbound(self): 604 """ Unblocks the connection from the container to outer network. 605 606 The iptables settings are not permanent which means they reset on 607 each instance invocation. But we can still use this function to 608 unblock the outbound connections during the test if needed. 609 """ 610 logging.info('Unblocking outbound connection') 611 _android_shell('iptables -D OUTPUT -p tcp -s localhost --sport 9008 -j ACCEPT') 612 _android_shell('iptables -D OUTPUT -p tcp -d localhost --dport 9008 -j ACCEPT') 613 _android_shell('iptables -D OUTPUT -p tcp -s 192.168.254.2 --sport 5555 -j ACCEPT') 614 _android_shell('iptables -D OUTPUT -j REJECT') 615