OLD | NEW |
| (Empty) |
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 | |
9 import argparse | |
10 import json | |
11 import logging | |
12 import os | |
13 import psutil | |
14 import re | |
15 import signal | |
16 import sys | |
17 | |
18 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) | |
19 import devil_chromium | |
20 from devil.android import battery_utils | |
21 from devil.android import device_blacklist | |
22 from devil.android import device_errors | |
23 from devil.android import device_list | |
24 from devil.android import device_utils | |
25 from devil.android.sdk import adb_wrapper | |
26 from devil.constants import exit_codes | |
27 from devil.utils import lsusb | |
28 from devil.utils import reset_usb | |
29 from devil.utils import run_tests_helper | |
30 from pylib.constants import host_paths | |
31 | |
32 _RE_DEVICE_ID = re.compile(r'Device ID = (\d+)') | |
33 | |
34 | |
35 def KillAllAdb(): | |
36 def GetAllAdb(): | |
37 for p in psutil.process_iter(): | |
38 try: | |
39 if 'adb' in p.name: | |
40 yield p | |
41 except (psutil.NoSuchProcess, psutil.AccessDenied): | |
42 pass | |
43 | |
44 for sig in [signal.SIGTERM, signal.SIGQUIT, signal.SIGKILL]: | |
45 for p in GetAllAdb(): | |
46 try: | |
47 logging.info('kill %d %d (%s [%s])', sig, p.pid, p.name, | |
48 ' '.join(p.cmdline)) | |
49 p.send_signal(sig) | |
50 except (psutil.NoSuchProcess, psutil.AccessDenied): | |
51 pass | |
52 for p in GetAllAdb(): | |
53 try: | |
54 logging.error('Unable to kill %d (%s [%s])', p.pid, p.name, | |
55 ' '.join(p.cmdline)) | |
56 except (psutil.NoSuchProcess, psutil.AccessDenied): | |
57 pass | |
58 | |
59 | |
60 def _IsBlacklisted(serial, blacklist): | |
61 return blacklist and serial in blacklist.Read() | |
62 | |
63 | |
64 def _BatteryStatus(device, blacklist): | |
65 battery_info = {} | |
66 try: | |
67 battery = battery_utils.BatteryUtils(device) | |
68 battery_info = battery.GetBatteryInfo(timeout=5) | |
69 battery_level = int(battery_info.get('level', 100)) | |
70 | |
71 if battery_level < 15: | |
72 logging.error('Critically low battery level (%d)', battery_level) | |
73 battery = battery_utils.BatteryUtils(device) | |
74 if not battery.GetCharging(): | |
75 battery.SetCharging(True) | |
76 if blacklist: | |
77 blacklist.Extend([device.adb.GetDeviceSerial()], reason='low_battery') | |
78 | |
79 except device_errors.CommandFailedError: | |
80 logging.exception('Failed to get battery information for %s', | |
81 str(device)) | |
82 | |
83 return battery_info | |
84 | |
85 | |
86 def _IMEISlice(device): | |
87 imei_slice = '' | |
88 try: | |
89 for l in device.RunShellCommand(['dumpsys', 'iphonesubinfo'], | |
90 check_return=True, timeout=5): | |
91 m = _RE_DEVICE_ID.match(l) | |
92 if m: | |
93 imei_slice = m.group(1)[-6:] | |
94 except device_errors.CommandFailedError: | |
95 logging.exception('Failed to get IMEI slice for %s', str(device)) | |
96 | |
97 return imei_slice | |
98 | |
99 | |
100 def DeviceStatus(devices, blacklist): | |
101 """Generates status information for the given devices. | |
102 | |
103 Args: | |
104 devices: The devices to generate status for. | |
105 blacklist: The current device blacklist. | |
106 Returns: | |
107 A dict of the following form: | |
108 { | |
109 '<serial>': { | |
110 'serial': '<serial>', | |
111 'adb_status': str, | |
112 'usb_status': bool, | |
113 'blacklisted': bool, | |
114 # only if the device is connected and not blacklisted | |
115 'type': ro.build.product, | |
116 'build': ro.build.id, | |
117 'build_detail': ro.build.fingerprint, | |
118 'battery': { | |
119 ... | |
120 }, | |
121 'imei_slice': str, | |
122 'wifi_ip': str, | |
123 }, | |
124 ... | |
125 } | |
126 """ | |
127 adb_devices = { | |
128 a[0].GetDeviceSerial(): a | |
129 for a in adb_wrapper.AdbWrapper.Devices(desired_state=None, long_list=True) | |
130 } | |
131 usb_devices = set(lsusb.get_android_devices()) | |
132 | |
133 def blacklisting_device_status(device): | |
134 serial = device.adb.GetDeviceSerial() | |
135 adb_status = ( | |
136 adb_devices[serial][1] if serial in adb_devices | |
137 else 'missing') | |
138 usb_status = bool(serial in usb_devices) | |
139 | |
140 device_status = { | |
141 'serial': serial, | |
142 'adb_status': adb_status, | |
143 'usb_status': usb_status, | |
144 } | |
145 | |
146 if not _IsBlacklisted(serial, blacklist): | |
147 if adb_status == 'device': | |
148 try: | |
149 build_product = device.build_product | |
150 build_id = device.build_id | |
151 build_fingerprint = device.GetProp('ro.build.fingerprint', cache=True) | |
152 wifi_ip = device.GetProp('dhcp.wlan0.ipaddress') | |
153 battery_info = _BatteryStatus(device, blacklist) | |
154 imei_slice = _IMEISlice(device) | |
155 | |
156 if (device.product_name == 'mantaray' and | |
157 battery_info.get('AC powered', None) != 'true'): | |
158 logging.error('Mantaray device not connected to AC power.') | |
159 | |
160 device_status.update({ | |
161 'ro.build.product': build_product, | |
162 'ro.build.id': build_id, | |
163 'ro.build.fingerprint': build_fingerprint, | |
164 'battery': battery_info, | |
165 'imei_slice': imei_slice, | |
166 'wifi_ip': wifi_ip, | |
167 | |
168 # TODO(jbudorick): Remove these once no clients depend on them. | |
169 'type': build_product, | |
170 'build': build_id, | |
171 'build_detail': build_fingerprint, | |
172 }) | |
173 | |
174 except device_errors.CommandFailedError: | |
175 logging.exception('Failure while getting device status for %s.', | |
176 str(device)) | |
177 if blacklist: | |
178 blacklist.Extend([serial], reason='status_check_failure') | |
179 | |
180 except device_errors.CommandTimeoutError: | |
181 logging.exception('Timeout while getting device status for %s.', | |
182 str(device)) | |
183 if blacklist: | |
184 blacklist.Extend([serial], reason='status_check_timeout') | |
185 | |
186 elif blacklist: | |
187 blacklist.Extend([serial], | |
188 reason=adb_status if usb_status else 'offline') | |
189 | |
190 device_status['blacklisted'] = _IsBlacklisted(serial, blacklist) | |
191 | |
192 return device_status | |
193 | |
194 parallel_devices = device_utils.DeviceUtils.parallel(devices) | |
195 statuses = parallel_devices.pMap(blacklisting_device_status).pGet(None) | |
196 return statuses | |
197 | |
198 | |
199 def RecoverDevices(devices, blacklist): | |
200 """Attempts to recover any inoperable devices in the provided list. | |
201 | |
202 Args: | |
203 devices: The list of devices to attempt to recover. | |
204 blacklist: The current device blacklist, which will be used then | |
205 reset. | |
206 Returns: | |
207 Nothing. | |
208 """ | |
209 | |
210 statuses = DeviceStatus(devices, blacklist) | |
211 | |
212 should_restart_usb = set( | |
213 status['serial'] for status in statuses | |
214 if (not status['usb_status'] | |
215 or status['adb_status'] in ('offline', 'missing'))) | |
216 should_restart_adb = should_restart_usb.union(set( | |
217 status['serial'] for status in statuses | |
218 if status['adb_status'] == 'unauthorized')) | |
219 should_reboot_device = should_restart_adb.union(set( | |
220 status['serial'] for status in statuses | |
221 if status['blacklisted'])) | |
222 | |
223 logging.debug('Should restart USB for:') | |
224 for d in should_restart_usb: | |
225 logging.debug(' %s', d) | |
226 logging.debug('Should restart ADB for:') | |
227 for d in should_restart_adb: | |
228 logging.debug(' %s', d) | |
229 logging.debug('Should reboot:') | |
230 for d in should_reboot_device: | |
231 logging.debug(' %s', d) | |
232 | |
233 if blacklist: | |
234 blacklist.Reset() | |
235 | |
236 if should_restart_adb: | |
237 KillAllAdb() | |
238 for serial in should_restart_usb: | |
239 try: | |
240 reset_usb.reset_android_usb(serial) | |
241 except IOError: | |
242 logging.exception('Unable to reset USB for %s.', serial) | |
243 if blacklist: | |
244 blacklist.Extend([serial], reason='usb_failure') | |
245 except device_errors.DeviceUnreachableError: | |
246 logging.exception('Unable to reset USB for %s.', serial) | |
247 if blacklist: | |
248 blacklist.Extend([serial], reason='offline') | |
249 | |
250 def blacklisting_recovery(device): | |
251 if _IsBlacklisted(device.adb.GetDeviceSerial(), blacklist): | |
252 logging.debug('%s is blacklisted, skipping recovery.', str(device)) | |
253 return | |
254 | |
255 if str(device) in should_reboot_device: | |
256 try: | |
257 device.WaitUntilFullyBooted(retries=0) | |
258 return | |
259 except (device_errors.CommandTimeoutError, | |
260 device_errors.CommandFailedError): | |
261 logging.exception('Failure while waiting for %s. ' | |
262 'Attempting to recover.', str(device)) | |
263 | |
264 try: | |
265 try: | |
266 device.Reboot(block=False, timeout=5, retries=0) | |
267 except device_errors.CommandTimeoutError: | |
268 logging.warning('Timed out while attempting to reboot %s normally.' | |
269 'Attempting alternative reboot.', str(device)) | |
270 # The device drops offline before we can grab the exit code, so | |
271 # we don't check for status. | |
272 device.adb.Root() | |
273 device.adb.Shell('echo b > /proc/sysrq-trigger', expect_status=None, | |
274 timeout=5, retries=0) | |
275 except device_errors.CommandFailedError: | |
276 logging.exception('Failed to reboot %s.', str(device)) | |
277 if blacklist: | |
278 blacklist.Extend([device.adb.GetDeviceSerial()], | |
279 reason='reboot_failure') | |
280 except device_errors.CommandTimeoutError: | |
281 logging.exception('Timed out while rebooting %s.', str(device)) | |
282 if blacklist: | |
283 blacklist.Extend([device.adb.GetDeviceSerial()], | |
284 reason='reboot_timeout') | |
285 | |
286 try: | |
287 device.WaitUntilFullyBooted(retries=0) | |
288 except device_errors.CommandFailedError: | |
289 logging.exception('Failure while waiting for %s.', str(device)) | |
290 if blacklist: | |
291 blacklist.Extend([device.adb.GetDeviceSerial()], | |
292 reason='reboot_failure') | |
293 except device_errors.CommandTimeoutError: | |
294 logging.exception('Timed out while waiting for %s.', str(device)) | |
295 if blacklist: | |
296 blacklist.Extend([device.adb.GetDeviceSerial()], | |
297 reason='reboot_timeout') | |
298 | |
299 device_utils.DeviceUtils.parallel(devices).pMap(blacklisting_recovery) | |
300 | |
301 | |
302 def main(): | |
303 parser = argparse.ArgumentParser() | |
304 parser.add_argument('--out-dir', | |
305 help='Directory where the device path is stored', | |
306 default=os.path.join(host_paths.DIR_SOURCE_ROOT, 'out')) | |
307 parser.add_argument('--restart-usb', action='store_true', | |
308 help='DEPRECATED. ' | |
309 'This script now always tries to reset USB.') | |
310 parser.add_argument('--json-output', | |
311 help='Output JSON information into a specified file.') | |
312 parser.add_argument('--adb-path', type=os.path.abspath, | |
313 help='Absolute path to the adb binary to use.') | |
314 parser.add_argument('--blacklist-file', help='Device blacklist JSON file.') | |
315 parser.add_argument('--known-devices-file', action='append', default=[], | |
316 dest='known_devices_files', | |
317 help='Path to known device lists.') | |
318 parser.add_argument('-v', '--verbose', action='count', default=1, | |
319 help='Log more information.') | |
320 | |
321 args = parser.parse_args() | |
322 | |
323 run_tests_helper.SetLogLevel(args.verbose) | |
324 | |
325 devil_chromium.Initialize(adb_path=args.adb_path) | |
326 | |
327 blacklist = (device_blacklist.Blacklist(args.blacklist_file) | |
328 if args.blacklist_file | |
329 else None) | |
330 | |
331 expected_devices = set() | |
332 try: | |
333 for path in args.known_devices_files: | |
334 if os.path.exists(path): | |
335 expected_devices.update(device_list.GetPersistentDeviceList(path)) | |
336 except IOError: | |
337 logging.warning('Problem reading %s, skipping.', path) | |
338 | |
339 logging.info('Expected devices:') | |
340 for device in expected_devices: | |
341 logging.info(' %s', device) | |
342 | |
343 usb_devices = set(lsusb.get_android_devices()) | |
344 devices = [device_utils.DeviceUtils(s) | |
345 for s in expected_devices.union(usb_devices)] | |
346 | |
347 RecoverDevices(devices, blacklist) | |
348 statuses = DeviceStatus(devices, blacklist) | |
349 | |
350 # Log the state of all devices. | |
351 for status in statuses: | |
352 logging.info(status['serial']) | |
353 adb_status = status.get('adb_status') | |
354 blacklisted = status.get('blacklisted') | |
355 logging.info(' USB status: %s', | |
356 'online' if status.get('usb_status') else 'offline') | |
357 logging.info(' ADB status: %s', adb_status) | |
358 logging.info(' Blacklisted: %s', str(blacklisted)) | |
359 if adb_status == 'device' and not blacklisted: | |
360 logging.info(' Device type: %s', status.get('ro.build.product')) | |
361 logging.info(' OS build: %s', status.get('ro.build.id')) | |
362 logging.info(' OS build fingerprint: %s', | |
363 status.get('ro.build.fingerprint')) | |
364 logging.info(' Battery state:') | |
365 for k, v in status.get('battery', {}).iteritems(): | |
366 logging.info(' %s: %s', k, v) | |
367 logging.info(' IMEI slice: %s', status.get('imei_slice')) | |
368 logging.info(' WiFi IP: %s', status.get('wifi_ip')) | |
369 | |
370 # Update the last devices file(s). | |
371 for path in args.known_devices_files: | |
372 device_list.WritePersistentDeviceList( | |
373 path, [status['serial'] for status in statuses]) | |
374 | |
375 # Write device info to file for buildbot info display. | |
376 if os.path.exists('/home/chrome-bot'): | |
377 with open('/home/chrome-bot/.adb_device_info', 'w') as f: | |
378 for status in statuses: | |
379 try: | |
380 if status['adb_status'] == 'device': | |
381 f.write('{serial} {adb_status} {build_product} {build_id} ' | |
382 '{temperature:.1f}C {level}%\n'.format( | |
383 serial=status['serial'], | |
384 adb_status=status['adb_status'], | |
385 build_product=status['type'], | |
386 build_id=status['build'], | |
387 temperature=float(status['battery']['temperature']) / 10, | |
388 level=status['battery']['level'] | |
389 )) | |
390 elif status.get('usb_status', False): | |
391 f.write('{serial} {adb_status}\n'.format( | |
392 serial=status['serial'], | |
393 adb_status=status['adb_status'] | |
394 )) | |
395 else: | |
396 f.write('{serial} offline\n'.format( | |
397 serial=status['serial'] | |
398 )) | |
399 except Exception: # pylint: disable=broad-except | |
400 pass | |
401 | |
402 # Dump the device statuses to JSON. | |
403 if args.json_output: | |
404 with open(args.json_output, 'wb') as f: | |
405 f.write(json.dumps(statuses, indent=4)) | |
406 | |
407 live_devices = [status['serial'] for status in statuses | |
408 if (status['adb_status'] == 'device' | |
409 and not _IsBlacklisted(status['serial'], blacklist))] | |
410 | |
411 # If all devices failed, or if there are no devices, it's an infra error. | |
412 if not live_devices: | |
413 logging.error('No available devices.') | |
414 return 0 if live_devices else exit_codes.INFRA | |
415 | |
416 | |
417 if __name__ == '__main__': | |
418 sys.exit(main()) | |
OLD | NEW |