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