Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python
      2 # Copyright 2016 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """A script to keep track of devices across builds and report state."""
      7 
      8 import argparse
      9 import json
     10 import logging
     11 import os
     12 import re
     13 import sys
     14 
     15 if __name__ == '__main__':
     16   sys.path.append(
     17       os.path.abspath(os.path.join(os.path.dirname(__file__),
     18                                    '..', '..', '..')))
     19 from devil.android import battery_utils
     20 from devil.android import device_blacklist
     21 from devil.android import device_errors
     22 from devil.android import device_list
     23 from devil.android import device_utils
     24 from devil.android.sdk import adb_wrapper
     25 from devil.android.tools import script_common
     26 from devil.constants import exit_codes
     27 from devil.utils import logging_common
     28 from devil.utils import lsusb
     29 
     30 logger = logging.getLogger(__name__)
     31 
     32 _RE_DEVICE_ID = re.compile(r'Device ID = (\d+)')
     33 
     34 
     35 def IsBlacklisted(serial, blacklist):
     36   return blacklist and serial in blacklist.Read()
     37 
     38 
     39 def _BatteryStatus(device, blacklist):
     40   battery_info = {}
     41   try:
     42     battery = battery_utils.BatteryUtils(device)
     43     battery_info = battery.GetBatteryInfo(timeout=5)
     44     battery_level = int(battery_info.get('level', 100))
     45 
     46     if battery_level < 15:
     47       logger.error('Critically low battery level (%d)', battery_level)
     48       battery = battery_utils.BatteryUtils(device)
     49       if not battery.GetCharging():
     50         battery.SetCharging(True)
     51       if blacklist:
     52         blacklist.Extend([device.adb.GetDeviceSerial()], reason='low_battery')
     53 
     54   except (device_errors.CommandFailedError,
     55           device_errors.DeviceUnreachableError):
     56     logger.exception('Failed to get battery information for %s',
     57                      str(device))
     58 
     59   return battery_info
     60 
     61 
     62 def DeviceStatus(devices, blacklist):
     63   """Generates status information for the given devices.
     64 
     65   Args:
     66     devices: The devices to generate status for.
     67     blacklist: The current device blacklist.
     68   Returns:
     69     A dict of the following form:
     70     {
     71       '<serial>': {
     72         'serial': '<serial>',
     73         'adb_status': str,
     74         'usb_status': bool,
     75         'blacklisted': bool,
     76         # only if the device is connected and not blacklisted
     77         'type': ro.build.product,
     78         'build': ro.build.id,
     79         'build_detail': ro.build.fingerprint,
     80         'battery': {
     81           ...
     82         },
     83         'imei_slice': str,
     84         'wifi_ip': str,
     85       },
     86       ...
     87     }
     88   """
     89   adb_devices = {
     90     a[0].GetDeviceSerial(): a
     91     for a in adb_wrapper.AdbWrapper.Devices(desired_state=None, long_list=True)
     92   }
     93   usb_devices = set(lsusb.get_android_devices())
     94 
     95   def blacklisting_device_status(device):
     96     serial = device.adb.GetDeviceSerial()
     97     adb_status = (
     98         adb_devices[serial][1] if serial in adb_devices
     99         else 'missing')
    100     usb_status = bool(serial in usb_devices)
    101 
    102     device_status = {
    103       'serial': serial,
    104       'adb_status': adb_status,
    105       'usb_status': usb_status,
    106     }
    107 
    108     if not IsBlacklisted(serial, blacklist):
    109       if adb_status == 'device':
    110         try:
    111           build_product = device.build_product
    112           build_id = device.build_id
    113           build_fingerprint = device.build_fingerprint
    114           build_description = device.build_description
    115           wifi_ip = device.GetProp('dhcp.wlan0.ipaddress')
    116           battery_info = _BatteryStatus(device, blacklist)
    117           try:
    118             imei_slice = device.GetIMEI()
    119           except device_errors.CommandFailedError:
    120             logging.exception('Unable to fetch IMEI for %s.', str(device))
    121             imei_slice = 'unknown'
    122 
    123           if (device.product_name == 'mantaray' and
    124               battery_info.get('AC powered', None) != 'true'):
    125             logger.error('Mantaray device not connected to AC power.')
    126 
    127           device_status.update({
    128             'ro.build.product': build_product,
    129             'ro.build.id': build_id,
    130             'ro.build.fingerprint': build_fingerprint,
    131             'ro.build.description': build_description,
    132             'battery': battery_info,
    133             'imei_slice': imei_slice,
    134             'wifi_ip': wifi_ip,
    135           })
    136 
    137         except (device_errors.CommandFailedError,
    138                 device_errors.DeviceUnreachableError):
    139           logger.exception('Failure while getting device status for %s.',
    140                            str(device))
    141           if blacklist:
    142             blacklist.Extend([serial], reason='status_check_failure')
    143 
    144         except device_errors.CommandTimeoutError:
    145           logger.exception('Timeout while getting device status for %s.',
    146                            str(device))
    147           if blacklist:
    148             blacklist.Extend([serial], reason='status_check_timeout')
    149 
    150       elif blacklist:
    151         blacklist.Extend([serial],
    152                          reason=adb_status if usb_status else 'offline')
    153 
    154     device_status['blacklisted'] = IsBlacklisted(serial, blacklist)
    155 
    156     return device_status
    157 
    158   parallel_devices = device_utils.DeviceUtils.parallel(devices)
    159   statuses = parallel_devices.pMap(blacklisting_device_status).pGet(None)
    160   return statuses
    161 
    162 
    163 def _LogStatuses(statuses):
    164   # Log the state of all devices.
    165   for status in statuses:
    166     logger.info(status['serial'])
    167     adb_status = status.get('adb_status')
    168     blacklisted = status.get('blacklisted')
    169     logger.info('  USB status: %s',
    170                 'online' if status.get('usb_status') else 'offline')
    171     logger.info('  ADB status: %s', adb_status)
    172     logger.info('  Blacklisted: %s', str(blacklisted))
    173     if adb_status == 'device' and not blacklisted:
    174       logger.info('  Device type: %s', status.get('ro.build.product'))
    175       logger.info('  OS build: %s', status.get('ro.build.id'))
    176       logger.info('  OS build fingerprint: %s',
    177                   status.get('ro.build.fingerprint'))
    178       logger.info('  Battery state:')
    179       for k, v in status.get('battery', {}).iteritems():
    180         logger.info('    %s: %s', k, v)
    181       logger.info('  IMEI slice: %s', status.get('imei_slice'))
    182       logger.info('  WiFi IP: %s', status.get('wifi_ip'))
    183 
    184 
    185 def _WriteBuildbotFile(file_path, statuses):
    186   buildbot_path, _ = os.path.split(file_path)
    187   if os.path.exists(buildbot_path):
    188     with open(file_path, 'w') as f:
    189       for status in statuses:
    190         try:
    191           if status['adb_status'] == 'device':
    192             f.write('{serial} {adb_status} {build_product} {build_id} '
    193                     '{temperature:.1f}C {level}%\n'.format(
    194                 serial=status['serial'],
    195                 adb_status=status['adb_status'],
    196                 build_product=status['type'],
    197                 build_id=status['build'],
    198                 temperature=float(status['battery']['temperature']) / 10,
    199                 level=status['battery']['level']
    200             ))
    201           elif status.get('usb_status', False):
    202             f.write('{serial} {adb_status}\n'.format(
    203                 serial=status['serial'],
    204                 adb_status=status['adb_status']
    205             ))
    206           else:
    207             f.write('{serial} offline\n'.format(
    208                 serial=status['serial']
    209             ))
    210         except Exception: # pylint: disable=broad-except
    211           pass
    212 
    213 
    214 def GetExpectedDevices(known_devices_files):
    215   expected_devices = set()
    216   try:
    217     for path in known_devices_files:
    218       if os.path.exists(path):
    219         expected_devices.update(device_list.GetPersistentDeviceList(path))
    220       else:
    221         logger.warning('Could not find known devices file: %s', path)
    222   except IOError:
    223     logger.warning('Problem reading %s, skipping.', path)
    224 
    225   logger.info('Expected devices:')
    226   for device in expected_devices:
    227     logger.info('  %s', device)
    228   return expected_devices
    229 
    230 
    231 def AddArguments(parser):
    232   parser.add_argument('--json-output',
    233                       help='Output JSON information into a specified file.')
    234   parser.add_argument('--blacklist-file', help='Device blacklist JSON file.')
    235   parser.add_argument('--known-devices-file', action='append', default=[],
    236                       dest='known_devices_files',
    237                       help='Path to known device lists.')
    238   parser.add_argument('--buildbot-path', '-b',
    239                       default='/home/chrome-bot/.adb_device_info',
    240                       help='Absolute path to buildbot file location')
    241   parser.add_argument('-w', '--overwrite-known-devices-files',
    242                       action='store_true',
    243                       help='If set, overwrites known devices files wiht new '
    244                            'values.')
    245 
    246 def main():
    247   parser = argparse.ArgumentParser()
    248   logging_common.AddLoggingArguments(parser)
    249   script_common.AddEnvironmentArguments(parser)
    250   AddArguments(parser)
    251   args = parser.parse_args()
    252 
    253   logging_common.InitializeLogging(args)
    254   script_common.InitializeEnvironment(args)
    255 
    256   blacklist = (device_blacklist.Blacklist(args.blacklist_file)
    257                if args.blacklist_file
    258                else None)
    259 
    260   expected_devices = GetExpectedDevices(args.known_devices_files)
    261   usb_devices = set(lsusb.get_android_devices())
    262   devices = [device_utils.DeviceUtils(s)
    263              for s in expected_devices.union(usb_devices)]
    264 
    265   statuses = DeviceStatus(devices, blacklist)
    266 
    267   # Log the state of all devices.
    268   _LogStatuses(statuses)
    269 
    270   # Update the last devices file(s).
    271   if args.overwrite_known_devices_files:
    272     for path in args.known_devices_files:
    273       device_list.WritePersistentDeviceList(
    274           path, [status['serial'] for status in statuses])
    275 
    276   # Write device info to file for buildbot info display.
    277   _WriteBuildbotFile(args.buildbot_path, statuses)
    278 
    279   # Dump the device statuses to JSON.
    280   if args.json_output:
    281     with open(args.json_output, 'wb') as f:
    282       f.write(json.dumps(
    283           statuses, indent=4, sort_keys=True, separators=(',', ': ')))
    284 
    285   live_devices = [status['serial'] for status in statuses
    286                   if (status['adb_status'] == 'device'
    287                       and not IsBlacklisted(status['serial'], blacklist))]
    288 
    289   # If all devices failed, or if there are no devices, it's an infra error.
    290   if not live_devices:
    291     logger.error('No available devices.')
    292   return 0 if live_devices else exit_codes.INFRA
    293 
    294 
    295 if __name__ == '__main__':
    296   sys.exit(main())
    297