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