Home | History | Annotate | Download | only in buildbot
      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