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 json
      9 import logging
     10 import optparse
     11 import os
     12 import psutil
     13 import re
     14 import signal
     15 import smtplib
     16 import subprocess
     17 import sys
     18 import time
     19 import urllib
     20 
     21 import bb_annotations
     22 import bb_utils
     23 
     24 sys.path.append(os.path.join(os.path.dirname(__file__),
     25                              os.pardir, os.pardir, 'util', 'lib',
     26                              'common'))
     27 import perf_tests_results_helper  # pylint: disable=F0401
     28 
     29 sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
     30 from pylib import android_commands
     31 from pylib import constants
     32 from pylib.cmd_helper import GetCmdOutput
     33 from pylib.device import device_blacklist
     34 from pylib.device import device_errors
     35 from pylib.device import device_list
     36 from pylib.device import device_utils
     37 
     38 def DeviceInfo(serial, options):
     39   """Gathers info on a device via various adb calls.
     40 
     41   Args:
     42     serial: The serial of the attached device to construct info about.
     43 
     44   Returns:
     45     Tuple of device type, build id, report as a string, error messages, and
     46     boolean indicating whether or not device can be used for testing.
     47   """
     48 
     49   device_adb = device_utils.DeviceUtils(serial)
     50   device_type = device_adb.GetProp('ro.build.product')
     51   device_build = device_adb.GetProp('ro.build.id')
     52   device_build_type = device_adb.GetProp('ro.build.type')
     53   device_product_name = device_adb.GetProp('ro.product.name')
     54 
     55   try:
     56     battery_info = device_adb.old_interface.GetBatteryInfo()
     57   except Exception as e:
     58     battery_info = {}
     59     logging.error('Unable to obtain battery info for %s, %s', serial, e)
     60 
     61   def _GetData(re_expression, line, lambda_function=lambda x:x):
     62     if not line:
     63       return 'Unknown'
     64     found = re.findall(re_expression, line)
     65     if found and len(found):
     66       return lambda_function(found[0])
     67     return 'Unknown'
     68 
     69   battery_level = int(battery_info.get('level', 100))
     70   imei_slice = _GetData('Device ID = (\d+)',
     71                         device_adb.old_interface.GetSubscriberInfo(),
     72                         lambda x: x[-6:])
     73   report = ['Device %s (%s)' % (serial, device_type),
     74             '  Build: %s (%s)' %
     75               (device_build, device_adb.GetProp('ro.build.fingerprint')),
     76             '  Current Battery Service state: ',
     77             '\n'.join(['    %s: %s' % (k, v)
     78                        for k, v in battery_info.iteritems()]),
     79             '  IMEI slice: %s' % imei_slice,
     80             '  Wifi IP: %s' % device_adb.GetProp('dhcp.wlan0.ipaddress'),
     81             '']
     82 
     83   errors = []
     84   dev_good = True
     85   if battery_level < 15:
     86     errors += ['Device critically low in battery. Turning off device.']
     87     dev_good = False
     88   if not options.no_provisioning_check:
     89     setup_wizard_disabled = (
     90         device_adb.GetProp('ro.setupwizard.mode') == 'DISABLED')
     91     if not setup_wizard_disabled and device_build_type != 'user':
     92       errors += ['Setup wizard not disabled. Was it provisioned correctly?']
     93   if (device_product_name == 'mantaray' and
     94       battery_info.get('AC powered', None) != 'true'):
     95     errors += ['Mantaray device not connected to AC power.']
     96 
     97   # Turn off devices with low battery.
     98   if battery_level < 15:
     99     try:
    100       device_adb.EnableRoot()
    101     except device_errors.CommandFailedError as e:
    102       # Attempt shutdown anyway.
    103       # TODO(jbudorick) Handle this exception appropriately after interface
    104       #                 conversions are finished.
    105       logging.error(str(e))
    106     device_adb.old_interface.Shutdown()
    107   full_report = '\n'.join(report)
    108   return device_type, device_build, battery_level, full_report, errors, dev_good
    109 
    110 
    111 def CheckForMissingDevices(options, adb_online_devs):
    112   """Uses file of previous online devices to detect broken phones.
    113 
    114   Args:
    115     options: out_dir parameter of options argument is used as the base
    116              directory to load and update the cache file.
    117     adb_online_devs: A list of serial numbers of the currently visible
    118                      and online attached devices.
    119   """
    120   # TODO(navabi): remove this once the bug that causes different number
    121   # of devices to be detected between calls is fixed.
    122   logger = logging.getLogger()
    123   logger.setLevel(logging.INFO)
    124 
    125   out_dir = os.path.abspath(options.out_dir)
    126 
    127   # last_devices denotes all known devices prior to this run
    128   last_devices_path = os.path.join(out_dir, device_list.LAST_DEVICES_FILENAME)
    129   last_missing_devices_path = os.path.join(out_dir,
    130       device_list.LAST_MISSING_DEVICES_FILENAME)
    131   try:
    132     last_devices = device_list.GetPersistentDeviceList(last_devices_path)
    133   except IOError:
    134     # Ignore error, file might not exist
    135     last_devices = []
    136 
    137   try:
    138     last_missing_devices = device_list.GetPersistentDeviceList(
    139         last_missing_devices_path)
    140   except IOError:
    141     last_missing_devices = []
    142 
    143   missing_devs = list(set(last_devices) - set(adb_online_devs))
    144   new_missing_devs = list(set(missing_devs) - set(last_missing_devices))
    145 
    146   if new_missing_devs and os.environ.get('BUILDBOT_SLAVENAME'):
    147     logging.info('new_missing_devs %s' % new_missing_devs)
    148     devices_missing_msg = '%d devices not detected.' % len(missing_devs)
    149     bb_annotations.PrintSummaryText(devices_missing_msg)
    150 
    151     from_address = 'chrome-bot (at] chromium.org'
    152     to_addresses = ['chrome-labs-tech-ticket (at] google.com',
    153                     'chrome-android-device-alert (at] google.com']
    154     cc_addresses = ['chrome-android-device-alert (at] google.com']
    155     subject = 'Devices offline on %s, %s, %s' % (
    156       os.environ.get('BUILDBOT_SLAVENAME'),
    157       os.environ.get('BUILDBOT_BUILDERNAME'),
    158       os.environ.get('BUILDBOT_BUILDNUMBER'))
    159     msg = ('Please reboot the following devices:\n%s' %
    160            '\n'.join(map(str,new_missing_devs)))
    161     SendEmail(from_address, to_addresses, cc_addresses, subject, msg)
    162 
    163   all_known_devices = list(set(adb_online_devs) | set(last_devices))
    164   device_list.WritePersistentDeviceList(last_devices_path, all_known_devices)
    165   device_list.WritePersistentDeviceList(last_missing_devices_path, missing_devs)
    166 
    167   if not all_known_devices:
    168     # This can happen if for some reason the .last_devices file is not
    169     # present or if it was empty.
    170     return ['No online devices. Have any devices been plugged in?']
    171   if missing_devs:
    172     devices_missing_msg = '%d devices not detected.' % len(missing_devs)
    173     bb_annotations.PrintSummaryText(devices_missing_msg)
    174 
    175     # TODO(navabi): Debug by printing both output from GetCmdOutput and
    176     # GetAttachedDevices to compare results.
    177     crbug_link = ('https://code.google.com/p/chromium/issues/entry?summary='
    178                   '%s&comment=%s&labels=Restrict-View-Google,OS-Android,Infra' %
    179                   (urllib.quote('Device Offline'),
    180                    urllib.quote('Buildbot: %s %s\n'
    181                                 'Build: %s\n'
    182                                 '(please don\'t change any labels)' %
    183                                 (os.environ.get('BUILDBOT_BUILDERNAME'),
    184                                  os.environ.get('BUILDBOT_SLAVENAME'),
    185                                  os.environ.get('BUILDBOT_BUILDNUMBER')))))
    186     return ['Current online devices: %s' % adb_online_devs,
    187             '%s are no longer visible. Were they removed?\n' % missing_devs,
    188             'SHERIFF:\n',
    189             '@@@STEP_LINK@Click here to file a bug@%s@@@\n' % crbug_link,
    190             'Cache file: %s\n\n' % last_devices_path,
    191             'adb devices: %s' % GetCmdOutput(['adb', 'devices']),
    192             'adb devices(GetAttachedDevices): %s' % adb_online_devs]
    193   else:
    194     new_devs = set(adb_online_devs) - set(last_devices)
    195     if new_devs and os.path.exists(last_devices_path):
    196       bb_annotations.PrintWarning()
    197       bb_annotations.PrintSummaryText(
    198           '%d new devices detected' % len(new_devs))
    199       print ('New devices detected %s. And now back to your '
    200              'regularly scheduled program.' % list(new_devs))
    201 
    202 
    203 def SendEmail(from_address, to_addresses, cc_addresses, subject, msg):
    204   msg_body = '\r\n'.join(['From: %s' % from_address,
    205                           'To: %s' % ', '.join(to_addresses),
    206                           'CC: %s' % ', '.join(cc_addresses),
    207                           'Subject: %s' % subject, '', msg])
    208   try:
    209     server = smtplib.SMTP('localhost')
    210     server.sendmail(from_address, to_addresses, msg_body)
    211     server.quit()
    212   except Exception as e:
    213     print 'Failed to send alert email. Error: %s' % e
    214 
    215 
    216 def RestartUsb():
    217   if not os.path.isfile('/usr/bin/restart_usb'):
    218     print ('ERROR: Could not restart usb. /usr/bin/restart_usb not installed '
    219            'on host (see BUG=305769).')
    220     return False
    221 
    222   lsusb_proc = bb_utils.SpawnCmd(['lsusb'], stdout=subprocess.PIPE)
    223   lsusb_output, _ = lsusb_proc.communicate()
    224   if lsusb_proc.returncode:
    225     print ('Error: Could not get list of USB ports (i.e. lsusb).')
    226     return lsusb_proc.returncode
    227 
    228   usb_devices = [re.findall('Bus (\d\d\d) Device (\d\d\d)', lsusb_line)[0]
    229                  for lsusb_line in lsusb_output.strip().split('\n')]
    230 
    231   all_restarted = True
    232   # Walk USB devices from leaves up (i.e reverse sorted) restarting the
    233   # connection. If a parent node (e.g. usb hub) is restarted before the
    234   # devices connected to it, the (bus, dev) for the hub can change, making the
    235   # output we have wrong. This way we restart the devices before the hub.
    236   for (bus, dev) in reversed(sorted(usb_devices)):
    237     # Can not restart root usb connections
    238     if dev != '001':
    239       return_code = bb_utils.RunCmd(['/usr/bin/restart_usb', bus, dev])
    240       if return_code:
    241         print 'Error restarting USB device /dev/bus/usb/%s/%s' % (bus, dev)
    242         all_restarted = False
    243       else:
    244         print 'Restarted USB device /dev/bus/usb/%s/%s' % (bus, dev)
    245 
    246   return all_restarted
    247 
    248 
    249 def KillAllAdb():
    250   def GetAllAdb():
    251     for p in psutil.process_iter():
    252       try:
    253         if 'adb' in p.name:
    254           yield p
    255       except (psutil.error.NoSuchProcess, psutil.error.AccessDenied):
    256         pass
    257 
    258   for sig in [signal.SIGTERM, signal.SIGQUIT, signal.SIGKILL]:
    259     for p in GetAllAdb():
    260       try:
    261         print 'kill %d %d (%s [%s])' % (sig, p.pid, p.name,
    262             ' '.join(p.cmdline))
    263         p.send_signal(sig)
    264       except (psutil.error.NoSuchProcess, psutil.error.AccessDenied):
    265         pass
    266   for p in GetAllAdb():
    267     try:
    268       print 'Unable to kill %d (%s [%s])' % (p.pid, p.name, ' '.join(p.cmdline))
    269     except (psutil.error.NoSuchProcess, psutil.error.AccessDenied):
    270       pass
    271 
    272 
    273 def main():
    274   parser = optparse.OptionParser()
    275   parser.add_option('', '--out-dir',
    276                     help='Directory where the device path is stored',
    277                     default=os.path.join(constants.DIR_SOURCE_ROOT, 'out'))
    278   parser.add_option('--no-provisioning-check', action='store_true',
    279                     help='Will not check if devices are provisioned properly.')
    280   parser.add_option('--device-status-dashboard', action='store_true',
    281                     help='Output device status data for dashboard.')
    282   parser.add_option('--restart-usb', action='store_true',
    283                     help='Restart USB ports before running device check.')
    284   parser.add_option('--json-output',
    285                     help='Output JSON information into a specified file.')
    286 
    287   options, args = parser.parse_args()
    288   if args:
    289     parser.error('Unknown options %s' % args)
    290 
    291   # Remove the last build's "bad devices" before checking device statuses.
    292   device_blacklist.ResetBlacklist()
    293 
    294   try:
    295     expected_devices = device_list.GetPersistentDeviceList(
    296         os.path.join(options.out_dir, device_list.LAST_DEVICES_FILENAME))
    297   except IOError:
    298     expected_devices = []
    299   devices = android_commands.GetAttachedDevices()
    300   # Only restart usb if devices are missing.
    301   if set(expected_devices) != set(devices):
    302     print 'expected_devices: %s, devices: %s' % (expected_devices, devices)
    303     KillAllAdb()
    304     retries = 5
    305     usb_restarted = True
    306     if options.restart_usb:
    307       if not RestartUsb():
    308         usb_restarted = False
    309         bb_annotations.PrintWarning()
    310         print 'USB reset stage failed, wait for any device to come back.'
    311     while retries:
    312       print 'retry adb devices...'
    313       time.sleep(1)
    314       devices = android_commands.GetAttachedDevices()
    315       if set(expected_devices) == set(devices):
    316         # All devices are online, keep going.
    317         break
    318       if not usb_restarted and devices:
    319         # The USB wasn't restarted, but there's at least one device online.
    320         # No point in trying to wait for all devices.
    321         break
    322       retries -= 1
    323 
    324   # TODO(navabi): Test to make sure this fails and then fix call
    325   offline_devices = android_commands.GetAttachedDevices(
    326       hardware=False, emulator=False, offline=True)
    327 
    328   types, builds, batteries, reports, errors = [], [], [], [], []
    329   fail_step_lst = []
    330   if devices:
    331     types, builds, batteries, reports, errors, fail_step_lst = (
    332         zip(*[DeviceInfo(dev, options) for dev in devices]))
    333 
    334   err_msg = CheckForMissingDevices(options, devices) or []
    335 
    336   unique_types = list(set(types))
    337   unique_builds = list(set(builds))
    338 
    339   bb_annotations.PrintMsg('Online devices: %d. Device types %s, builds %s'
    340                            % (len(devices), unique_types, unique_builds))
    341   print '\n'.join(reports)
    342 
    343   for serial, dev_errors in zip(devices, errors):
    344     if dev_errors:
    345       err_msg += ['%s errors:' % serial]
    346       err_msg += ['    %s' % error for error in dev_errors]
    347 
    348   if err_msg:
    349     bb_annotations.PrintWarning()
    350     msg = '\n'.join(err_msg)
    351     print msg
    352     from_address = 'buildbot (at] chromium.org'
    353     to_addresses = ['chromium-android-device-alerts (at] google.com']
    354     bot_name = os.environ.get('BUILDBOT_BUILDERNAME')
    355     slave_name = os.environ.get('BUILDBOT_SLAVENAME')
    356     subject = 'Device status check errors on %s, %s.' % (slave_name, bot_name)
    357     SendEmail(from_address, to_addresses, [], subject, msg)
    358 
    359   if options.device_status_dashboard:
    360     perf_tests_results_helper.PrintPerfResult('BotDevices', 'OnlineDevices',
    361                                               [len(devices)], 'devices')
    362     perf_tests_results_helper.PrintPerfResult('BotDevices', 'OfflineDevices',
    363                                               [len(offline_devices)], 'devices',
    364                                               'unimportant')
    365     for serial, battery in zip(devices, batteries):
    366       perf_tests_results_helper.PrintPerfResult('DeviceBattery', serial,
    367                                                 [battery], '%',
    368                                                 'unimportant')
    369 
    370   if options.json_output:
    371     with open(options.json_output, 'wb') as f:
    372       f.write(json.dumps({
    373         'online_devices': devices,
    374         'offline_devices': offline_devices,
    375         'expected_devices': expected_devices,
    376         'unique_types': unique_types,
    377         'unique_builds': unique_builds,
    378       }))
    379 
    380   if False in fail_step_lst:
    381     # TODO(navabi): Build fails on device status check step if there exists any
    382     # devices with critically low battery. Remove those devices from testing,
    383     # allowing build to continue with good devices.
    384     return 2
    385 
    386   if not devices:
    387     return 1
    388 
    389 
    390 if __name__ == '__main__':
    391   sys.exit(main())
    392