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