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