• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
16interface with Android ADB.'''
17
18from __future__ import absolute_import
19
20import logging
21import re
22import subprocess
23import time
24import collections
25import multiprocessing
26try:
27    # Python 3
28    import queue
29except ImportError:
30    import Queue as queue
31
32from .exception import TestSuiteException
33from . import util_log
34
35
36class 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.boot.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
723def _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