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