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