Home | History | Annotate | Download | only in harness
      1 # Copyright (C) 2016 The Android Open Source Project
      2 #
      3 # Licensed under the Apache License, Version 2.0 (the "License");
      4 # you may not use this file except in compliance with the License.
      5 # You may obtain a copy of the License at
      6 #
      7 #      http://www.apache.org/licenses/LICENSE-2.0
      8 #
      9 # Unless required by applicable law or agreed to in writing, software
     10 # distributed under the License is distributed on an "AS IS" BASIS,
     11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 # See the License for the specific language governing permissions and
     13 # limitations under the License.
     14 
     15 '''Module that contains the class UtilAndroid, providing utility method to
     16 interface with Android ADB.'''
     17 
     18 from __future__ import absolute_import
     19 
     20 import logging
     21 import re
     22 import subprocess
     23 import time
     24 import collections
     25 import multiprocessing
     26 try:
     27     # Python 3
     28     import queue
     29 except ImportError:
     30     import Queue as queue
     31 
     32 from .exception import TestSuiteException
     33 from . import util_log
     34 
     35 
     36 class UtilAndroid(object):
     37     '''Provides some utility methods that interface with Android using adb.'''
     38     # pylint: disable=too-many-public-methods
     39 
     40     def __init__(self, adb_path, lldb_server_path_device, device):
     41         # The path to the adb binary on the local machine
     42         self._path_adb = adb_path
     43         # The path to the lldb server binary on the device
     44         self._path_lldbserver = lldb_server_path_device
     45         self._log = util_log.get_logger()
     46         self.device = device
     47         self._prop_stacks = collections.defaultdict(list)
     48         return
     49 
     50     @staticmethod
     51     def _validate_string(string):
     52         '''Check that a string is valid and not empty.
     53 
     54         Args:
     55             string: The string to be checked.
     56         '''
     57         assert isinstance(string, str)
     58         assert len(string) > 0
     59 
     60     def adb(self, args, async=False, device=True, timeout=None):
     61         '''Run an adb command (async optional).
     62 
     63         Args:
     64             args: The command (including arguments) to run in adb.
     65             async: Boolean to specify whether adb should run the command
     66                    asynchronously.
     67             device: boolean to specify whether the serial id of the android
     68                     device should be inserted in the adb command.
     69             timeout: it specifies the number of seconds to wait for
     70                      a synchronous invocation before aborting. If unspecified or
     71                      None it waits indefinitely for the command to complete.
     72 
     73         Raises:
     74             ValueError: it can be caused by any of the following situations:
     75                         - when both the combination async=True and timeout are
     76                           given.
     77                         - when a timeout <= 0 is specified.
     78 
     79         Returns:
     80             If adb was synchronously run and the command completed by the
     81             specified timeout, a string which is the output (standard out and
     82             error) from adb. Otherwise it returns None.
     83         '''
     84 
     85         # Form the command
     86         if device:
     87             cmd = '{0} -s {1} {2}'.format(self._path_adb, self.device, args)
     88         else:
     89             cmd = '{0} {1}'.format(self._path_adb, args)
     90 
     91         self._log.debug('Execute ADB: %s', cmd)
     92 
     93         if timeout is None:
     94             # local invocation
     95             return_code, output = UtilAndroid._execute_command_local(cmd, async)
     96 
     97         else:
     98             # remote invocation
     99             if async:
    100                 raise ValueError('Invalid combination: asynchronous invocation '
    101                                  'with timeout specified')
    102 
    103             return_code, output = UtilAndroid._execute_command_remote(cmd,
    104                                                                       timeout)
    105 
    106             if return_code is None:
    107                 self._log.warn('[ADB] The command timed out: %s', cmd)
    108 
    109         # log the output message
    110         if output is not None:
    111             self._adb_log_output(cmd, output, return_code)
    112 
    113         return output
    114 
    115     def adb_retry(self, args, max_num_attempts, timeout):
    116         '''Attempt to execute the given adb command a certain number of times.
    117 
    118         The function executes the given command through adb, waiting for its
    119         completion up to 'timeout' seconds. If the command completes then it
    120         returns its output. Otherwise it aborts the execution of the adb
    121         command and re-issues it anew with the same parameters. In case of
    122         timeout this process is repeated up to 'max_num_attempts'.
    123 
    124         The purpose of this function is to handle the cases when, for some
    125         reason, a command sent to 'adb' freezes, blocking the whole test suite
    126         indefinitely.
    127 
    128         Args:
    129             args: The command (including arguments) to run in adb.
    130             max_num_attempts: the max number of attempts to repeat the command
    131                               in case of timeout.
    132             timeout: it specifies the number of seconds to wait for the adb
    133                      command to complete.
    134 
    135         Raises:
    136             ValueError: when the parameter timeout is invalid (None or <= 0).
    137 
    138         Returns:
    139             If adb was synchronously run and the command completes by the
    140             specified timeout, a string which is the output (standard out and
    141             error) from adb. Otherwise it returns None.
    142         '''
    143         if timeout is None or timeout <= 0:
    144             raise ValueError('Invalid value for timeout')
    145 
    146         output = None
    147 
    148         for attempt in range(max_num_attempts):
    149             self._log.debug('[ADB] Attempt #%d: %s', attempt + 1, args)
    150             output = self.adb(args, False, True, timeout)
    151             if output:
    152                 break
    153 
    154         return output
    155 
    156     def _adb_log_output(self, cmd, output, return_code):
    157         '''Save in the log the command & output from `adb`.
    158 
    159         Internal function, helper to record in the log the issued adb command
    160         together with its output and return code.
    161 
    162         Params:
    163             cmd: string, the command issued to `adb`.
    164             output: string, the output retrieved from `adb`.
    165             return_code: int, the return code from `adb`.
    166         '''
    167 
    168         message = output.strip()
    169 
    170         # if return_code != 0, we wish to also record the command executed
    171         # (which occurs if and only if we are in verbose mode)
    172         is_warning = return_code != 0
    173         threshold = self._log.getEffectiveLevel()
    174         if is_warning and threshold > logging.DEBUG:
    175             self._log.warn("[ADB] Command executed: {0}".format(cmd))
    176 
    177         level = logging.WARNING if is_warning else logging.DEBUG
    178         if message:
    179             # if message is composed by multiple lines, then print it after
    180             # the log preamble
    181             if re.search('\n', message):
    182                 message = '\n' + message
    183         else:
    184             message = '<empty>'
    185 
    186         self._log.log(level, 'RC: {0}, Output: {1}'.format(return_code,
    187                                                            message))
    188 
    189     def check_adb_alive(self):
    190         '''Ping the device and raise an exception in case of timeout.
    191 
    192         It sends a ping message through 'adb shell'. The emulator/device should
    193         echo the same message back by one minute. If it does not, it raises
    194         a TestSuiteException.
    195 
    196         Purpose of this method is to check whether 'adb' became frozen or
    197         stuck.
    198 
    199         Raises:
    200             TestSuiteException: in case the device/emulator does not reply by
    201                                 one minute or the `ping' message is not echoed
    202                                 back.
    203         '''
    204         token = 'PING'
    205         log = util_log.get_logger()
    206         cmd = "echo {0}".format(token)
    207 
    208         tries = 10
    209         try_number = tries
    210         while try_number > 0:
    211             log.debug('Sending a ping through "adb shell" (try #%s)...',
    212                       try_number)
    213             output = self.shell(cmd, False, 60)
    214 
    215             if output is None:
    216                 raise TestSuiteException(
    217                     'Timeout when pinging the device/emulator through '
    218                     '"adb shell".  Is "adb" stuck or dead?')
    219             elif token not in output:
    220                 log.debug('Ping failed. Cannot match the token "%s" in "adb '
    221                           'shell %s"', token, cmd)
    222             else:
    223                 log.debug('Pong message received')
    224                 return
    225 
    226             try_number -= 1
    227             time.sleep(5)
    228 
    229         raise TestSuiteException('Cannot ping the device/emulator through '
    230                                  '"adb shell". Tried %s times. Is "adb" stuck '
    231                                  'or dead?' % tries)
    232 
    233     def shell(self, cmd, async=False, timeout=None):
    234         '''Run a command via the adb shell.
    235 
    236         Args:
    237             cmd: The command (including arguments) to run in the adb shell.
    238             async: Boolean to specify whether adb should run the command
    239                    asynchronously.
    240             timeout: it specifies the number of seconds to wait for
    241                      a synchronous invocation before aborting. If unspecified or
    242                      None it waits indefinitely for the command to complete
    243 
    244         Returns:
    245             If adb was synchronously run, a string which is the output (standard
    246             out and error) from adb. Otherwise None.
    247         '''
    248         return self.adb('shell "{0}"'.format(cmd), async, True, timeout)
    249 
    250     def find_app_pid(self, process_name):
    251         '''Find the process ID of a process with a given name.
    252 
    253         If more than one instance of the process is running return the first pid
    254         it finds.
    255 
    256         Args:
    257             process_name: A string representing the name of the package or
    258                           binary for which the id should be found. I.e. the
    259                           string or part of the string that shows up in the "ps"
    260                           command.
    261 
    262         Returns:
    263             An integer representing the id of the process, or None if it was not
    264             found.
    265         '''
    266         self._validate_string(process_name)
    267 
    268         pid_output = self.shell('pidof ' + process_name)
    269         pid_output = re.sub(r'\*.+\*', '', pid_output)
    270         pids = pid_output.split()
    271 
    272         if len(pids) < 1:
    273             self._log.warn('Unable to find pid of: {0}'.format(process_name))
    274             return None
    275 
    276         if len(pids) > 1:
    277             self._log.warn('Found multiple instances of {0} running: {1}'
    278                            .format(process_name, pids))
    279 
    280         try:
    281             pid = int(pids[0])
    282             self._log.info('App pid found: {0}'.format(pids[0]))
    283             return pid
    284         except ValueError:
    285             return None
    286 
    287     def adb_root(self):
    288         '''Set adb to be in root mode.'''
    289         self.adb('root')
    290 
    291     def _adb_remount(self):
    292         '''Remount the filesystem of the device.'''
    293         self.adb('remount')
    294 
    295     def validate_adb(self):
    296         '''Validate adb that it can be run.
    297 
    298         Raises:
    299             TestSuiteException: Unable to validate that adb exists and runs
    300                                 successfully.
    301         '''
    302         out = self.adb('version', False, False)
    303         if out and 'Android' in out and 'version' in out:
    304             self._log.info('adb found: {0}'.format(out))
    305             return None
    306         raise TestSuiteException('unable to validate adb')
    307 
    308     def is_booted(self):
    309         ''' Check if the device/emulator has finished booting.
    310 
    311         Returns: True if the property sys.boot_completed is true, False
    312                  otherwise.
    313         '''
    314         return self._get_prop('sys.boot_completed').strip() == '1'
    315 
    316     def validate_device(self, check_boot=True, device_substring=''):
    317         '''Validate that there is at least one device.
    318 
    319         Args:
    320             check_boot: Boolean to specify whether to check whether the device
    321                         has finished booting as well as being present.
    322             device_substring: String that needs to be part of the name of the
    323                               device.
    324 
    325         Raises:
    326             TestSuiteException: There was a failure to run adb to list the
    327                                 devices or there is no device connected or
    328                                 multiple devices connected without the user
    329                                 having specified the device to use.
    330         '''
    331 
    332         out = self.adb('devices', False, False)
    333         if not 'List of devices attached' in out:
    334             raise TestSuiteException('Unable to list devices')
    335 
    336         lines = out.split('\n')
    337         found_device = False # True if the specified device is found
    338         devices = []
    339 
    340         for line in lines[1:]:
    341             if '\tdevice' in line and device_substring in line:
    342                 device = line.split()[0]
    343                 devices.append(device)
    344                 if self.device:
    345                     if self.device == device:
    346                         found_device = True
    347 
    348         if len(devices) == 0:
    349             raise TestSuiteException('adb is unable to find a connected '
    350                                      'device/emulator to test.')
    351 
    352         if not self.device:
    353             if len(devices) == 1:
    354                 self.device = devices[0]
    355             else:
    356                 raise TestSuiteException('Multiple devices connected,'
    357                                          'specify -d device id.')
    358         else:
    359             if not found_device:
    360                 raise TestSuiteException('Couldn\'t find the device {0} that '
    361                                          'was specified, please check -d '
    362                                          'argument'.format(self.device))
    363 
    364         if check_boot and not self.is_booted():
    365             raise TestSuiteException(
    366                 'The device {0} has not yet finished booting.'
    367                 .format(self.device))
    368 
    369     def device_with_substring_exists(self, device_substring):
    370         '''Check whether a device exists whose name contains a given string.
    371 
    372         Args:
    373             device_substring: String that is part of the name of the device to
    374                               look for.
    375 
    376         Raises:
    377             TestSuiteException: There was a failure to run adb to list the
    378                                 devices.
    379         '''
    380         out = self.adb('devices', False, False)
    381         if not 'List of devices attached' in out:
    382             raise TestSuiteException('Unable to list devices')
    383 
    384         lines = out.split('\n')
    385 
    386         for line in lines[1:]:
    387             if '\tdevice' in line:
    388                 device = line.split()[0]
    389                 if device.find(device_substring) != -1:
    390                     return True
    391 
    392         return False
    393 
    394     def get_device_id(self):
    395         '''Return ID of the device that will be used for running the tests on.
    396 
    397         Returns:
    398             String representing device ID.
    399         '''
    400         return self.device
    401 
    402     def _kill_pid(self, pid):
    403         '''Kill a process identified by its pid by issuing a "kill" command.
    404 
    405         Args:
    406             pid: The integer that is the process id of the process to be killed.
    407         '''
    408         self.shell('kill -9 ' + str(pid))
    409 
    410     def stop_app(self, package_name):
    411         '''Terminate an app by calling am force-stop.
    412 
    413         Args:
    414             package_name: The string representing the name of the package of the
    415                           app that is to be stopped.
    416         '''
    417         self._validate_string(package_name)
    418         self.shell('am force-stop ' + package_name)
    419 
    420     def kill_process(self, name):
    421         '''Kill a process identified by its name (package name in case of apk).
    422 
    423         Issues the "kill" command.
    424 
    425         Args:
    426             name: The string representing the name of the binary of the process
    427                   that is to be killed.
    428 
    429         Returns:
    430             True if the kill command was executed, False if it could not be
    431             found.
    432         '''
    433         pid = self.find_app_pid(name)
    434         if pid:
    435             self._kill_pid(pid)
    436             return True
    437         return False
    438 
    439     def kill_all_processes(self, name):
    440         '''Repeatedly try to call "kill" on a process to ensure it is gone.
    441 
    442         If the process is still there after 5 attempts reboot the device.
    443 
    444         Args:
    445             name: The string representing the name of the binary of the process
    446                   that is to be killed.
    447 
    448         Raises:
    449             TestSuiteException: If the process could not be killed after 5
    450                                 attempts and the device then failed to boot
    451                                 after rebooting.
    452         '''
    453 
    454         # try 5 times to kill this process
    455         for _ in range(1, 5):
    456             if not self.kill_process(name):
    457                 return
    458         # stalled process must reboot
    459         self._reboot_device()
    460 
    461     def kill_servers(self):
    462         '''Kill all gdbserver and lldb-server instances.
    463 
    464         Raises:
    465             TestSuiteException: If gdbserver or lldb-server could not be killed
    466                                 after 5 attempts and the device then failed to
    467                                 boot after rebooting.
    468         '''
    469         self.kill_all_processes('gdbserver')
    470         self.kill_all_processes('lldb-server')
    471 
    472     def launch_elf(self, binary_name):
    473         '''Launch a binary (compiled with the NDK).
    474 
    475         Args:
    476             binary_name: The string representing the name of the binary that is
    477                          to be launched.
    478 
    479         Returns:
    480             Boolean, failure if the app is not installed, success otherwise.
    481         '''
    482         # Ensure the apk is actually installed.
    483         output = self.shell('ls /data/ | grep ' + binary_name)
    484         if binary_name not in output:
    485             return False
    486 
    487         stdout = self.shell('exec /data/' + binary_name, True)
    488         self._log.info(str(stdout))
    489 
    490         return True
    491 
    492     def wait_for_device(self):
    493         '''Ask ADB to wait for a device to become ready.'''
    494         self.adb('wait-for-device')
    495 
    496     def _reboot_device(self):
    497         '''Reboot the remote device.
    498 
    499         Raises:
    500             TestSuiteException: If the device failed to boot after rebooting.
    501         '''
    502         self.adb('reboot')
    503         self.wait_for_device()
    504         # Allow 20  mins boot time to give emulators such as MIPS enough time
    505         sleeping_countdown = 60*20
    506         while not self.is_booted():
    507             time.sleep(1)
    508             sleeping_countdown -= 1
    509             if sleeping_countdown == 0:
    510                 raise TestSuiteException('Failed to reboot. Terminating.')
    511 
    512         self.adb_root()
    513         self.wait_for_device()
    514         self._adb_remount()
    515         self.wait_for_device()
    516 
    517     def launch_app(self, name, activity):
    518         '''Launch a Renderscript application.
    519 
    520         Args:
    521             name: The string representing the name of the app that is to be
    522                   launched.
    523             activity: The string representing the activity of the app that is to
    524                       be started.
    525 
    526         Returns:
    527             Boolean, failure if the apk is not installed, success otherwise.
    528         '''
    529         assert name and activity
    530 
    531         # Ensure the apk is actually installed.
    532         output = self.shell('pm list packages ' + name)
    533         if not output:
    534             return False
    535 
    536         cmd = 'am start -S -W {0}/{0}.{1}'.format(name, activity)
    537         stdout = self.shell(cmd)
    538 
    539         self._log.info(str(stdout))
    540 
    541         return True
    542 
    543     def launch_lldb_platform(self, port):
    544         '''Launch lldb server and attach to target app.
    545 
    546         Args:
    547             port: The integer that is the port on which lldb should listen.
    548         '''
    549         cmd = "export LLDB_DEBUGSERVER_PATH='{0}';{0} p --listen *:{1}"\
    550             .format(self._path_lldbserver, port)
    551         self.shell(cmd, True)
    552         time.sleep(5)
    553 
    554     def forward_port(self, local, remote):
    555         '''Use adb to forward a device port onto the local machine.
    556 
    557         Args:
    558             local: The integer that is the local port to forward.
    559             remote: The integer that is the remote port to which to forward.
    560         '''
    561         cmd = 'forward tcp:%s tcp:%s' % (str(local), str(remote))
    562         self.adb(cmd)
    563 
    564     def remove_port_forwarding(self):
    565         '''Remove all of the forward socket connections open in adb.
    566 
    567         Avoids a windows adb error where we can't bind to a listener
    568         because too many files are open.
    569         '''
    570         self.adb('forward --remove-all')
    571 
    572     def _get_prop(self, name):
    573         '''Get the value of an Android system property.
    574 
    575         Args:
    576             name: Name of the property of interest [string].
    577 
    578         Returns:
    579             Current value of the property [string].
    580         '''
    581         return self.shell('getprop %s' % str(name))
    582 
    583     def _set_prop(self, name, value):
    584         '''Set the value of an Android system property.
    585 
    586         Args:
    587             name: Name of the property of interest [string].
    588             value: Desired new value for the property [string or integer].
    589         '''
    590         self.shell("setprop %s '%s'" % (str(name), str(value)))
    591 
    592     def push_prop(self, name, new_value):
    593         '''Save the value of an Android system property and set a new value.
    594 
    595         Saves the old value onto a stack so it can be restored later.
    596 
    597         Args:
    598             name: Name of the property of interest [string].
    599             new_value: Desired new value for the property [string or integer].
    600         '''
    601         old_value = self._get_prop(name)
    602         self._set_prop(name, new_value)
    603         self._prop_stacks[name].append(old_value.strip())
    604 
    605     def pop_prop(self, name):
    606         '''Restore the value of an Android system property previously set by
    607         push_prop.
    608 
    609         Args:
    610             name: Name of the property of interest [string].
    611 
    612         Returns:
    613             Current value of the property [string].
    614         '''
    615         old_value = self._prop_stacks[name].pop()
    616         self._set_prop(name, old_value)
    617 
    618     def reset_all_props(self):
    619         '''Restore all the android properties to the state before the first push
    620 
    621         This is equivalent to popping each property the number of times it has
    622         been pushed.
    623         '''
    624         for name in self._prop_stacks:
    625             if self._prop_stacks[name] != []:
    626                 self._set_prop(name, self._prop_stacks[name][0])
    627                 self._prop_stacks[name] = []
    628 
    629     def make_device_writeable(self):
    630         ''' Ensure the device is full writable, in particular the system folder.
    631 
    632         This disables verity and remounts.
    633         '''
    634         output = self.adb('disable-verity')
    635 
    636         # if the remote is an emulator do not even try to reboot
    637         # otherwise check whether a reboot is advised
    638         if (self._get_prop('ro.kernel.qemu') != '1' and output and
    639                 'Now reboot your device for settings to take effect' in output):
    640             self._reboot_device()
    641 
    642         self._adb_remount()
    643         self.wait_for_device()
    644         self.adb_root()
    645         self.wait_for_device()
    646 
    647     @staticmethod
    648     def _execute_command_local(command, async=False):
    649         '''Execute the given shell command in the same process.
    650 
    651         Args:
    652             command: String, the command to execute
    653             async: Boolean to specify whether adb should run the command
    654                    asynchronously.
    655 
    656         Returns:
    657             if async == False, it returns a tuple with the return code and
    658             the output from the executed command. Otherwise the tuple
    659             (None, None).
    660         '''
    661         proc = subprocess.Popen(command,
    662                                 stdout=subprocess.PIPE,
    663                                 stderr=subprocess.STDOUT,
    664                                 shell=True)
    665         if async:
    666             return None, None
    667 
    668         # read the whole output from the command
    669         with proc.stdout as file_proc:
    670             output = ''.join(line for line in file_proc)
    671 
    672         # release the process state
    673         proc.terminate()
    674         return_code = proc.wait()
    675 
    676         return return_code, output
    677 
    678     @staticmethod
    679     def _execute_command_remote(command, timeout):
    680         '''Execute the given shell command remotely, in a separate process.
    681 
    682         It spawns an ad hoc process to execute the given command. It waits up
    683         to timeout for the command to complete, otherwise it aborts the
    684         execution and returns None.
    685 
    686         Args:
    687             command: String, the command to execute.
    688             timeout: the number of seconds to wait for the command to complete.
    689 
    690         Returns:
    691             a pair with the return code and the output from the command, if it
    692             completed by the specified 'timeout' seconds. Otherwise the tuple
    693             (None, None).
    694         '''
    695 
    696         channel = multiprocessing.Queue()
    697         proc = multiprocessing.Process(
    698             target=_handle_remote_request,
    699             name="Executor of `{0}'".format(command),
    700             args=(command, channel)
    701         )
    702 
    703         # execute the command
    704         proc.start()
    705         return_code = None
    706         output = None
    707 
    708         # wait for the result
    709         try:
    710             return_code, output = channel.get(True, timeout)
    711         except queue.Empty:
    712             # timeout hit, the remote process has not fulfilled our request by
    713             # the given time. We are going to return <None, None>, nothing to
    714             # do here as it already holds return_code = output = None.
    715             pass
    716 
    717         # terminate the helper process
    718         proc.terminate()
    719 
    720         return return_code, output
    721 
    722 
    723 def _handle_remote_request(command, channel):
    724     '''Entry point for the remote process.
    725 
    726     It executes the given command and reports the result into the channel.
    727     This function is supposed to be only called by
    728     UtilAndroid._execute_command_remote to handle the inter-process
    729     communication.
    730 
    731     Args:
    732         command: the command to execute.
    733         channel: the channel to communicate with the caller process.
    734     '''
    735     channel.put(UtilAndroid._execute_command_local(command))
    736 
    737