device_status.py revision 7332cdb42368a904cbf7418de329868989e592da
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.build_fingerprint
125          build_description = device.build_description
126          wifi_ip = device.GetProp('dhcp.wlan0.ipaddress')
127          battery_info = _BatteryStatus(device, blacklist)
128          imei_slice = _IMEISlice(device)
129
130          if (device.product_name == 'mantaray' and
131              battery_info.get('AC powered', None) != 'true'):
132            logging.error('Mantaray device not connected to AC power.')
133
134          device_status.update({
135            'ro.build.product': build_product,
136            'ro.build.id': build_id,
137            'ro.build.fingerprint': build_fingerprint,
138            'ro.build.description': build_description,
139            'battery': battery_info,
140            'imei_slice': imei_slice,
141            'wifi_ip': wifi_ip,
142          })
143
144        except device_errors.CommandFailedError:
145          logging.exception('Failure while getting device status for %s.',
146                            str(device))
147          if blacklist:
148            blacklist.Extend([serial], reason='status_check_failure')
149
150        except device_errors.CommandTimeoutError:
151          logging.exception('Timeout while getting device status for %s.',
152                            str(device))
153          if blacklist:
154            blacklist.Extend([serial], reason='status_check_timeout')
155
156      elif blacklist:
157        blacklist.Extend([serial],
158                         reason=adb_status if usb_status else 'offline')
159
160    device_status['blacklisted'] = IsBlacklisted(serial, blacklist)
161
162    return device_status
163
164  parallel_devices = device_utils.DeviceUtils.parallel(devices)
165  statuses = parallel_devices.pMap(blacklisting_device_status).pGet(None)
166  return statuses
167
168
169def _LogStatuses(statuses):
170  # Log the state of all devices.
171  for status in statuses:
172    logging.info(status['serial'])
173    adb_status = status.get('adb_status')
174    blacklisted = status.get('blacklisted')
175    logging.info('  USB status: %s',
176                 'online' if status.get('usb_status') else 'offline')
177    logging.info('  ADB status: %s', adb_status)
178    logging.info('  Blacklisted: %s', str(blacklisted))
179    if adb_status == 'device' and not blacklisted:
180      logging.info('  Device type: %s', status.get('ro.build.product'))
181      logging.info('  OS build: %s', status.get('ro.build.id'))
182      logging.info('  OS build fingerprint: %s',
183                   status.get('ro.build.fingerprint'))
184      logging.info('  Battery state:')
185      for k, v in status.get('battery', {}).iteritems():
186        logging.info('    %s: %s', k, v)
187      logging.info('  IMEI slice: %s', status.get('imei_slice'))
188      logging.info('  WiFi IP: %s', status.get('wifi_ip'))
189
190
191def _WriteBuildbotFile(file_path, statuses):
192  buildbot_path, _ = os.path.split(file_path)
193  if os.path.exists(buildbot_path):
194    with open(file_path, 'w') as f:
195      for status in statuses:
196        try:
197          if status['adb_status'] == 'device':
198            f.write('{serial} {adb_status} {build_product} {build_id} '
199                    '{temperature:.1f}C {level}%\n'.format(
200                serial=status['serial'],
201                adb_status=status['adb_status'],
202                build_product=status['type'],
203                build_id=status['build'],
204                temperature=float(status['battery']['temperature']) / 10,
205                level=status['battery']['level']
206            ))
207          elif status.get('usb_status', False):
208            f.write('{serial} {adb_status}\n'.format(
209                serial=status['serial'],
210                adb_status=status['adb_status']
211            ))
212          else:
213            f.write('{serial} offline\n'.format(
214                serial=status['serial']
215            ))
216        except Exception: # pylint: disable=broad-except
217          pass
218
219
220def GetExpectedDevices(known_devices_files):
221  expected_devices = set()
222  try:
223    for path in known_devices_files:
224      if os.path.exists(path):
225        expected_devices.update(device_list.GetPersistentDeviceList(path))
226      else:
227        logging.warning('Could not find known devices file: %s', path)
228  except IOError:
229    logging.warning('Problem reading %s, skipping.', path)
230
231  logging.info('Expected devices:')
232  for device in expected_devices:
233    logging.info('  %s', device)
234  return expected_devices
235
236
237def AddArguments(parser):
238  parser.add_argument('--json-output',
239                      help='Output JSON information into a specified file.')
240  parser.add_argument('--adb-path',
241                      help='Absolute path to the adb binary to use.')
242  parser.add_argument('--blacklist-file', help='Device blacklist JSON file.')
243  parser.add_argument('--known-devices-file', action='append', default=[],
244                      dest='known_devices_files',
245                      help='Path to known device lists.')
246  parser.add_argument('--buildbot-path', '-b',
247                      default='/home/chrome-bot/.adb_device_info',
248                      help='Absolute path to buildbot file location')
249  parser.add_argument('-v', '--verbose', action='count', default=1,
250                      help='Log more information.')
251  parser.add_argument('-w', '--overwrite-known-devices-files',
252                      action='store_true',
253                      help='If set, overwrites known devices files wiht new '
254                           'values.')
255
256def main():
257  parser = argparse.ArgumentParser()
258  AddArguments(parser)
259  args = parser.parse_args()
260
261  run_tests_helper.SetLogLevel(args.verbose)
262
263  devil_dynamic_config = devil_env.EmptyConfig()
264
265  if args.adb_path:
266    devil_dynamic_config['dependencies'].update(
267        devil_env.LocalConfigItem(
268            'adb', devil_env.GetPlatform(), args.adb_path))
269  devil_env.config.Initialize(configs=[devil_dynamic_config])
270
271  blacklist = (device_blacklist.Blacklist(args.blacklist_file)
272               if args.blacklist_file
273               else None)
274
275  expected_devices = GetExpectedDevices(args.known_devices_files)
276  usb_devices = set(lsusb.get_android_devices())
277  devices = [device_utils.DeviceUtils(s)
278             for s in expected_devices.union(usb_devices)]
279
280  statuses = DeviceStatus(devices, blacklist)
281
282  # Log the state of all devices.
283  _LogStatuses(statuses)
284
285  # Update the last devices file(s).
286  if args.overwrite_known_devices_files:
287    for path in args.known_devices_files:
288      device_list.WritePersistentDeviceList(
289          path, [status['serial'] for status in statuses])
290
291  # Write device info to file for buildbot info display.
292  _WriteBuildbotFile(args.buildbot_path, statuses)
293
294  # Dump the device statuses to JSON.
295  if args.json_output:
296    with open(args.json_output, 'wb') as f:
297      f.write(json.dumps(
298          statuses, indent=4, sort_keys=True, separators=(',', ': ')))
299
300  live_devices = [status['serial'] for status in statuses
301                  if (status['adb_status'] == 'device'
302                      and not IsBlacklisted(status['serial'], blacklist))]
303
304  # If all devices failed, or if there are no devices, it's an infra error.
305  if not live_devices:
306    logging.error('No available devices.')
307  return 0 if live_devices else exit_codes.INFRA
308
309
310if __name__ == '__main__':
311  sys.exit(main())
312