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