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