1#!/usr/bin/env python
2# Copyright 2016 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""A script to keep track of devices across builds and report state."""
7
8import argparse
9import json
10import logging
11import os
12import re
13import sys
14
15if __name__ == '__main__':
16  sys.path.append(
17      os.path.abspath(os.path.join(os.path.dirname(__file__),
18                                   '..', '..', '..')))
19from devil.android import battery_utils
20from devil.android import device_blacklist
21from devil.android import device_errors
22from devil.android import device_list
23from devil.android import device_utils
24from devil.android.sdk import adb_wrapper
25from devil.android.tools import script_common
26from devil.constants import exit_codes
27from devil.utils import logging_common
28from devil.utils import lsusb
29
30logger = logging.getLogger(__name__)
31
32_RE_DEVICE_ID = re.compile(r'Device ID = (\d+)')
33
34
35def IsBlacklisted(serial, blacklist):
36  return blacklist and serial in blacklist.Read()
37
38
39def _BatteryStatus(device, blacklist):
40  battery_info = {}
41  try:
42    battery = battery_utils.BatteryUtils(device)
43    battery_info = battery.GetBatteryInfo(timeout=5)
44    battery_level = int(battery_info.get('level', 100))
45
46    if battery_level < 15:
47      logger.error('Critically low battery level (%d)', battery_level)
48      battery = battery_utils.BatteryUtils(device)
49      if not battery.GetCharging():
50        battery.SetCharging(True)
51      if blacklist:
52        blacklist.Extend([device.adb.GetDeviceSerial()], reason='low_battery')
53
54  except (device_errors.CommandFailedError,
55          device_errors.DeviceUnreachableError):
56    logger.exception('Failed to get battery information for %s',
57                     str(device))
58
59  return battery_info
60
61
62def DeviceStatus(devices, blacklist):
63  """Generates status information for the given devices.
64
65  Args:
66    devices: The devices to generate status for.
67    blacklist: The current device blacklist.
68  Returns:
69    A dict of the following form:
70    {
71      '<serial>': {
72        'serial': '<serial>',
73        'adb_status': str,
74        'usb_status': bool,
75        'blacklisted': bool,
76        # only if the device is connected and not blacklisted
77        'type': ro.build.product,
78        'build': ro.build.id,
79        'build_detail': ro.build.fingerprint,
80        'battery': {
81          ...
82        },
83        'imei_slice': str,
84        'wifi_ip': str,
85      },
86      ...
87    }
88  """
89  adb_devices = {
90    a[0].GetDeviceSerial(): a
91    for a in adb_wrapper.AdbWrapper.Devices(desired_state=None, long_list=True)
92  }
93  usb_devices = set(lsusb.get_android_devices())
94
95  def blacklisting_device_status(device):
96    serial = device.adb.GetDeviceSerial()
97    adb_status = (
98        adb_devices[serial][1] if serial in adb_devices
99        else 'missing')
100    usb_status = bool(serial in usb_devices)
101
102    device_status = {
103      'serial': serial,
104      'adb_status': adb_status,
105      'usb_status': usb_status,
106    }
107
108    if not IsBlacklisted(serial, blacklist):
109      if adb_status == 'device':
110        try:
111          build_product = device.build_product
112          build_id = device.build_id
113          build_fingerprint = device.build_fingerprint
114          build_description = device.build_description
115          wifi_ip = device.GetProp('dhcp.wlan0.ipaddress')
116          battery_info = _BatteryStatus(device, blacklist)
117          try:
118            imei_slice = device.GetIMEI()
119          except device_errors.CommandFailedError:
120            logging.exception('Unable to fetch IMEI for %s.', str(device))
121            imei_slice = 'unknown'
122
123          if (device.product_name == 'mantaray' and
124              battery_info.get('AC powered', None) != 'true'):
125            logger.error('Mantaray device not connected to AC power.')
126
127          device_status.update({
128            'ro.build.product': build_product,
129            'ro.build.id': build_id,
130            'ro.build.fingerprint': build_fingerprint,
131            'ro.build.description': build_description,
132            'battery': battery_info,
133            'imei_slice': imei_slice,
134            'wifi_ip': wifi_ip,
135          })
136
137        except (device_errors.CommandFailedError,
138                device_errors.DeviceUnreachableError):
139          logger.exception('Failure while getting device status for %s.',
140                           str(device))
141          if blacklist:
142            blacklist.Extend([serial], reason='status_check_failure')
143
144        except device_errors.CommandTimeoutError:
145          logger.exception('Timeout while getting device status for %s.',
146                           str(device))
147          if blacklist:
148            blacklist.Extend([serial], reason='status_check_timeout')
149
150      elif blacklist:
151        blacklist.Extend([serial],
152                         reason=adb_status if usb_status else 'offline')
153
154    device_status['blacklisted'] = IsBlacklisted(serial, blacklist)
155
156    return device_status
157
158  parallel_devices = device_utils.DeviceUtils.parallel(devices)
159  statuses = parallel_devices.pMap(blacklisting_device_status).pGet(None)
160  return statuses
161
162
163def _LogStatuses(statuses):
164  # Log the state of all devices.
165  for status in statuses:
166    logger.info(status['serial'])
167    adb_status = status.get('adb_status')
168    blacklisted = status.get('blacklisted')
169    logger.info('  USB status: %s',
170                'online' if status.get('usb_status') else 'offline')
171    logger.info('  ADB status: %s', adb_status)
172    logger.info('  Blacklisted: %s', str(blacklisted))
173    if adb_status == 'device' and not blacklisted:
174      logger.info('  Device type: %s', status.get('ro.build.product'))
175      logger.info('  OS build: %s', status.get('ro.build.id'))
176      logger.info('  OS build fingerprint: %s',
177                  status.get('ro.build.fingerprint'))
178      logger.info('  Battery state:')
179      for k, v in status.get('battery', {}).iteritems():
180        logger.info('    %s: %s', k, v)
181      logger.info('  IMEI slice: %s', status.get('imei_slice'))
182      logger.info('  WiFi IP: %s', status.get('wifi_ip'))
183
184
185def _WriteBuildbotFile(file_path, statuses):
186  buildbot_path, _ = os.path.split(file_path)
187  if os.path.exists(buildbot_path):
188    with open(file_path, 'w') as f:
189      for status in statuses:
190        try:
191          if status['adb_status'] == 'device':
192            f.write('{serial} {adb_status} {build_product} {build_id} '
193                    '{temperature:.1f}C {level}%\n'.format(
194                serial=status['serial'],
195                adb_status=status['adb_status'],
196                build_product=status['type'],
197                build_id=status['build'],
198                temperature=float(status['battery']['temperature']) / 10,
199                level=status['battery']['level']
200            ))
201          elif status.get('usb_status', False):
202            f.write('{serial} {adb_status}\n'.format(
203                serial=status['serial'],
204                adb_status=status['adb_status']
205            ))
206          else:
207            f.write('{serial} offline\n'.format(
208                serial=status['serial']
209            ))
210        except Exception: # pylint: disable=broad-except
211          pass
212
213
214def GetExpectedDevices(known_devices_files):
215  expected_devices = set()
216  try:
217    for path in known_devices_files:
218      if os.path.exists(path):
219        expected_devices.update(device_list.GetPersistentDeviceList(path))
220      else:
221        logger.warning('Could not find known devices file: %s', path)
222  except IOError:
223    logger.warning('Problem reading %s, skipping.', path)
224
225  logger.info('Expected devices:')
226  for device in expected_devices:
227    logger.info('  %s', device)
228  return expected_devices
229
230
231def AddArguments(parser):
232  parser.add_argument('--json-output',
233                      help='Output JSON information into a specified file.')
234  parser.add_argument('--blacklist-file', help='Device blacklist JSON file.')
235  parser.add_argument('--known-devices-file', action='append', default=[],
236                      dest='known_devices_files',
237                      help='Path to known device lists.')
238  parser.add_argument('--buildbot-path', '-b',
239                      default='/home/chrome-bot/.adb_device_info',
240                      help='Absolute path to buildbot file location')
241  parser.add_argument('-w', '--overwrite-known-devices-files',
242                      action='store_true',
243                      help='If set, overwrites known devices files wiht new '
244                           'values.')
245
246def main():
247  parser = argparse.ArgumentParser()
248  logging_common.AddLoggingArguments(parser)
249  script_common.AddEnvironmentArguments(parser)
250  AddArguments(parser)
251  args = parser.parse_args()
252
253  logging_common.InitializeLogging(args)
254  script_common.InitializeEnvironment(args)
255
256  blacklist = (device_blacklist.Blacklist(args.blacklist_file)
257               if args.blacklist_file
258               else None)
259
260  expected_devices = GetExpectedDevices(args.known_devices_files)
261  usb_devices = set(lsusb.get_android_devices())
262  devices = [device_utils.DeviceUtils(s)
263             for s in expected_devices.union(usb_devices)]
264
265  statuses = DeviceStatus(devices, blacklist)
266
267  # Log the state of all devices.
268  _LogStatuses(statuses)
269
270  # Update the last devices file(s).
271  if args.overwrite_known_devices_files:
272    for path in args.known_devices_files:
273      device_list.WritePersistentDeviceList(
274          path, [status['serial'] for status in statuses])
275
276  # Write device info to file for buildbot info display.
277  _WriteBuildbotFile(args.buildbot_path, statuses)
278
279  # Dump the device statuses to JSON.
280  if args.json_output:
281    with open(args.json_output, 'wb') as f:
282      f.write(json.dumps(
283          statuses, indent=4, sort_keys=True, separators=(',', ': ')))
284
285  live_devices = [status['serial'] for status in statuses
286                  if (status['adb_status'] == 'device'
287                      and not IsBlacklisted(status['serial'], blacklist))]
288
289  # If all devices failed, or if there are no devices, it's an infra error.
290  if not live_devices:
291    logger.error('No available devices.')
292  return 0 if live_devices else exit_codes.INFRA
293
294
295if __name__ == '__main__':
296  sys.exit(main())
297