arc.py revision c4127418e8f61c1a0bec85fe3aa09e1379e2a6c7
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_removable_media_pid():
194    """Returns the PID of the arc-removable-media FUSE daemon."""
195    job_pid = get_job_pid('arc-removable-media')
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_obb_mounter_pid():
203    """Returns the PID of the OBB mounter."""
204    return utils.system_output('pgrep -f -u root ^/usr/bin/arc-obb-mounter')
205
206
207def is_android_booted():
208    """Return whether Android has completed booting."""
209    output = _android_shell('getprop sys.boot_completed',
210                            ignore_status=True)
211    return output.strip() == '1'
212
213
214def is_android_process_running(process_name):
215    """Return whether Android has completed booting.
216
217    @param process_name: Process name.
218    """
219    output = adb_shell('ps | grep %s' % pipes.quote(' %s$' % process_name))
220    return bool(output)
221
222
223def check_android_file_exists(filename):
224    """Checks whether the given file exists in the Android filesystem
225
226    @param filename: File to check.
227    """
228    return adb_shell('test -e {} && echo FileExists'.format(
229            pipes.quote(filename))).find("FileExists") >= 0
230
231
232def read_android_file(filename):
233    """Reads a file in Android filesystem.
234
235    @param filename: File to read.
236    """
237    with tempfile.NamedTemporaryFile() as tmpfile:
238        adb_cmd('pull %s %s' % (pipes.quote(filename), pipes.quote(tmpfile.name)))
239        with open(tmpfile.name) as f:
240            return f.read()
241
242    return None
243
244
245def write_android_file(filename, data):
246    """Writes to a file in Android filesystem.
247
248    @param filename: File to write.
249    @param data: Data to write.
250    """
251    with tempfile.NamedTemporaryFile() as tmpfile:
252        tmpfile.write(data)
253        tmpfile.flush()
254
255        adb_cmd('push %s %s' % (pipes.quote(tmpfile.name), pipes.quote(filename)))
256
257
258def _write_android_file(filename, data):
259    """Writes to a file in Android filesystem.
260
261    This is an internal function used to bootstrap adb.
262    Tests should use write_android_file instead.
263    """
264    android_cmd = 'cat > %s' % pipes.quote(filename)
265    cros_cmd = 'android-sh -c %s' % pipes.quote(android_cmd)
266    utils.run(cros_cmd, stdin=data)
267
268
269def remove_android_file(filename):
270    """Removes a file in Android filesystem.
271
272    @param filename: File to remove.
273    """
274    adb_shell('rm -f %s' % pipes.quote(filename))
275
276
277def wait_for_android_boot(timeout=None):
278    """Sleep until Android has completed booting or timeout occurs.
279
280    @param timeout: Timeout in seconds.
281    """
282    arc_common.wait_for_android_boot(timeout)
283
284
285def wait_for_android_process(process_name,
286                             timeout=_WAIT_FOR_ANDROID_PROCESS_SECONDS):
287    """Sleep until an Android process is running or timeout occurs.
288
289    @param process_name: Process name.
290    @param timeout: Timeout in seconds.
291    """
292    condition = lambda: is_android_process_running(process_name)
293    utils.poll_for_condition(condition=condition,
294                             desc='%s is running' % process_name,
295                             timeout=timeout,
296                             sleep_interval=_PROCESS_CHECK_INTERVAL_SECONDS)
297
298
299def _android_shell(cmd, **kwargs):
300    """Execute cmd instead the Android container.
301
302    This function is strictly for internal use only, as commands do not run in a
303    fully consistent Android environment. Prefer adb_shell instead.
304    """
305    return utils.system_output('android-sh -c {}'.format(pipes.quote(cmd)),
306                               **kwargs)
307
308
309def is_android_container_alive():
310    """Check if android container is alive."""
311    try:
312        container_pid = get_container_pid()
313    except Exception:
314        return False
315    return utils.pid_is_alive(int(container_pid))
316
317
318def is_package_installed(package):
319    """Check if a package is installed. adb must be ready.
320
321    @param package: Package in request.
322    """
323    packages = adb_shell('pm list packages').splitlines()
324    package_entry = 'package:{}'.format(package)
325    return package_entry in packages
326
327
328def _before_iteration_hook(obj):
329    """Executed by parent class before every iteration.
330
331    This function resets the run_once_finished flag before every iteration
332    so we can detect failure on every single iteration.
333
334    Args:
335        obj: the test itself
336    """
337    obj.run_once_finished = False
338
339
340def _after_iteration_hook(obj):
341    """Executed by parent class after every iteration.
342
343    The parent class will handle exceptions and failures in the run and will
344    always call this hook afterwards. Take a screenshot if the run has not
345    been marked as finished (i.e. there was a failure/exception).
346
347    Args:
348        obj: the test itself
349    """
350    if not obj.run_once_finished:
351        if not os.path.exists(_SCREENSHOT_DIR_PATH):
352            os.mkdir(_SCREENSHOT_DIR_PATH, 0755)
353        obj.num_screenshots += 1
354        if obj.num_screenshots <= _MAX_SCREENSHOT_NUM:
355            logging.warning('Iteration %d failed, taking a screenshot.',
356                            obj.iteration)
357            from cros.graphics.drm import crtcScreenshot
358            image = crtcScreenshot()
359            image.save('{}/{}_iter{}.png'.format(_SCREENSHOT_DIR_PATH,
360                                                 _SCREENSHOT_BASENAME,
361                                                 obj.iteration))
362        else:
363            logging.warning('Too many failures, no screenshot taken')
364
365
366def send_keycode(keycode):
367    """Sends the given keycode to the container
368
369    @param keycode: keycode to send.
370    """
371    adb_shell('input keyevent {}'.format(keycode))
372
373
374class ArcTest(test.test):
375    """ Base class of ARC Test.
376
377    This class could be used as super class of an ARC test for saving
378    redundant codes for container bringup, autotest-dep package(s) including
379    uiautomator setup if required, and apks install/remove during
380    arc_setup/arc_teardown, respectively. By default arc_setup() is called in
381    initialize() after Android have been brought up. It could also be overridden
382    to perform non-default tasks. For example, a simple ArcHelloWorldTest can be
383    just implemented with print 'HelloWorld' in its run_once() and no other
384    functions are required. We could expect ArcHelloWorldTest would bring up
385    browser and  wait for container up, then print 'Hello World', and shutdown
386    browser after. As a precaution, if you overwrite initialize(), arc_setup(),
387    or cleanup() function(s) in ARC test, remember to call the corresponding
388    function(s) in this base class as well.
389
390    """
391    version = 1
392    _PKG_UIAUTOMATOR = 'uiautomator'
393    _FULL_PKG_NAME_UIAUTOMATOR = 'com.github.uiautomator'
394
395    def __init__(self, *args, **kwargs):
396        """Initialize flag setting."""
397        super(ArcTest, self).__init__(*args, **kwargs)
398        self.initialized = False
399        # Set the flag run_once_finished to detect if a test is executed
400        # successfully without any exception thrown. Otherwise, generate
401        # a screenshot in /var/log for debugging.
402        self.run_once_finished = False
403        self.logcat_proc = None
404        self.dep_package = None
405        self.apks = None
406        self.full_pkg_names = []
407        self.uiautomator = False
408        self.email_id = None
409        self.password = None
410        self._chrome = None
411        if os.path.exists(_SCREENSHOT_DIR_PATH):
412            shutil.rmtree(_SCREENSHOT_DIR_PATH)
413        self.register_before_iteration_hook(_before_iteration_hook)
414        self.register_after_iteration_hook(_after_iteration_hook)
415        # Keep track of the number of debug screenshots taken and keep the
416        # total number sane to avoid issues.
417        self.num_screenshots = 0
418
419    def initialize(self, extension_path=None,
420                   arc_mode=arc_common.ARC_MODE_ENABLED, **chrome_kargs):
421        """Log in to a test account."""
422        extension_paths = [extension_path] if extension_path else []
423        self._chrome = chrome.Chrome(extension_paths=extension_paths,
424                                     arc_mode=arc_mode,
425                                     **chrome_kargs)
426        if extension_path:
427            self._extension = self._chrome.get_extension(extension_path)
428        else:
429            self._extension = None
430        # With ARC enabled, Chrome will wait until container to boot up
431        # before returning here, see chrome.py.
432        self.initialized = True
433        try:
434            if is_android_container_alive():
435                self.arc_setup()
436            else:
437                logging.error('Container is alive?')
438        except Exception as err:
439            self.cleanup()
440            raise error.TestFail(err)
441
442    def after_run_once(self):
443        """Executed after run_once() only if there were no errors.
444
445        This function marks the run as finished with a flag. If there was a
446        failure the flag won't be set and the failure can then be detected by
447        testing the run_once_finished flag.
448        """
449        logging.info('After run_once')
450        self.run_once_finished = True
451
452    def cleanup(self):
453        """Log out of Chrome."""
454        if not self.initialized:
455            logging.info('Skipping ARC cleanup: not initialized')
456            return
457        logging.info('Starting ARC cleanup')
458        try:
459            if is_android_container_alive():
460                self.arc_teardown()
461        except Exception as err:
462            raise error.TestFail(err)
463        finally:
464            try:
465                self._stop_logcat()
466            finally:
467                if self._chrome is not None:
468                    self._chrome.close()
469
470    def arc_setup(self, dep_package=None, apks=None, full_pkg_names=[],
471                  uiautomator=False, email_id=None, password=None):
472        """ARC test setup: Setup dependencies and install apks.
473
474        This function disables package verification and enables non-market
475        APK installation. Then, it installs specified APK(s) and uiautomator
476        package and path if required in a test.
477
478        @param dep_package: Package name of autotest_deps APK package.
479        @param apks: Array of APK names to be installed in dep_package.
480        @param full_pkg_names: Array of full package names to be removed
481                               in teardown.
482        @param uiautomator: uiautomator python package is required or not.
483
484        @param email_id: email id to be attached to the android. Only used
485                         when  account_util is set to true.
486        @param password: password related to the email_id.
487        """
488        if not self.initialized:
489            logging.info('Skipping ARC setup: not initialized')
490            return
491        logging.info('Starting ARC setup')
492        self.dep_package = dep_package
493        self.apks = apks
494        self.uiautomator = uiautomator
495        self.email_id = email_id
496        self.password = password
497        # Setup dependent packages if required
498        packages = []
499        if dep_package:
500            packages.append(dep_package)
501        if self.uiautomator:
502            packages.append(self._PKG_UIAUTOMATOR)
503        if packages:
504            logging.info('Setting up dependent package(s) %s', packages)
505            self.job.setup_dep(packages)
506
507        # TODO(b/29341443): Run logcat on non ArcTest test cases too.
508        with open(_VAR_LOGCAT_PATH, 'w') as f:
509            self.logcat_proc = subprocess.Popen(
510                ['android-sh', '-c', 'logcat -v threadtime'],
511                stdout=f,
512                stderr=subprocess.STDOUT,
513                close_fds=True)
514
515        wait_for_adb_ready()
516
517        # package_verifier_user_consent == -1 means to reject Google's
518        # verification on the server side through Play Store.  This suppress a
519        # consent dialog from the system.
520        adb_shell('settings put secure package_verifier_user_consent -1')
521        # TODO(30310952): Remove the workaround below to a Phonesky bug.
522        adb_shell('am broadcast -a com.google.gservices.intent.action.GSERVICES_OVERRIDE '
523                  '-e finsky.platform_anti_malware_enabled false')
524        adb_shell('settings put global package_verifier_enable 0')
525        adb_shell('settings put secure install_non_market_apps 1')
526
527        if self.dep_package:
528            apk_path = os.path.join(self.autodir, 'deps', self.dep_package)
529            if self.apks:
530                for apk in self.apks:
531                    logging.info('Installing %s', apk)
532                    adb_install('%s/%s' % (apk_path, apk))
533                # Verify if package(s) are installed correctly
534                if not full_pkg_names:
535                    raise error.TestError('Package names of apks expected')
536                for pkg in full_pkg_names:
537                    logging.info('Check if %s is installed', pkg)
538                    if not is_package_installed(pkg):
539                        raise error.TestError('Package %s not found' % pkg)
540                    # Make sure full_pkg_names contains installed packages only
541                    # so arc_teardown() knows what packages to uninstall.
542                    self.full_pkg_names.append(pkg)
543
544        if self.uiautomator:
545            path = os.path.join(self.autodir, 'deps', self._PKG_UIAUTOMATOR)
546            sys.path.append(path)
547
548    def _stop_logcat(self):
549        """Stop the adb logcat process gracefully."""
550        if not self.logcat_proc:
551            return
552        # Running `adb kill-server` should have killed `adb logcat`
553        # process, but just in case also send termination signal.
554        self.logcat_proc.terminate()
555
556        class TimeoutException(Exception):
557            """Termination timeout timed out."""
558
559        try:
560            utils.poll_for_condition(
561                condition=lambda: self.logcat_proc.poll() is not None,
562                exception=TimeoutException,
563                timeout=10,
564                sleep_interval=0.1,
565                desc='Waiting for adb logcat to terminate')
566        except TimeoutException:
567            logging.info('Killing adb logcat due to timeout')
568            self.logcat_proc.kill()
569            self.logcat_proc.wait()
570
571    def arc_teardown(self):
572        """ARC test teardown.
573
574        This function removes all installed packages in arc_setup stage
575        first. Then, it restores package verification and disables non-market
576        APK installation.
577
578        """
579        if self.full_pkg_names:
580            for pkg in self.full_pkg_names:
581                logging.info('Uninstalling %s', pkg)
582                if not is_package_installed(pkg):
583                    raise error.TestError('Package %s was not installed' % pkg)
584                adb_uninstall(pkg)
585        if self.uiautomator:
586            logging.info('Uninstalling %s', self._FULL_PKG_NAME_UIAUTOMATOR)
587            adb_uninstall(self._FULL_PKG_NAME_UIAUTOMATOR)
588        adb_shell('settings put secure install_non_market_apps 0')
589        adb_shell('settings put global package_verifier_enable 1')
590        # TODO(30310952): Remove the workaround below to a Phonesky bug.
591        adb_shell('am broadcast -a com.google.gservices.intent.action.GSERVICES_OVERRIDE '
592                  '--esn finsky.platform_anti_malware_enabled')
593        adb_shell('settings put secure package_verifier_user_consent 0')
594
595        remove_android_file(_ANDROID_ADB_KEYS_PATH)
596        utils.system_output('adb kill-server')
597
598    def block_outbound(self):
599        """ Blocks the connection from the container to outer network.
600
601            The iptables settings accept only 100.115.92.2 port 5555 (adb) and
602            localhost port 9008 (uiautomator)
603        """
604        logging.info('Blocking outbound connection')
605        _android_shell('iptables -I OUTPUT -j REJECT')
606        _android_shell('iptables -I OUTPUT -p tcp -s 100.115.92.2 --sport 5555 -j ACCEPT')
607        _android_shell('iptables -I OUTPUT -p tcp -d localhost --dport 9008 -j ACCEPT')
608        _android_shell('iptables -I OUTPUT -p tcp -s localhost --sport 9008 -j ACCEPT')
609
610
611    def unblock_outbound(self):
612        """ Unblocks the connection from the container to outer network.
613
614            The iptables settings are not permanent which means they reset on
615            each instance invocation. But we can still use this function to
616            unblock the outbound connections during the test if needed.
617        """
618        logging.info('Unblocking outbound connection')
619        _android_shell('iptables -D OUTPUT -p tcp -s localhost --sport 9008 -j ACCEPT')
620        _android_shell('iptables -D OUTPUT -p tcp -d localhost --dport 9008 -j ACCEPT')
621        _android_shell('iptables -D OUTPUT -p tcp -s 100.115.92.2 --sport 5555 -j ACCEPT')
622        _android_shell('iptables -D OUTPUT -j REJECT')
623