1 #!/usr/bin/env python 2 # 3 # Copyright 2013 The Chromium Authors. All rights reserved. 4 # Use of this source code is governed by a BSD-style license that can be 5 # found in the LICENSE file. 6 7 """A class to keep track of devices across builds and report state.""" 8 9 import argparse 10 import json 11 import logging 12 import os 13 import psutil 14 import re 15 import signal 16 import sys 17 18 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 19 import devil_chromium 20 from devil import devil_env 21 from devil.android import battery_utils 22 from devil.android import device_blacklist 23 from devil.android import device_errors 24 from devil.android import device_list 25 from devil.android import device_utils 26 from devil.android.sdk import adb_wrapper 27 from devil.constants import exit_codes 28 from devil.utils import lsusb 29 from devil.utils import reset_usb 30 from devil.utils import run_tests_helper 31 from pylib.constants import host_paths 32 33 _RE_DEVICE_ID = re.compile(r'Device ID = (\d+)') 34 35 36 def KillAllAdb(): 37 def GetAllAdb(): 38 for p in psutil.process_iter(): 39 try: 40 if 'adb' in p.name: 41 yield p 42 except (psutil.NoSuchProcess, psutil.AccessDenied): 43 pass 44 45 for sig in [signal.SIGTERM, signal.SIGQUIT, signal.SIGKILL]: 46 for p in GetAllAdb(): 47 try: 48 logging.info('kill %d %d (%s [%s])', sig, p.pid, p.name, 49 ' '.join(p.cmdline)) 50 p.send_signal(sig) 51 except (psutil.NoSuchProcess, psutil.AccessDenied): 52 pass 53 for p in GetAllAdb(): 54 try: 55 logging.error('Unable to kill %d (%s [%s])', p.pid, p.name, 56 ' '.join(p.cmdline)) 57 except (psutil.NoSuchProcess, psutil.AccessDenied): 58 pass 59 60 61 def _IsBlacklisted(serial, blacklist): 62 return blacklist and serial in blacklist.Read() 63 64 65 def _BatteryStatus(device, blacklist): 66 battery_info = {} 67 try: 68 battery = battery_utils.BatteryUtils(device) 69 battery_info = battery.GetBatteryInfo(timeout=5) 70 battery_level = int(battery_info.get('level', 100)) 71 72 if battery_level < 15: 73 logging.error('Critically low battery level (%d)', battery_level) 74 battery = battery_utils.BatteryUtils(device) 75 if not battery.GetCharging(): 76 battery.SetCharging(True) 77 if blacklist: 78 blacklist.Extend([device.adb.GetDeviceSerial()], reason='low_battery') 79 80 except device_errors.CommandFailedError: 81 logging.exception('Failed to get battery information for %s', 82 str(device)) 83 84 return battery_info 85 86 87 def _IMEISlice(device): 88 imei_slice = '' 89 try: 90 for l in device.RunShellCommand(['dumpsys', 'iphonesubinfo'], 91 check_return=True, timeout=5): 92 m = _RE_DEVICE_ID.match(l) 93 if m: 94 imei_slice = m.group(1)[-6:] 95 except device_errors.CommandFailedError: 96 logging.exception('Failed to get IMEI slice for %s', str(device)) 97 98 return imei_slice 99 100 101 def DeviceStatus(devices, blacklist): 102 """Generates status information for the given devices. 103 104 Args: 105 devices: The devices to generate status for. 106 blacklist: The current device blacklist. 107 Returns: 108 A dict of the following form: 109 { 110 '<serial>': { 111 'serial': '<serial>', 112 'adb_status': str, 113 'usb_status': bool, 114 'blacklisted': bool, 115 # only if the device is connected and not blacklisted 116 'type': ro.build.product, 117 'build': ro.build.id, 118 'build_detail': ro.build.fingerprint, 119 'battery': { 120 ... 121 }, 122 'imei_slice': str, 123 'wifi_ip': str, 124 }, 125 ... 126 } 127 """ 128 adb_devices = { 129 a[0].GetDeviceSerial(): a 130 for a in adb_wrapper.AdbWrapper.Devices(desired_state=None, long_list=True) 131 } 132 usb_devices = set(lsusb.get_android_devices()) 133 134 def blacklisting_device_status(device): 135 serial = device.adb.GetDeviceSerial() 136 adb_status = ( 137 adb_devices[serial][1] if serial in adb_devices 138 else 'missing') 139 usb_status = bool(serial in usb_devices) 140 141 device_status = { 142 'serial': serial, 143 'adb_status': adb_status, 144 'usb_status': usb_status, 145 } 146 147 if not _IsBlacklisted(serial, blacklist): 148 if adb_status == 'device': 149 try: 150 build_product = device.build_product 151 build_id = device.build_id 152 build_fingerprint = device.GetProp('ro.build.fingerprint', cache=True) 153 wifi_ip = device.GetProp('dhcp.wlan0.ipaddress') 154 battery_info = _BatteryStatus(device, blacklist) 155 imei_slice = _IMEISlice(device) 156 157 if (device.product_name == 'mantaray' and 158 battery_info.get('AC powered', None) != 'true'): 159 logging.error('Mantaray device not connected to AC power.') 160 161 device_status.update({ 162 'ro.build.product': build_product, 163 'ro.build.id': build_id, 164 'ro.build.fingerprint': build_fingerprint, 165 'battery': battery_info, 166 'imei_slice': imei_slice, 167 'wifi_ip': wifi_ip, 168 169 # TODO(jbudorick): Remove these once no clients depend on them. 170 'type': build_product, 171 'build': build_id, 172 'build_detail': build_fingerprint, 173 }) 174 175 except device_errors.CommandFailedError: 176 logging.exception('Failure while getting device status for %s.', 177 str(device)) 178 if blacklist: 179 blacklist.Extend([serial], reason='status_check_failure') 180 181 except device_errors.CommandTimeoutError: 182 logging.exception('Timeout while getting device status for %s.', 183 str(device)) 184 if blacklist: 185 blacklist.Extend([serial], reason='status_check_timeout') 186 187 elif blacklist: 188 blacklist.Extend([serial], 189 reason=adb_status if usb_status else 'offline') 190 191 device_status['blacklisted'] = _IsBlacklisted(serial, blacklist) 192 193 return device_status 194 195 parallel_devices = device_utils.DeviceUtils.parallel(devices) 196 statuses = parallel_devices.pMap(blacklisting_device_status).pGet(None) 197 return statuses 198 199 200 def RecoverDevices(devices, blacklist): 201 """Attempts to recover any inoperable devices in the provided list. 202 203 Args: 204 devices: The list of devices to attempt to recover. 205 blacklist: The current device blacklist, which will be used then 206 reset. 207 Returns: 208 Nothing. 209 """ 210 211 statuses = DeviceStatus(devices, blacklist) 212 213 should_restart_usb = set( 214 status['serial'] for status in statuses 215 if (not status['usb_status'] 216 or status['adb_status'] in ('offline', 'missing'))) 217 should_restart_adb = should_restart_usb.union(set( 218 status['serial'] for status in statuses 219 if status['adb_status'] == 'unauthorized')) 220 should_reboot_device = should_restart_adb.union(set( 221 status['serial'] for status in statuses 222 if status['blacklisted'])) 223 224 logging.debug('Should restart USB for:') 225 for d in should_restart_usb: 226 logging.debug(' %s', d) 227 logging.debug('Should restart ADB for:') 228 for d in should_restart_adb: 229 logging.debug(' %s', d) 230 logging.debug('Should reboot:') 231 for d in should_reboot_device: 232 logging.debug(' %s', d) 233 234 if blacklist: 235 blacklist.Reset() 236 237 if should_restart_adb: 238 KillAllAdb() 239 for serial in should_restart_usb: 240 try: 241 reset_usb.reset_android_usb(serial) 242 except IOError: 243 logging.exception('Unable to reset USB for %s.', serial) 244 if blacklist: 245 blacklist.Extend([serial], reason='usb_failure') 246 except device_errors.DeviceUnreachableError: 247 logging.exception('Unable to reset USB for %s.', serial) 248 if blacklist: 249 blacklist.Extend([serial], reason='offline') 250 251 def blacklisting_recovery(device): 252 if _IsBlacklisted(device.adb.GetDeviceSerial(), blacklist): 253 logging.debug('%s is blacklisted, skipping recovery.', str(device)) 254 return 255 256 if str(device) in should_reboot_device: 257 try: 258 device.WaitUntilFullyBooted(retries=0) 259 return 260 except (device_errors.CommandTimeoutError, 261 device_errors.CommandFailedError): 262 logging.exception('Failure while waiting for %s. ' 263 'Attempting to recover.', str(device)) 264 265 try: 266 try: 267 device.Reboot(block=False, timeout=5, retries=0) 268 except device_errors.CommandTimeoutError: 269 logging.warning('Timed out while attempting to reboot %s normally.' 270 'Attempting alternative reboot.', str(device)) 271 # The device drops offline before we can grab the exit code, so 272 # we don't check for status. 273 device.adb.Root() 274 device.adb.Shell('echo b > /proc/sysrq-trigger', expect_status=None, 275 timeout=5, retries=0) 276 except device_errors.CommandFailedError: 277 logging.exception('Failed to reboot %s.', str(device)) 278 if blacklist: 279 blacklist.Extend([device.adb.GetDeviceSerial()], 280 reason='reboot_failure') 281 except device_errors.CommandTimeoutError: 282 logging.exception('Timed out while rebooting %s.', str(device)) 283 if blacklist: 284 blacklist.Extend([device.adb.GetDeviceSerial()], 285 reason='reboot_timeout') 286 287 try: 288 device.WaitUntilFullyBooted(retries=0) 289 except device_errors.CommandFailedError: 290 logging.exception('Failure while waiting for %s.', str(device)) 291 if blacklist: 292 blacklist.Extend([device.adb.GetDeviceSerial()], 293 reason='reboot_failure') 294 except device_errors.CommandTimeoutError: 295 logging.exception('Timed out while waiting for %s.', str(device)) 296 if blacklist: 297 blacklist.Extend([device.adb.GetDeviceSerial()], 298 reason='reboot_timeout') 299 300 device_utils.DeviceUtils.parallel(devices).pMap(blacklisting_recovery) 301 302 303 def main(): 304 parser = argparse.ArgumentParser() 305 parser.add_argument('--out-dir', 306 help='Directory where the device path is stored', 307 default=os.path.join(host_paths.DIR_SOURCE_ROOT, 'out')) 308 parser.add_argument('--restart-usb', action='store_true', 309 help='DEPRECATED. ' 310 'This script now always tries to reset USB.') 311 parser.add_argument('--json-output', 312 help='Output JSON information into a specified file.') 313 parser.add_argument('--adb-path', 314 help='Absolute path to the adb binary to use.') 315 parser.add_argument('--blacklist-file', help='Device blacklist JSON file.') 316 parser.add_argument('--known-devices-file', action='append', default=[], 317 dest='known_devices_files', 318 help='Path to known device lists.') 319 parser.add_argument('-v', '--verbose', action='count', default=1, 320 help='Log more information.') 321 322 args = parser.parse_args() 323 324 run_tests_helper.SetLogLevel(args.verbose) 325 326 devil_custom_deps = None 327 if args.adb_path: 328 devil_custom_deps = { 329 'adb': { 330 devil_env.GetPlatform(): [args.adb_path], 331 }, 332 } 333 334 devil_chromium.Initialize(custom_deps=devil_custom_deps) 335 336 blacklist = (device_blacklist.Blacklist(args.blacklist_file) 337 if args.blacklist_file 338 else None) 339 340 last_devices_path = os.path.join( 341 args.out_dir, device_list.LAST_DEVICES_FILENAME) 342 args.known_devices_files.append(last_devices_path) 343 344 expected_devices = set() 345 try: 346 for path in args.known_devices_files: 347 if os.path.exists(path): 348 expected_devices.update(device_list.GetPersistentDeviceList(path)) 349 except IOError: 350 logging.warning('Problem reading %s, skipping.', path) 351 352 logging.info('Expected devices:') 353 for device in expected_devices: 354 logging.info(' %s', device) 355 356 usb_devices = set(lsusb.get_android_devices()) 357 devices = [device_utils.DeviceUtils(s) 358 for s in expected_devices.union(usb_devices)] 359 360 RecoverDevices(devices, blacklist) 361 statuses = DeviceStatus(devices, blacklist) 362 363 # Log the state of all devices. 364 for status in statuses: 365 logging.info(status['serial']) 366 adb_status = status.get('adb_status') 367 blacklisted = status.get('blacklisted') 368 logging.info(' USB status: %s', 369 'online' if status.get('usb_status') else 'offline') 370 logging.info(' ADB status: %s', adb_status) 371 logging.info(' Blacklisted: %s', str(blacklisted)) 372 if adb_status == 'device' and not blacklisted: 373 logging.info(' Device type: %s', status.get('ro.build.product')) 374 logging.info(' OS build: %s', status.get('ro.build.id')) 375 logging.info(' OS build fingerprint: %s', 376 status.get('ro.build.fingerprint')) 377 logging.info(' Battery state:') 378 for k, v in status.get('battery', {}).iteritems(): 379 logging.info(' %s: %s', k, v) 380 logging.info(' IMEI slice: %s', status.get('imei_slice')) 381 logging.info(' WiFi IP: %s', status.get('wifi_ip')) 382 383 # Update the last devices file(s). 384 for path in args.known_devices_files: 385 device_list.WritePersistentDeviceList( 386 path, [status['serial'] for status in statuses]) 387 388 # Write device info to file for buildbot info display. 389 if os.path.exists('/home/chrome-bot'): 390 with open('/home/chrome-bot/.adb_device_info', 'w') as f: 391 for status in statuses: 392 try: 393 if status['adb_status'] == 'device': 394 f.write('{serial} {adb_status} {build_product} {build_id} ' 395 '{temperature:.1f}C {level}%\n'.format( 396 serial=status['serial'], 397 adb_status=status['adb_status'], 398 build_product=status['type'], 399 build_id=status['build'], 400 temperature=float(status['battery']['temperature']) / 10, 401 level=status['battery']['level'] 402 )) 403 elif status.get('usb_status', False): 404 f.write('{serial} {adb_status}\n'.format( 405 serial=status['serial'], 406 adb_status=status['adb_status'] 407 )) 408 else: 409 f.write('{serial} offline\n'.format( 410 serial=status['serial'] 411 )) 412 except Exception: # pylint: disable=broad-except 413 pass 414 415 # Dump the device statuses to JSON. 416 if args.json_output: 417 with open(args.json_output, 'wb') as f: 418 f.write(json.dumps(statuses, indent=4)) 419 420 live_devices = [status['serial'] for status in statuses 421 if (status['adb_status'] == 'device' 422 and not _IsBlacklisted(status['serial'], blacklist))] 423 424 # If all devices failed, or if there are no devices, it's an infra error. 425 return 0 if live_devices else exit_codes.INFRA 426 427 428 if __name__ == '__main__': 429 sys.exit(main()) 430