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