media/webrtc/trunk/build/android/pylib/android_commands.py

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 # Copyright (c) 2012 The Chromium 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.
     5 """Provides an interface to communicate with the device via the adb command.
     7 Assumes adb binary is currently on system path.
     8 """
    10 import collections
    11 import datetime
    12 import logging
    13 import os
    14 import re
    15 import shlex
    16 import subprocess
    17 import sys
    18 import tempfile
    19 import time
    21 import io_stats_parser
    22 from pylib import pexpect
    24 CHROME_SRC = os.path.join(
    25     os.path.abspath(os.path.dirname(__file__)), '..', '..', '..')
    27 sys.path.append(os.path.join(CHROME_SRC, 'third_party', 'android_testrunner'))
    28 import adb_interface
    30 import cmd_helper
    31 import errors  #  is under ../../../third_party/android_testrunner/errors.py
    34 # Pattern to search for the next whole line of pexpect output and capture it
    35 # into a match group. We can't use ^ and $ for line start end with pexpect,
    36 # see http://www.noah.org/python/pexpect/#doc for explanation why.
    37 PEXPECT_LINE_RE = re.compile('\n([^\r]*)\r')
    39 # Set the adb shell prompt to be a unique marker that will [hopefully] not
    40 # appear at the start of any line of a command's output.
    41 SHELL_PROMPT = '~+~PQ\x17RS~+~'
    43 # Java properties file
    44 LOCAL_PROPERTIES_PATH = '/data/local.prop'
    46 # Property in /data/local.prop that controls Java assertions.
    47 JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions'
    49 MEMORY_INFO_RE = re.compile('^(?P<key>\w+):\s+(?P<usage_kb>\d+) kB$')
    50 NVIDIA_MEMORY_INFO_RE = re.compile('^\s*(?P<user>\S+)\s*(?P<name>\S+)\s*'
    51                                    '(?P<pid>\d+)\s*(?P<usage_bytes>\d+)$')
    53 # Keycode "enum" suitable for passing to AndroidCommands.SendKey().
    54 KEYCODE_HOME = 3
    55 KEYCODE_BACK = 4
    56 KEYCODE_DPAD_UP = 19
    57 KEYCODE_DPAD_DOWN = 20
    58 KEYCODE_DPAD_RIGHT = 22
    59 KEYCODE_ENTER = 66
    60 KEYCODE_MENU = 82
    62 MD5SUM_DEVICE_PATH = '/data/local/tmp/md5sum_bin'
    64 def GetEmulators():
    65   """Returns a list of emulators.  Does not filter by status (e.g. offline).
    67   Both devices starting with 'emulator' will be returned in below output:
    69     * daemon not running. starting it now on port 5037 *
    70     * daemon started successfully *
    71     List of devices attached
    72     027c10494100b4d7        device
    73     emulator-5554   offline
    74     emulator-5558   device
    75   """
    76   re_device = re.compile('^emulator-[0-9]+', re.MULTILINE)
    77   devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices']))
    78   return devices
    81 def GetAVDs():
    82   """Returns a list of AVDs."""
    83   re_avd = re.compile('^[ ]+Name: ([a-zA-Z0-9_:.-]+)', re.MULTILINE)
    84   avds = re_avd.findall(cmd_helper.GetCmdOutput(['android', 'list', 'avd']))
    85   return avds
    88 def GetAttachedDevices():
    89   """Returns a list of attached, online android devices.
    91   If a preferred device has been set with ANDROID_SERIAL, it will be first in
    92   the returned list.
    94   Example output:
    96     * daemon not running. starting it now on port 5037 *
    97     * daemon started successfully *
    98     List of devices attached
    99     027c10494100b4d7        device
   100     emulator-5554   offline
   101   """
   102   re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE)
   103   devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices']))
   104   preferred_device = os.environ.get('ANDROID_SERIAL')
   105   if preferred_device in devices:
   106     devices.remove(preferred_device)
   107     devices.insert(0, preferred_device)
   108   return devices
   110 def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None):
   111   """Gets a list of files from `ls` command output.
   113   Python's os.walk isn't used because it doesn't work over adb shell.
   115   Args:
   116     path: The path to list.
   117     ls_output: A list of lines returned by an `ls -lR` command.
   118     re_file: A compiled regular expression which parses a line into named groups
   119         consisting of at minimum "filename", "date", "time", "size" and
   120         optionally "timezone".
   121     utc_offset: A 5-character string of the form +HHMM or -HHMM, where HH is a
   122         2-digit string giving the number of UTC offset hours, and MM is a
   123         2-digit string giving the number of UTC offset minutes. If the input
   124         utc_offset is None, will try to look for the value of "timezone" if it
   125         is specified in re_file.
   127   Returns:
   128     A dict of {"name": (size, lastmod), ...} where:
   129       name: The file name relative to |path|'s directory.
   130       size: The file size in bytes (0 for directories).
   131       lastmod: The file last modification date in UTC.
   132   """
   133   re_directory = re.compile('^%s/(?P<dir>[^:]+):$' % re.escape(path))
   134   path_dir = os.path.dirname(path)
   136   current_dir = ''
   137   files = {}
   138   for line in ls_output:
   139     directory_match = re_directory.match(line)
   140     if directory_match:
   141       current_dir = directory_match.group('dir')
   142       continue
   143     file_match = re_file.match(line)
   144     if file_match:
   145       filename = os.path.join(current_dir, file_match.group('filename'))
   146       if filename.startswith(path_dir):
   147         filename = filename[len(path_dir)+1:]
   148       lastmod = datetime.datetime.strptime(
   149           file_match.group('date') + ' ' + file_match.group('time')[:5],
   150           '%Y-%m-%d %H:%M')
   151       if not utc_offset and 'timezone' in re_file.groupindex:
   152         utc_offset = file_match.group('timezone')
   153       if isinstance(utc_offset, str) and len(utc_offset) == 5:
   154         utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]),
   155                                        minutes=int(utc_offset[3:5]))
   156         if utc_offset[0:1] == '-':
   157           utc_delta = -utc_delta
   158         lastmod -= utc_delta
   159       files[filename] = (int(file_match.group('size')), lastmod)
   160   return files
   162 def _ComputeFileListHash(md5sum_output):
   163   """Returns a list of MD5 strings from the provided md5sum output."""
   164   return [line.split('  ')[0] for line in md5sum_output]
   166 def _HasAdbPushSucceeded(command_output):
   167   """Returns whether adb push has succeeded from the provided output."""
   168   if not command_output:
   169     return False
   170   # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)"
   171   # Errors look like this: "failed to copy  ... "
   172   if not re.search('^[0-9]', command_output.splitlines()[-1]):
   173     logging.critical('PUSH FAILED: ' + command_output)
   174     return False
   175   return True
   177 def GetLogTimestamp(log_line, year):
   178   """Returns the timestamp of the given |log_line| in the given year."""
   179   try:
   180     return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]),
   181                                       '%Y-%m-%d %H:%M:%S.%f')
   182   except (ValueError, IndexError):
   183     logging.critical('Error reading timestamp from ' + log_line)
   184     return None
   187 class AndroidCommands(object):
   188   """Helper class for communicating with Android device via adb.
   190   Args:
   191     device: If given, adb commands are only send to the device of this ID.
   192         Otherwise commands are sent to all attached devices.
   193   """
   195   def __init__(self, device=None):
   196     self._adb = adb_interface.AdbInterface()
   197     if device:
   198       self._adb.SetTargetSerial(device)
   199     self._logcat = None
   200     self.logcat_process = None
   201     self._pushed_files = []
   202     self._device_utc_offset = self.RunShellCommand('date +%z')[0]
   203     self._md5sum_path = ''
   204     self._external_storage = ''
   206   def Adb(self):
   207     """Returns our AdbInterface to avoid us wrapping all its methods."""
   208     return self._adb
   210   def IsRootEnabled(self):
   211     """Checks if root is enabled on the device."""
   212     root_test_output = self.RunShellCommand('ls /root') or ['']
   213     return not 'Permission denied' in root_test_output[0]
   215   def EnableAdbRoot(self):
   216     """Enables adb root on the device.
   218     Returns:
   219       True: if output from executing adb root was as expected.
   220       False: otherwise.
   221     """
   222     return_value = self._adb.EnableAdbRoot()
   223     # EnableAdbRoot inserts a call for wait-for-device only when adb logcat
   224     # output matches what is expected. Just to be safe add a call to
   225     # wait-for-device.
   226     self._adb.SendCommand('wait-for-device')
   227     return return_value
   229   def GetDeviceYear(self):
   230     """Returns the year information of the date on device."""
   231     return self.RunShellCommand('date +%Y')[0]
   233   def GetExternalStorage(self):
   234     if not self._external_storage:
   235       self._external_storage = self.RunShellCommand('echo $EXTERNAL_STORAGE')[0]
   236       assert self._external_storage, 'Unable to find $EXTERNAL_STORAGE'
   237     return self._external_storage
   239   def WaitForDevicePm(self):
   240     """Blocks until the device's package manager is available.
   242     To workaround http://b/5201039, we restart the shell and retry if the
   243     package manager isn't back after 120 seconds.
   245     Raises:
   246       errors.WaitForResponseTimedOutError after max retries reached.
   247     """
   248     last_err = None
   249     retries = 3
   250     while retries:
   251       try:
   252         self._adb.WaitForDevicePm()
   253         return  # Success
   254       except errors.WaitForResponseTimedOutError as e:
   255         last_err = e
   256         logging.warning('Restarting and retrying after timeout: %s', e)
   257         retries -= 1
   258         self.RestartShell()
   259     raise last_err  # Only reached after max retries, re-raise the last error.
   261   def RestartShell(self):
   262     """Restarts the shell on the device. Does not block for it to return."""
   263     self.RunShellCommand('stop')
   264     self.RunShellCommand('start')
   266   def Reboot(self, full_reboot=True):
   267     """Reboots the device and waits for the package manager to return.
   269     Args:
   270       full_reboot: Whether to fully reboot the device or just restart the shell.
   271     """
   272     # TODO(torne): hive can't reboot the device either way without breaking the
   273     # connection; work out if we can handle this better
   274     if os.environ.get('USING_HIVE'):
   275       logging.warning('Ignoring reboot request as we are on hive')
   276       return
   277     if full_reboot or not self.IsRootEnabled():
   278       self._adb.SendCommand('reboot')
   279       timeout = 300
   280     else:
   281       self.RestartShell()
   282       timeout = 120
   283     # To run tests we need at least the package manager and the sd card (or
   284     # other external storage) to be ready.
   285     self.WaitForDevicePm()
   286     self.WaitForSdCardReady(timeout)
   288   def Uninstall(self, package):
   289     """Uninstalls the specified package from the device.
   291     Args:
   292       package: Name of the package to remove.
   294     Returns:
   295       A status string returned by adb uninstall
   296     """
   297     uninstall_command = 'uninstall %s' % package
   299     logging.info('>>> $' + uninstall_command)
   300     return self._adb.SendCommand(uninstall_command, timeout_time=60)
   302   def Install(self, package_file_path, reinstall=False):
   303     """Installs the specified package to the device.
   305     Args:
   306       package_file_path: Path to .apk file to install.
   307       reinstall: Reinstall an existing apk, keeping the data.
   309     Returns:
   310       A status string returned by adb install
   311     """
   312     assert os.path.isfile(package_file_path), ('<%s> is not file' %
   313                                                package_file_path)
   315     install_cmd = ['install']
   317     if reinstall:
   318       install_cmd.append('-r')
   320     install_cmd.append(package_file_path)
   321     install_cmd = ' '.join(install_cmd)
   323     logging.info('>>> $' + install_cmd)
   324     return self._adb.SendCommand(install_cmd, timeout_time=2*60, retry_count=0)
   326   def ManagedInstall(self, apk_path, keep_data=False, package_name=None,
   327                      reboots_on_failure=2):
   328     """Installs specified package and reboots device on timeouts.
   330     Args:
   331       apk_path: Path to .apk file to install.
   332       keep_data: Reinstalls instead of uninstalling first, preserving the
   333         application data.
   334       package_name: Package name (only needed if keep_data=False).
   335       reboots_on_failure: number of time to reboot if package manager is frozen.
   337     Returns:
   338       A status string returned by adb install
   339     """
   340     reboots_left = reboots_on_failure
   341     while True:
   342       try:
   343         if not keep_data:
   344           assert package_name
   345           self.Uninstall(package_name)
   346         install_status = self.Install(apk_path, reinstall=keep_data)
   347         if 'Success' in install_status:
   348           return install_status
   349       except errors.WaitForResponseTimedOutError:
   350         print '@@@STEP_WARNINGS@@@'
   351         logging.info('Timeout on installing %s' % apk_path)
   353       if reboots_left <= 0:
   354         raise Exception('Install failure')
   356       # Force a hard reboot on last attempt
   357       self.Reboot(full_reboot=(reboots_left == 1))
   358       reboots_left -= 1
   360   def MakeSystemFolderWritable(self):
   361     """Remounts the /system folder rw."""
   362     out = self._adb.SendCommand('remount')
   363     if out.strip() != 'remount succeeded':
   364       raise errors.MsgException('Remount failed: %s' % out)
   366   def RestartAdbServer(self):
   367     """Restart the adb server."""
   368     self.KillAdbServer()
   369     self.StartAdbServer()
   371   def KillAdbServer(self):
   372     """Kill adb server."""
   373     adb_cmd = ['adb', 'kill-server']
   374     return cmd_helper.RunCmd(adb_cmd)
   376   def StartAdbServer(self):
   377     """Start adb server."""
   378     adb_cmd = ['adb', 'start-server']
   379     return cmd_helper.RunCmd(adb_cmd)
   381   def WaitForSystemBootCompleted(self, wait_time):
   382     """Waits for targeted system's boot_completed flag to be set.
   384     Args:
   385       wait_time: time in seconds to wait
   387     Raises:
   388       WaitForResponseTimedOutError if wait_time elapses and flag still not
   389       set.
   390     """
   391     logging.info('Waiting for system boot completed...')
   392     self._adb.SendCommand('wait-for-device')
   393     # Now the device is there, but system not boot completed.
   394     # Query the sys.boot_completed flag with a basic command
   395     boot_completed = False
   396     attempts = 0
   397     wait_period = 5
   398     while not boot_completed and (attempts * wait_period) < wait_time:
   399       output = self._adb.SendShellCommand('getprop sys.boot_completed',
   400                                           retry_count=1)
   401       output = output.strip()
   402       if output == '1':
   403         boot_completed = True
   404       else:
   405         # If 'error: xxx' returned when querying the flag, it means
   406         # adb server lost the connection to the emulator, so restart the adb
   407         # server.
   408         if 'error:' in output:
   409           self.RestartAdbServer()
   410         time.sleep(wait_period)
   411         attempts += 1
   412     if not boot_completed:
   413       raise errors.WaitForResponseTimedOutError(
   414           'sys.boot_completed flag was not set after %s seconds' % wait_time)
   416   def WaitForSdCardReady(self, timeout_time):
   417     """Wait for the SD card ready before pushing data into it."""
   418     logging.info('Waiting for SD card ready...')
   419     sdcard_ready = False
   420     attempts = 0
   421     wait_period = 5
   422     external_storage = self.GetExternalStorage()
   423     while not sdcard_ready and attempts * wait_period < timeout_time:
   424       output = self.RunShellCommand('ls ' + external_storage)
   425       if output:
   426         sdcard_ready = True
   427       else:
   428         time.sleep(wait_period)
   429         attempts += 1
   430     if not sdcard_ready:
   431       raise errors.WaitForResponseTimedOutError(
   432           'SD card not ready after %s seconds' % timeout_time)
   434   # It is tempting to turn this function into a generator, however this is not
   435   # possible without using a private (local) adb_shell instance (to ensure no
   436   # other command interleaves usage of it), which would defeat the main aim of
   437   # being able to reuse the adb shell instance across commands.
   438   def RunShellCommand(self, command, timeout_time=20, log_result=False):
   439     """Send a command to the adb shell and return the result.
   441     Args:
   442       command: String containing the shell command to send. Must not include
   443                the single quotes as we use them to escape the whole command.
   444       timeout_time: Number of seconds to wait for command to respond before
   445         retrying, used by AdbInterface.SendShellCommand.
   446       log_result: Boolean to indicate whether we should log the result of the
   447                   shell command.
   449     Returns:
   450       list containing the lines of output received from running the command
   451     """
   452     logging.info('>>> $' + command)
   453     if "'" in command: logging.warning(command + " contains ' quotes")
   454     result = self._adb.SendShellCommand(
   455         "'%s'" % command, timeout_time).splitlines()
   456     if ['error: device not found'] == result:
   457       raise errors.DeviceUnresponsiveError('device not found')
   458     if log_result:
   459       logging.info('\n>>> '.join(result))
   460     return result
   462   def KillAll(self, process):
   463     """Android version of killall, connected via adb.
   465     Args:
   466       process: name of the process to kill off
   468     Returns:
   469       the number of processes killed
   470     """
   471     pids = self.ExtractPid(process)
   472     if pids:
   473       self.RunShellCommand('kill ' + ' '.join(pids))
   474     return len(pids)
   476   def KillAllBlocking(self, process, timeout_sec):
   477     """Blocking version of killall, connected via adb.
   479     This waits until no process matching the corresponding name appears in ps'
   480     output anymore.
   482     Args:
   483       process: name of the process to kill off
   484       timeout_sec: the timeout in seconds
   486     Returns:
   487       the number of processes killed
   488     """
   489     processes_killed = self.KillAll(process)
   490     if processes_killed:
   491       elapsed = 0
   492       wait_period = 0.1
   493       # Note that this doesn't take into account the time spent in ExtractPid().
   494       while self.ExtractPid(process) and elapsed < timeout_sec:
   495         time.sleep(wait_period)
   496         elapsed += wait_period
   497       if elapsed >= timeout_sec:
   498         return 0
   499     return processes_killed
   501   def StartActivity(self, package, activity, wait_for_completion=False,
   502                     action='android.intent.action.VIEW',
   503                     category=None, data=None,
   504                     extras=None, trace_file_name=None):
   505     """Starts |package|'s activity on the device.
   507     Args:
   508       package: Name of package to start (e.g. 'com.google.android.apps.chrome').
   509       activity: Name of activity (e.g. '.Main' or
   510         'com.google.android.apps.chrome.Main').
   511       wait_for_completion: wait for the activity to finish launching (-W flag).
   512       action: string (e.g. "android.intent.action.MAIN"). Default is VIEW.
   513       category: string (e.g. "android.intent.category.HOME")
   514       data: Data string to pass to activity (e.g. 'http://www.example.com/').
   515       extras: Dict of extras to pass to activity. Values are significant.
   516       trace_file_name: If used, turns on and saves the trace to this file name.
   517     """
   518     cmd = 'am start -a %s' % action
   519     if wait_for_completion:
   520       cmd += ' -W'
   521     if category:
   522       cmd += ' -c %s' % category
   523     if package and activity:
   524       cmd += ' -n %s/%s' % (package, activity)
   525     if data:
   526       cmd += ' -d "%s"' % data
   527     if extras:
   528       for key in extras:
   529         value = extras[key]
   530         if isinstance(value, str):
   531           cmd += ' --es'
   532         elif isinstance(value, bool):
   533           cmd += ' --ez'
   534         elif isinstance(value, int):
   535           cmd += ' --ei'
   536         else:
   537           raise NotImplementedError(
   538               'Need to teach StartActivity how to pass %s extras' % type(value))
   539         cmd += ' %s %s' % (key, value)
   540     if trace_file_name:
   541       cmd += ' --start-profiler ' + trace_file_name
   542     self.RunShellCommand(cmd)
   544   def GoHome(self):
   545     """Tell the device to return to the home screen. Blocks until completion."""
   546     self.RunShellCommand('am start -W '
   547         '-a android.intent.action.MAIN -c android.intent.category.HOME')
   549   def CloseApplication(self, package):
   550     """Attempt to close down the application, using increasing violence.
   552     Args:
   553       package: Name of the process to kill off, e.g.
   554       com.google.android.apps.chrome
   555     """
   556     self.RunShellCommand('am force-stop ' + package)
   558   def ClearApplicationState(self, package):
   559     """Closes and clears all state for the given |package|."""
   560     self.CloseApplication(package)
   561     self.RunShellCommand('rm -r /data/data/%s/app_*' % package)
   562     self.RunShellCommand('rm -r /data/data/%s/cache/*' % package)
   563     self.RunShellCommand('rm -r /data/data/%s/files/*' % package)
   564     self.RunShellCommand('rm -r /data/data/%s/shared_prefs/*' % package)
   566   def SendKeyEvent(self, keycode):
   567     """Sends keycode to the device.
   569     Args:
   570       keycode: Numeric keycode to send (see "enum" at top of file).
   571     """
   572     self.RunShellCommand('input keyevent %d' % keycode)
   574   def PushIfNeeded(self, local_path, device_path):
   575     """Pushes |local_path| to |device_path|.
   577     Works for files and directories. This method skips copying any paths in
   578     |test_data_paths| that already exist on the device with the same hash.
   580     All pushed files can be removed by calling RemovePushedFiles().
   581     """
   582     assert os.path.exists(local_path), 'Local path not found %s' % local_path
   584     if not self._md5sum_path:
   585       default_build_type = os.environ.get('BUILD_TYPE', 'Debug')
   586       md5sum_path = '%s/out/%s/md5sum_bin' % (CHROME_SRC, default_build_type)
   587       if not os.path.exists(md5sum_path):
   588         md5sum_path = '%s/out/Release/md5sum_bin' % (CHROME_SRC)
   589         if not os.path.exists(md5sum_path):
   590           print >> sys.stderr, 'Please build md5sum.'
   591           sys.exit(1)
   592       command = 'push %s %s' % (md5sum_path, MD5SUM_DEVICE_PATH)
   593       assert _HasAdbPushSucceeded(self._adb.SendCommand(command))
   594       self._md5sum_path = md5sum_path
   596     self._pushed_files.append(device_path)
   597     hashes_on_device = _ComputeFileListHash(
   598         self.RunShellCommand(MD5SUM_DEVICE_PATH + ' ' + device_path))
   599     assert os.path.exists(local_path), 'Local path not found %s' % local_path
   600     hashes_on_host = _ComputeFileListHash(
   601         subprocess.Popen(
   602             '%s_host %s' % (self._md5sum_path, local_path),
   603             stdout=subprocess.PIPE, shell=True).stdout)
   604     if hashes_on_device == hashes_on_host:
   605       return
   607     # They don't match, so remove everything first and then create it.
   608     if os.path.isdir(local_path):
   609       self.RunShellCommand('rm -r %s' % device_path, timeout_time=2*60)
   610       self.RunShellCommand('mkdir -p %s' % device_path)
   612     # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout of
   613     # 60 seconds which isn't sufficient for a lot of users of this method.
   614     push_command = 'push %s %s' % (local_path, device_path)
   615     logging.info('>>> $' + push_command)
   616     output = self._adb.SendCommand(push_command, timeout_time=30*60)
   617     assert _HasAdbPushSucceeded(output)
   620   def GetFileContents(self, filename, log_result=False):
   621     """Gets contents from the file specified by |filename|."""
   622     return self.RunShellCommand('if [ -f "' + filename + '" ]; then cat "' +
   623                                 filename + '"; fi', log_result=log_result)
   625   def SetFileContents(self, filename, contents):
   626     """Writes |contents| to the file specified by |filename|."""
   627     with tempfile.NamedTemporaryFile() as f:
   628       f.write(contents)
   629       f.flush()
   630       self._adb.Push(f.name, filename)
   632   def RemovePushedFiles(self):
   633     """Removes all files pushed with PushIfNeeded() from the device."""
   634     for p in self._pushed_files:
   635       self.RunShellCommand('rm -r %s' % p, timeout_time=2*60)
   637   def ListPathContents(self, path):
   638     """Lists files in all subdirectories of |path|.
   640     Args:
   641       path: The path to list.
   643     Returns:
   644       A dict of {"name": (size, lastmod), ...}.
   645     """
   646     # Example output:
   647     # /foo/bar:
   648     # -rw-r----- 1 user group   102 2011-05-12 12:29:54.131623387 +0100 baz.txt
   649     re_file = re.compile('^-(?P<perms>[^\s]+)\s+'
   650                          '(?P<user>[^\s]+)\s+'
   651                          '(?P<group>[^\s]+)\s+'
   652                          '(?P<size>[^\s]+)\s+'
   653                          '(?P<date>[^\s]+)\s+'
   654                          '(?P<time>[^\s]+)\s+'
   655                          '(?P<filename>[^\s]+)$')
   656     return _GetFilesFromRecursiveLsOutput(
   657         path, self.RunShellCommand('ls -lR %s' % path), re_file,
   658         self._device_utc_offset)
   661   def SetJavaAssertsEnabled(self, enable):
   662     """Sets or removes the device java assertions property.
   664     Args:
   665       enable: If True the property will be set.
   667     Returns:
   668       True if the file was modified (reboot is required for it to take effect).
   669     """
   670     # First ensure the desired property is persisted.
   671     temp_props_file = tempfile.NamedTemporaryFile()
   672     properties = ''
   673     if self._adb.Pull(LOCAL_PROPERTIES_PATH, temp_props_file.name):
   674       properties = file(temp_props_file.name).read()
   675     re_search = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) +
   676                            r'\s*=\s*all\s*$', re.MULTILINE)
   677     if enable != bool(re.search(re_search, properties)):
   678       re_replace = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) +
   679                               r'\s*=\s*\w+\s*$', re.MULTILINE)
   680       properties = re.sub(re_replace, '', properties)
   681       if enable:
   682         properties += '\n%s=all\n' % JAVA_ASSERT_PROPERTY
   684       file(temp_props_file.name, 'w').write(properties)
   685       self._adb.Push(temp_props_file.name, LOCAL_PROPERTIES_PATH)
   687     # Next, check the current runtime value is what we need, and
   688     # if not, set it and report that a reboot is required.
   689     was_set = 'all' in self.RunShellCommand('getprop ' + JAVA_ASSERT_PROPERTY)
   690     if was_set == enable:
   691       return False
   693     self.RunShellCommand('setprop %s "%s"' % (JAVA_ASSERT_PROPERTY,
   694                                               enable and 'all' or ''))
   695     return True
   697   def GetBuildId(self):
   698     """Returns the build ID of the system (e.g. JRM79C)."""
   699     build_id = self.RunShellCommand('getprop ro.build.id')[0]
   700     assert build_id
   701     return build_id
   703   def GetBuildType(self):
   704     """Returns the build type of the system (e.g. eng)."""
   705     build_type = self.RunShellCommand('getprop ro.build.type')[0]
   706     assert build_type
   707     return build_type
   709   def StartMonitoringLogcat(self, clear=True, timeout=10, logfile=None,
   710                             filters=None):
   711     """Starts monitoring the output of logcat, for use with WaitForLogMatch.
   713     Args:
   714       clear: If True the existing logcat output will be cleared, to avoiding
   715              matching historical output lurking in the log.
   716       timeout: How long WaitForLogMatch will wait for the given match
   717       filters: A list of logcat filters to be used.
   718     """
   719     if clear:
   720       self.RunShellCommand('logcat -c')
   721     args = []
   722     if self._adb._target_arg:
   723       args += shlex.split(self._adb._target_arg)
   724     args += ['logcat', '-v', 'threadtime']
   725     if filters:
   726       args.extend(filters)
   727     else:
   728       args.append('*:v')
   730     if logfile:
   731       logfile = NewLineNormalizer(logfile)
   733     # Spawn logcat and syncronize with it.
   734     for _ in range(4):
   735       self._logcat = pexpect.spawn('adb', args, timeout=timeout,
   736                                    logfile=logfile)
   737       self.RunShellCommand('log startup_sync')
   738       if self._logcat.expect(['startup_sync', pexpect.EOF,
   739                               pexpect.TIMEOUT]) == 0:
   740         break
   741       self._logcat.close(force=True)
   742     else:
   743       logging.critical('Error reading from logcat: ' + str(self._logcat.match))
   744       sys.exit(1)
   746   def GetMonitoredLogCat(self):
   747     """Returns an "adb logcat" command as created by pexpected.spawn."""
   748     if not self._logcat:
   749       self.StartMonitoringLogcat(clear=False)
   750     return self._logcat
   752   def WaitForLogMatch(self, success_re, error_re, clear=False):
   753     """Blocks until a matching line is logged or a timeout occurs.
   755     Args:
   756       success_re: A compiled re to search each line for.
   757       error_re: A compiled re which, if found, terminates the search for
   758           |success_re|. If None is given, no error condition will be detected.
   759       clear: If True the existing logcat output will be cleared, defaults to
   760           false.
   762     Raises:
   763       pexpect.TIMEOUT upon the timeout specified by StartMonitoringLogcat().
   765     Returns:
   766       The re match object if |success_re| is matched first or None if |error_re|
   767       is matched first.
   768     """
   769     logging.info('<<< Waiting for logcat:' + str(success_re.pattern))
   770     t0 = time.time()
   771     while True:
   772       if not self._logcat:
   773         self.StartMonitoringLogcat(clear)
   774       try:
   775         while True:
   776           # Note this will block for upto the timeout _per log line_, so we need
   777           # to calculate the overall timeout remaining since t0.
   778           time_remaining = t0 + self._logcat.timeout - time.time()
   779           if time_remaining < 0: raise pexpect.TIMEOUT(self._logcat)
   780           self._logcat.expect(PEXPECT_LINE_RE, timeout=time_remaining)
   781           line = self._logcat.match.group(1)
   782           if error_re:
   783             error_match = error_re.search(line)
   784             if error_match:
   785               return None
   786           success_match = success_re.search(line)
   787           if success_match:
   788             return success_match
   789           logging.info('<<< Skipped Logcat Line:' + str(line))
   790       except pexpect.TIMEOUT:
   791         raise pexpect.TIMEOUT(
   792             'Timeout (%ds) exceeded waiting for pattern "%s" (tip: use -vv '
   793             'to debug)' %
   794             (self._logcat.timeout, success_re.pattern))
   795       except pexpect.EOF:
   796         # It seems that sometimes logcat can end unexpectedly. This seems
   797         # to happen during Chrome startup after a reboot followed by a cache
   798         # clean. I don't understand why this happens, but this code deals with
   799         # getting EOF in logcat.
   800         logging.critical('Found EOF in adb logcat. Restarting...')
   801         # Rerun spawn with original arguments. Note that self._logcat.args[0] is
   802         # the path of adb, so we don't want it in the arguments.
   803         self._logcat = pexpect.spawn('adb',
   804                                      self._logcat.args[1:],
   805                                      timeout=self._logcat.timeout,
   806                                      logfile=self._logcat.logfile)
   808   def StartRecordingLogcat(self, clear=True, filters=['*:v']):
   809     """Starts recording logcat output to eventually be saved as a string.
   811     This call should come before some series of tests are run, with either
   812     StopRecordingLogcat or SearchLogcatRecord following the tests.
   814     Args:
   815       clear: True if existing log output should be cleared.
   816       filters: A list of logcat filters to be used.
   817     """
   818     if clear:
   819       self._adb.SendCommand('logcat -c')
   820     logcat_command = 'adb %s logcat -v threadtime %s' % (self._adb._target_arg,
   821                                                          ' '.join(filters))
   822     self.logcat_process = subprocess.Popen(logcat_command, shell=True,
   823                                            stdout=subprocess.PIPE)
   825   def StopRecordingLogcat(self):
   826     """Stops an existing logcat recording subprocess and returns output.
   828     Returns:
   829       The logcat output as a string or an empty string if logcat was not
   830       being recorded at the time.
   831     """
   832     if not self.logcat_process:
   833       return ''
   834     # Cannot evaluate directly as 0 is a possible value.
   835     # Better to read the self.logcat_process.stdout before killing it,
   836     # Otherwise the communicate may return incomplete output due to pipe break.
   837     if self.logcat_process.poll() is None:
   838       self.logcat_process.kill()
   839     (output, _) = self.logcat_process.communicate()
   840     self.logcat_process = None
   841     return output
   843   def SearchLogcatRecord(self, record, message, thread_id=None, proc_id=None,
   844                          log_level=None, component=None):
   845     """Searches the specified logcat output and returns results.
   847     This method searches through the logcat output specified by record for a
   848     certain message, narrowing results by matching them against any other
   849     specified criteria.  It returns all matching lines as described below.
   851     Args:
   852       record: A string generated by Start/StopRecordingLogcat to search.
   853       message: An output string to search for.
   854       thread_id: The thread id that is the origin of the message.
   855       proc_id: The process that is the origin of the message.
   856       log_level: The log level of the message.
   857       component: The name of the component that would create the message.
   859     Returns:
   860       A list of dictionaries represeting matching entries, each containing keys
   861       thread_id, proc_id, log_level, component, and message.
   862     """
   863     if thread_id:
   864       thread_id = str(thread_id)
   865     if proc_id:
   866       proc_id = str(proc_id)
   867     results = []
   868     reg = re.compile('(\d+)\s+(\d+)\s+([A-Z])\s+([A-Za-z]+)\s*:(.*)$',
   869                      re.MULTILINE)
   870     log_list = reg.findall(record)
   871     for (tid, pid, log_lev, comp, msg) in log_list:
   872       if ((not thread_id or thread_id == tid) and
   873           (not proc_id or proc_id == pid) and
   874           (not log_level or log_level == log_lev) and
   875           (not component or component == comp) and msg.find(message) > -1):
   876         match = dict({'thread_id': tid, 'proc_id': pid,
   877                       'log_level': log_lev, 'component': comp,
   878                       'message': msg})
   879         results.append(match)
   880     return results
   882   def ExtractPid(self, process_name):
   883     """Extracts Process Ids for a given process name from Android Shell.
   885     Args:
   886       process_name: name of the process on the device.
   888     Returns:
   889       List of all the process ids (as strings) that match the given name.
   890       If the name of a process exactly matches the given name, the pid of
   891       that process will be inserted to the front of the pid list.
   892     """
   893     pids = []
   894     for line in self.RunShellCommand('ps', log_result=False):
   895       data = line.split()
   896       try:
   897         if process_name in data[-1]:  # name is in the last column
   898           if process_name == data[-1]:
   899             pids.insert(0, data[1])  # PID is in the second column
   900           else:
   901             pids.append(data[1])
   902       except IndexError:
   903         pass
   904     return pids
   906   def GetIoStats(self):
   907     """Gets cumulative disk IO stats since boot (for all processes).
   909     Returns:
   910       Dict of {num_reads, num_writes, read_ms, write_ms} or None if there
   911       was an error.
   912     """
   913     for line in self.GetFileContents('/proc/diskstats', log_result=False):
   914       stats = io_stats_parser.ParseIoStatsLine(line)
   915       if stats.device == 'mmcblk0':
   916         return {
   917             'num_reads': stats.num_reads_issued,
   918             'num_writes': stats.num_writes_completed,
   919             'read_ms': stats.ms_spent_reading,
   920             'write_ms': stats.ms_spent_writing,
   921         }
   922     logging.warning('Could not find disk IO stats.')
   923     return None
   925   def GetMemoryUsageForPid(self, pid):
   926     """Returns the memory usage for given pid.
   928     Args:
   929       pid: The pid number of the specific process running on device.
   931     Returns:
   932       A tuple containg:
   933       [0]: Dict of {metric:usage_kb}, for the process which has specified pid.
   934       The metric keys which may be included are: Size, Rss, Pss, Shared_Clean,
   935       Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap,
   936       KernelPageSize, MMUPageSize, Nvidia (tablet only).
   937       [1]: Detailed /proc/[PID]/smaps information.
   938     """
   939     usage_dict = collections.defaultdict(int)
   940     smaps = collections.defaultdict(dict)
   941     current_smap = ''
   942     for line in self.GetFileContents('/proc/%s/smaps' % pid, log_result=False):
   943       items = line.split()
   944       # See man 5 proc for more details. The format is:
   945       # address perms offset dev inode pathname
   946       if len(items) > 5:
   947         current_smap = ' '.join(items[5:])
   948       elif len(items) > 3:
   949         current_smap = ' '.join(items[3:])
   950       match = re.match(MEMORY_INFO_RE, line)
   951       if match:
   952         key = match.group('key')
   953         usage_kb = int(match.group('usage_kb'))
   954         usage_dict[key] += usage_kb
   955         if key not in smaps[current_smap]:
   956           smaps[current_smap][key] = 0
   957         smaps[current_smap][key] += usage_kb
   958     if not usage_dict or not any(usage_dict.values()):
   959       # Presumably the process died between ps and calling this method.
   960       logging.warning('Could not find memory usage for pid ' + str(pid))
   962     for line in self.GetFileContents('/d/nvmap/generic-0/clients',
   963                                      log_result=False):
   964       match = re.match(NVIDIA_MEMORY_INFO_RE, line)
   965       if match and match.group('pid') == pid:
   966         usage_bytes = int(match.group('usage_bytes'))
   967         usage_dict['Nvidia'] = int(round(usage_bytes / 1000.0))  # kB
   968         break
   970     return (usage_dict, smaps)
   972   def GetMemoryUsageForPackage(self, package):
   973     """Returns the memory usage for all processes whose name contains |pacakge|.
   975     Args:
   976       package: A string holding process name to lookup pid list for.
   978     Returns:
   979       A tuple containg:
   980       [0]: Dict of {metric:usage_kb}, summed over all pids associated with
   981            |name|.
   982       The metric keys which may be included are: Size, Rss, Pss, Shared_Clean,
   983       Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap,
   984       KernelPageSize, MMUPageSize, Nvidia (tablet only).
   985       [1]: a list with detailed /proc/[PID]/smaps information.
   986     """
   987     usage_dict = collections.defaultdict(int)
   988     pid_list = self.ExtractPid(package)
   989     smaps = collections.defaultdict(dict)
   991     for pid in pid_list:
   992       usage_dict_per_pid, smaps_per_pid = self.GetMemoryUsageForPid(pid)
   993       smaps[pid] = smaps_per_pid
   994       for (key, value) in usage_dict_per_pid.items():
   995         usage_dict[key] += value
   997     return usage_dict, smaps
   999   def ProcessesUsingDevicePort(self, device_port):
  1000     """Lists processes using the specified device port on loopback interface.
  1002     Args:
  1003       device_port: Port on device we want to check.
  1005     Returns:
  1006       A list of (pid, process_name) tuples using the specified port.
  1007     """
  1008     tcp_results = self.RunShellCommand('cat /proc/net/tcp', log_result=False)
  1009     tcp_address = '0100007F:%04X' % device_port
  1010     pids = []
  1011     for single_connect in tcp_results:
  1012       connect_results = single_connect.split()
  1013       # Column 1 is the TCP port, and Column 9 is the inode of the socket
  1014       if connect_results[1] == tcp_address:
  1015         socket_inode = connect_results[9]
  1016         socket_name = 'socket:[%s]' % socket_inode
  1017         lsof_results = self.RunShellCommand('lsof', log_result=False)
  1018         for single_process in lsof_results:
  1019           process_results = single_process.split()
  1020           # Ignore the line if it has less than nine columns in it, which may
  1021           # be the case when a process stops while lsof is executing.
  1022           if len(process_results) <= 8:
  1023             continue
  1024           # Column 0 is the executable name
  1025           # Column 1 is the pid
  1026           # Column 8 is the Inode in use
  1027           if process_results[8] == socket_name:
  1028             pids.append((int(process_results[1]), process_results[0]))
  1029         break
  1030     logging.info('PidsUsingDevicePort: %s', pids)
  1031     return pids
  1033   def FileExistsOnDevice(self, file_name):
  1034     """Checks whether the given file exists on the device.
  1036     Args:
  1037       file_name: Full path of file to check.
  1039     Returns:
  1040       True if the file exists, False otherwise.
  1041     """
  1042     assert '"' not in file_name, 'file_name cannot contain double quotes'
  1043     status = self._adb.SendShellCommand(
  1044         '\'test -e "%s"; echo $?\'' % (file_name))
  1045     if 'test: not found' not in status:
  1046       return int(status) == 0
  1048     status = self._adb.SendShellCommand(
  1049         '\'ls "%s" >/dev/null 2>&1; echo $?\'' % (file_name))
  1050     return int(status) == 0
  1053 class NewLineNormalizer(object):
  1054   """A file-like object to normalize EOLs to '\n'.
  1056   Pexpect runs adb within a pseudo-tty device (see
  1057   http://www.noah.org/wiki/pexpect), so any '\n' printed by adb is written
  1058   as '\r\n' to the logfile. Since adb already uses '\r\n' to terminate
  1059   lines, the log ends up having '\r\r\n' at the end of each line. This
  1060   filter replaces the above with a single '\n' in the data stream.
  1061   """
  1062   def __init__(self, output):
  1063     self._output = output
  1065   def write(self, data):
  1066     data = data.replace('\r\r\n', '\n')
  1067     self._output.write(data)
  1069   def flush(self):
  1070     self._output.flush()

mercurial