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.
7 """A class to keep track of devices across builds and report state."""
23 sys.path.append(os.path.join(os.path.dirname(__file__),
24 os.pardir, os.pardir, 'util', 'lib',
26 import perf_tests_results_helper # pylint: disable=F0401
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
34 def DeviceInfo(serial, options):
35 """Gathers info on a device via various adb calls.
38 serial: The serial of the attached device to construct info about.
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.
45 device_adb = android_commands.AndroidCommands(serial)
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()
54 battery = device_adb.GetBatteryInfo()
55 except Exception as e:
57 logging.error('Unable to obtain battery info for %s, %s', serial, e)
59 def _GetData(re_expression, line, lambda_function=lambda x:x):
62 found = re.findall(re_expression, line)
63 if found and len(found):
64 return lambda_function(found[0])
67 if options.device_status_dashboard:
68 # Dashboard does not track install speed. Do not unnecessarily install.
69 install_speed = 'Unknown'
71 install_output = GetCmdOutput(
72 ['%s/build/android/adb_install_apk.py' % constants.DIR_SOURCE_ROOT,
74 '%s/build/android/CheckInstallApk-debug.apk' % constants.DIR_SOURCE_ROOT
76 install_speed = _GetData('(\d+) KB/s', install_output)
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(),
85 report = ['Device %s (%s)' % (serial, device_type),
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,
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.']
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
119 def GetLastDevices(out_dir):
120 """Returns a list of devices that have been seen on the bot.
123 options: out_dir parameter of options argument is used as the base
124 directory to load and update the cache file.
126 Returns: List of device serial numbers that were on the bot.
128 devices_path = os.path.join(out_dir, '.last_devices')
131 with open(devices_path) as f:
132 devices = f.read().splitlines()
134 # Ignore error, file might not exist
139 def CheckForMissingDevices(options, adb_online_devs):
140 """Uses file of previous online devices to detect broken phones.
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.
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)
153 out_dir = os.path.abspath(options.out_dir)
155 def WriteDeviceList(file_name, device_list):
156 path = os.path.join(out_dir, file_name)
157 if not os.path.exists(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)))
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))
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)
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?']
176 devices_missing_msg = '%d devices not detected.' % len(missing_devs)
177 bb_annotations.PrintSummaryText(devices_missing_msg)
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'
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,
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()]
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))
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])
217 server = smtplib.SMTP('localhost')
218 server.sendmail(from_address, [to_address], msg_body)
220 except Exception as e:
221 print 'Failed to send alert email. Error: %s' % e
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).')
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
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')]
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
247 return_code = bb_utils.RunCmd(['/usr/bin/restart_usb', bus, dev])
249 print 'Error restarting USB device /dev/bus/usb/%s/%s' % (bus, dev)
250 all_restarted = False
252 print 'Restarted USB device /dev/bus/usb/%s/%s' % (bus, dev)
259 for p in psutil.process_iter():
263 except psutil.error.NoSuchProcess:
266 for sig in [signal.SIGTERM, signal.SIGQUIT, signal.SIGKILL]:
267 for p in GetAllAdb():
269 print 'kill %d %d (%s [%s])' % (sig, p.pid, p.name,
272 except psutil.error.NoSuchProcess:
274 for p in GetAllAdb():
276 print 'Unable to kill %d (%s [%s])' % (p.pid, p.name, ' '.join(p.cmdline))
277 except psutil.error.NoSuchProcess:
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()
294 parser.error('Unknown options %s' % args)
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):
305 usb_restarted = False
306 bb_annotations.PrintWarning()
307 print 'USB reset stage failed, wait for any device to come back.'
310 devices = android_commands.GetAttachedDevices()
311 if set(expected_devices) == set(devices):
312 # All devices are online, keep going.
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.
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,
326 types, builds, batteries, reports, errors = [], [], [], [], []
329 types, builds, batteries, reports, errors, fail_step_lst = (
330 zip(*[DeviceInfo(dev, options) for dev in devices]))
332 err_msg = CheckForMissingDevices(options, devices) or []
334 unique_types = list(set(types))
335 unique_builds = list(set(builds))
337 bb_annotations.PrintMsg('Online devices: %d. Device types %s, builds %s'
338 % (len(devices), unique_types, unique_builds))
339 print '\n'.join(reports)
341 for serial, dev_errors in zip(devices, errors):
343 err_msg += ['%s errors:' % serial]
344 err_msg += [' %s' % error for error in dev_errors]
347 bb_annotations.PrintWarning()
348 msg = '\n'.join(err_msg)
350 SendDeviceStatusAlert(msg)
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',
358 for serial, battery in zip(devices, batteries):
359 perf_tests_results_helper.PrintPerfResult('DeviceBattery', serial,
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.
373 if __name__ == '__main__':