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 import logging 9 import optparse 10 import os 11 import smtplib 12 import sys 13 import re 14 import urllib 15 16 import bb_annotations 17 18 sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 19 from pylib import android_commands 20 from pylib import constants 21 from pylib import perf_tests_helper 22 from pylib.cmd_helper import GetCmdOutput 23 24 25 def DeviceInfo(serial, options): 26 """Gathers info on a device via various adb calls. 27 28 Args: 29 serial: The serial of the attached device to construct info about. 30 31 Returns: 32 Tuple of device type, build id, report as a string, error messages, and 33 boolean indicating whether or not device can be used for testing. 34 """ 35 36 device_adb = android_commands.AndroidCommands(serial) 37 38 # TODO(navabi): Replace AdbShellCmd with device_adb. 39 device_type = device_adb.GetBuildProduct() 40 device_build = device_adb.GetBuildId() 41 device_build_type = device_adb.GetBuildType() 42 device_product_name = device_adb.GetProductName() 43 44 setup_wizard_disabled = device_adb.GetSetupWizardStatus() == 'DISABLED' 45 battery = device_adb.GetBatteryInfo() 46 install_output = GetCmdOutput( 47 ['%s/build/android/adb_install_apk.py' % constants.DIR_SOURCE_ROOT, '--apk', 48 '%s/build/android/CheckInstallApk-debug.apk' % constants.DIR_SOURCE_ROOT]) 49 50 def _GetData(re_expression, line, lambda_function=lambda x:x): 51 if not line: 52 return 'Unknown' 53 found = re.findall(re_expression, line) 54 if found and len(found): 55 return lambda_function(found[0]) 56 return 'Unknown' 57 58 install_speed = _GetData('(\d+) KB/s', install_output) 59 ac_power = _GetData('AC powered: (\w+)', battery) 60 battery_level = _GetData('level: (\d+)', battery) 61 battery_temp = _GetData('temperature: (\d+)', battery, 62 lambda x: float(x) / 10.0) 63 imei_slice = _GetData('Device ID = (\d+)', 64 device_adb.GetSubscriberInfo(), 65 lambda x: x[-6:]) 66 report = ['Device %s (%s)' % (serial, device_type), 67 ' Build: %s (%s)' % 68 (device_build, device_adb.GetBuildFingerprint()), 69 ' Battery: %s%%' % battery_level, 70 ' Battery temp: %s' % battery_temp, 71 ' IMEI slice: %s' % imei_slice, 72 ' Wifi IP: %s' % device_adb.GetWifiIP(), 73 ' Install Speed: %s KB/s' % install_speed, 74 ''] 75 76 errors = [] 77 if battery_level < 15: 78 errors += ['Device critically low in battery. Turning off device.'] 79 if (not setup_wizard_disabled and device_build_type != 'user' and 80 not options.no_provisioning_check): 81 errors += ['Setup wizard not disabled. Was it provisioned correctly?'] 82 if device_product_name == 'mantaray' and ac_power != 'true': 83 errors += ['Mantaray device not connected to AC power.'] 84 # TODO(navabi): Insert warning once we have a better handle of what install 85 # speeds to expect. The following lines were causing too many alerts. 86 # if install_speed < 500: 87 # errors += ['Device install speed too low. Do not use for testing.'] 88 89 # Causing the device status check step fail for slow install speed or low 90 # battery currently is too disruptive to the bots (especially try bots). 91 # Turn off devices with low battery and the step does not fail. 92 if battery_level < 15: 93 device_adb.EnableAdbRoot() 94 device_adb.Shutdown() 95 full_report = '\n'.join(report) 96 return device_type, device_build, battery_level, full_report, errors, True 97 98 99 def CheckForMissingDevices(options, adb_online_devs): 100 """Uses file of previous online devices to detect broken phones. 101 102 Args: 103 options: out_dir parameter of options argument is used as the base 104 directory to load and update the cache file. 105 adb_online_devs: A list of serial numbers of the currently visible 106 and online attached devices. 107 """ 108 # TODO(navabi): remove this once the bug that causes different number 109 # of devices to be detected between calls is fixed. 110 logger = logging.getLogger() 111 logger.setLevel(logging.INFO) 112 113 out_dir = os.path.abspath(options.out_dir) 114 115 def ReadDeviceList(file_name): 116 devices_path = os.path.join(out_dir, file_name) 117 devices = [] 118 try: 119 with open(devices_path) as f: 120 devices = f.read().splitlines() 121 except IOError: 122 # Ignore error, file might not exist 123 pass 124 return devices 125 126 def WriteDeviceList(file_name, device_list): 127 path = os.path.join(out_dir, file_name) 128 if not os.path.exists(out_dir): 129 os.makedirs(out_dir) 130 with open(path, 'w') as f: 131 # Write devices currently visible plus devices previously seen. 132 f.write('\n'.join(set(device_list))) 133 134 last_devices_path = os.path.join(out_dir, '.last_devices') 135 last_devices = ReadDeviceList('.last_devices') 136 missing_devs = list(set(last_devices) - set(adb_online_devs)) 137 138 all_known_devices = list(set(adb_online_devs) | set(last_devices)) 139 WriteDeviceList('.last_devices', all_known_devices) 140 WriteDeviceList('.last_missing', missing_devs) 141 142 if not all_known_devices: 143 # This can happen if for some reason the .last_devices file is not 144 # present or if it was empty. 145 return ['No online devices. Have any devices been plugged in?'] 146 if missing_devs: 147 devices_missing_msg = '%d devices not detected.' % len(missing_devs) 148 bb_annotations.PrintSummaryText(devices_missing_msg) 149 150 # TODO(navabi): Debug by printing both output from GetCmdOutput and 151 # GetAttachedDevices to compare results. 152 crbug_link = ('https://code.google.com/p/chromium/issues/entry?summary=' 153 '%s&comment=%s&labels=Restrict-View-Google,OS-Android,Infra' % 154 (urllib.quote('Device Offline'), 155 urllib.quote('Buildbot: %s %s\n' 156 'Build: %s\n' 157 '(please don\'t change any labels)' % 158 (os.environ.get('BUILDBOT_BUILDERNAME'), 159 os.environ.get('BUILDBOT_SLAVENAME'), 160 os.environ.get('BUILDBOT_BUILDNUMBER'))))) 161 return ['Current online devices: %s' % adb_online_devs, 162 '%s are no longer visible. Were they removed?\n' % missing_devs, 163 'SHERIFF:\n', 164 '@@@STEP_LINK@Click here to file a bug@%s@@@\n' % crbug_link, 165 'Cache file: %s\n\n' % last_devices_path, 166 'adb devices: %s' % GetCmdOutput(['adb', 'devices']), 167 'adb devices(GetAttachedDevices): %s' % 168 android_commands.GetAttachedDevices()] 169 else: 170 new_devs = set(adb_online_devs) - set(last_devices) 171 if new_devs and os.path.exists(last_devices_path): 172 bb_annotations.PrintWarning() 173 bb_annotations.PrintSummaryText( 174 '%d new devices detected' % len(new_devs)) 175 print ('New devices detected %s. And now back to your ' 176 'regularly scheduled program.' % list(new_devs)) 177 178 179 def SendDeviceStatusAlert(msg): 180 from_address = 'buildbot (at] chromium.org' 181 to_address = 'chromium-android-device-alerts (at] google.com' 182 bot_name = os.environ.get('BUILDBOT_BUILDERNAME') 183 slave_name = os.environ.get('BUILDBOT_SLAVENAME') 184 subject = 'Device status check errors on %s, %s.' % (slave_name, bot_name) 185 msg_body = '\r\n'.join(['From: %s' % from_address, 'To: %s' % to_address, 186 'Subject: %s' % subject, '', msg]) 187 try: 188 server = smtplib.SMTP('localhost') 189 server.sendmail(from_address, [to_address], msg_body) 190 server.quit() 191 except Exception as e: 192 print 'Failed to send alert email. Error: %s' % e 193 194 195 def main(): 196 parser = optparse.OptionParser() 197 parser.add_option('', '--out-dir', 198 help='Directory where the device path is stored', 199 default=os.path.join(constants.DIR_SOURCE_ROOT, 'out')) 200 parser.add_option('--no-provisioning-check', 201 help='Will not check if devices are provisioned properly.') 202 parser.add_option('--device-status-dashboard', 203 help='Output device status data for dashboard.') 204 options, args = parser.parse_args() 205 if args: 206 parser.error('Unknown options %s' % args) 207 devices = android_commands.GetAttachedDevices() 208 # TODO(navabi): Test to make sure this fails and then fix call 209 offline_devices = android_commands.GetAttachedDevices(hardware=False, 210 emulator=False, 211 offline=True) 212 213 types, builds, batteries, reports, errors = [], [], [], [], [] 214 fail_step_lst = [] 215 if devices: 216 types, builds, batteries, reports, errors, fail_step_lst = ( 217 zip(*[DeviceInfo(dev, options) for dev in devices])) 218 219 err_msg = CheckForMissingDevices(options, devices) or [] 220 221 unique_types = list(set(types)) 222 unique_builds = list(set(builds)) 223 224 bb_annotations.PrintMsg('Online devices: %d. Device types %s, builds %s' 225 % (len(devices), unique_types, unique_builds)) 226 print '\n'.join(reports) 227 228 for serial, dev_errors in zip(devices, errors): 229 if dev_errors: 230 err_msg += ['%s errors:' % serial] 231 err_msg += [' %s' % error for error in dev_errors] 232 233 if err_msg: 234 bb_annotations.PrintWarning() 235 msg = '\n'.join(err_msg) 236 print msg 237 SendDeviceStatusAlert(msg) 238 239 if options.device_status_dashboard: 240 perf_tests_helper.PrintPerfResult('BotDevices', 'OnlineDevices', 241 [len(devices)], 'devices') 242 perf_tests_helper.PrintPerfResult('BotDevices', 'OfflineDevices', 243 [len(offline_devices)], 'devices', 244 'unimportant') 245 for serial, battery in zip(devices, batteries): 246 perf_tests_helper.PrintPerfResult('DeviceBattery', serial, [battery], '%', 247 'unimportant') 248 249 if False in fail_step_lst: 250 # TODO(navabi): Build fails on device status check step if there exists any 251 # devices with critically low battery or install speed. Remove those devices 252 # from testing, allowing build to continue with good devices. 253 return 1 254 255 if not devices: 256 return 1 257 258 259 if __name__ == '__main__': 260 sys.exit(main()) 261