Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 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 |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 14 | import urllib |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 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 |
Ben Murdoch | a3f7b4e | 2013-07-24 10:36:34 +0100 | [diff] [blame] | 21 | from pylib import perf_tests_helper |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 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 | |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 36 | device_adb = android_commands.AndroidCommands(serial) |
| 37 | |
| 38 | # TODO(navabi): Replace AdbShellCmd with device_adb. |
Ben Murdoch | a3f7b4e | 2013-07-24 10:36:34 +0100 | [diff] [blame] | 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() |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 43 | |
Ben Murdoch | a3f7b4e | 2013-07-24 10:36:34 +0100 | [diff] [blame] | 44 | setup_wizard_disabled = device_adb.GetSetupWizardStatus() == 'DISABLED' |
| 45 | battery = device_adb.GetBatteryInfo() |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 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]) |
Ben Murdoch | 58e6fbe | 2013-07-26 10:20:38 +0100 | [diff] [blame] | 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:]) |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 66 | report = ['Device %s (%s)' % (serial, device_type), |
Ben Murdoch | a3f7b4e | 2013-07-24 10:36:34 +0100 | [diff] [blame] | 67 | ' Build: %s (%s)' % |
| 68 | (device_build, device_adb.GetBuildFingerprint()), |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 69 | ' Battery: %s%%' % battery_level, |
| 70 | ' Battery temp: %s' % battery_temp, |
Ben Murdoch | a3f7b4e | 2013-07-24 10:36:34 +0100 | [diff] [blame] | 71 | ' IMEI slice: %s' % imei_slice, |
| 72 | ' Wifi IP: %s' % device_adb.GetWifiIP(), |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 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() |
Ben Murdoch | a3f7b4e | 2013-07-24 10:36:34 +0100 | [diff] [blame] | 94 | device_adb.Shutdown() |
| 95 | full_report = '\n'.join(report) |
| 96 | return device_type, device_build, battery_level, full_report, errors, True |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 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. |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 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'))))) |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 161 | return ['Current online devices: %s' % adb_online_devs, |
| 162 | '%s are no longer visible. Were they removed?\n' % missing_devs, |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 163 | 'SHERIFF:\n', |
| 164 | '@@@STEP_LINK@Click here to file a bug@%s@@@\n' % crbug_link, |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 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@chromium.org' |
| 181 | to_address = 'chromium-android-device-alerts@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', |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 199 | default=os.path.join(constants.DIR_SOURCE_ROOT, 'out')) |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 200 | parser.add_option('--no-provisioning-check', |
| 201 | help='Will not check if devices are provisioned properly.') |
Ben Murdoch | a3f7b4e | 2013-07-24 10:36:34 +0100 | [diff] [blame] | 202 | parser.add_option('--device-status-dashboard', |
| 203 | help='Output device status data for dashboard.') |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 204 | options, args = parser.parse_args() |
| 205 | if args: |
| 206 | parser.error('Unknown options %s' % args) |
| 207 | devices = android_commands.GetAttachedDevices() |
Ben Murdoch | a3f7b4e | 2013-07-24 10:36:34 +0100 | [diff] [blame] | 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 = [], [], [], [], [] |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 214 | fail_step_lst = [] |
| 215 | if devices: |
Ben Murdoch | a3f7b4e | 2013-07-24 10:36:34 +0100 | [diff] [blame] | 216 | types, builds, batteries, reports, errors, fail_step_lst = ( |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 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 | |
Ben Murdoch | a3f7b4e | 2013-07-24 10:36:34 +0100 | [diff] [blame] | 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 | |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 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()) |