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