blob: 17ff80b90b1af2d3c580dac394bee3835946268e [file] [log] [blame]
Ang Li93420002016-05-10 19:11:44 -07001#!/usr/bin/env python3.4
2#
3# Copyright 2016 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17from builtins import str
18from builtins import open
19
20import os
21import time
22import traceback
23
24from vts.runners.host import logger as vts_logger
25from vts.runners.host import signals
26from vts.runners.host import utils
27from vts.runners.utils.pythoncontrollers import adb
28from vts.runners.utils.pythoncontrollers import android
29from vts.runners.utils.pythoncontrollers import event_dispatcher
30from vts.runners.utils.pythoncontrollers import fastboot
31
32VTS_CONTROLLER_CONFIG_NAME = "AndroidDevice"
33VTS_CONTROLLER_REFERENCE_NAME = "android_devices"
34
35ANDROID_DEVICE_PICK_ALL_TOKEN = "*"
36# Key name for adb logcat extra params in config file.
37ANDROID_DEVICE_ADB_LOGCAT_PARAM_KEY = "adb_logcat_param"
38ANDROID_DEVICE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
39ANDROID_DEVICE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
40
41class AndroidDeviceError(signals.ControllerError):
42 pass
43
44def create(configs, logger):
45 if not configs:
46 raise AndroidDeviceError(ANDROID_DEVICE_EMPTY_CONFIG_MSG)
47 elif configs == ANDROID_DEVICE_PICK_ALL_TOKEN:
48 ads = get_all_instances(logger=logger)
49 elif not isinstance(configs, list):
50 raise AndroidDeviceError(ANDROID_DEVICE_NOT_LIST_CONFIG_MSG)
51 elif isinstance(configs[0], str):
52 # Configs is a list of serials.
53 ads = get_instances(configs, logger)
54 else:
55 # Configs is a list of dicts.
56 ads = get_instances_with_configs(configs, logger)
57 connected_ads = list_adb_devices()
58 for ad in ads:
59 if ad.serial not in connected_ads:
60 raise AndroidDeviceError(("Android device %s is specified in config"
61 " but is not attached.") % ad.serial)
62 ad.startAdbLogcat()
63 try:
64 ad.getSl4aClient()
65 ad.ed.start()
66 except:
67 # This exception is logged here to help with debugging under py2,
68 # because "exception raised while processing another exception" is
69 # only printed under py3.
70 msg = "Failed to start sl4a on %s" % ad.serial
71 logger.exception(msg)
72 raise AndroidDeviceError(msg)
73 return ads
74
75def destroy(ads):
76 for ad in ads:
77 try:
78 ad.closeAllSl4aSession()
79 except:
80 pass
81 if ad.adb_logcat_process:
82 ad.stopAdbLogcat()
83
84def _parse_device_list(device_list_str, key):
85 """Parses a byte string representing a list of devices. The string is
86 generated by calling either adb or fastboot.
87
88 Args:
89 device_list_str: Output of adb or fastboot.
90 key: The token that signifies a device in device_list_str.
91
92 Returns:
93 A list of android device serial numbers.
94 """
95 clean_lines = str(device_list_str, 'utf-8').strip().split('\n')
96 results = []
97 for line in clean_lines:
98 tokens = line.strip().split('\t')
99 if len(tokens) == 2 and tokens[1] == key:
100 results.append(tokens[0])
101 return results
102
103def list_adb_devices():
104 """List all android devices connected to the computer that are detected by
105 adb.
106
107 Returns:
108 A list of android device serials. Empty if there's none.
109 """
110 out = adb.AdbProxy().devices()
111 return _parse_device_list(out, "device")
112
113def list_fastboot_devices():
114 """List all android devices connected to the computer that are in in
115 fastboot mode. These are detected by fastboot.
116
117 Returns:
118 A list of android device serials. Empty if there's none.
119 """
120 out = fastboot.FastbootProxy().devices()
121 return _parse_device_list(out, "fastboot")
122
123def get_instances(serials, logger=None):
124 """Create AndroidDevice instances from a list of serials.
125
126 Args:
127 serials: A list of android device serials.
128 logger: A logger to be passed to each instance.
129
130 Returns:
131 A list of AndroidDevice objects.
132 """
133 results = []
134 for s in serials:
135 results.append(AndroidDevice(s, logger=logger))
136 return results
137
138def get_instances_with_configs(configs, logger=None):
139 """Create AndroidDevice instances from a list of json configs.
140
141 Each config should have the required key-value pair "serial".
142
143 Args:
144 configs: A list of dicts each representing the configuration of one
145 android device.
146 logger: A logger to be passed to each instance.
147
148 Returns:
149 A list of AndroidDevice objects.
150 """
151 results = []
152 for c in configs:
153 try:
154 serial = c.pop("serial")
155 except KeyError:
156 raise AndroidDeviceError(('Required value "serial" is missing in '
157 'AndroidDevice config %s.') % c)
158 ad = AndroidDevice(serial, logger=logger)
159 ad.loadConfig(c)
160 results.append(ad)
161 return results
162
163def get_all_instances(include_fastboot=False, logger=None):
164 """Create AndroidDevice instances for all attached android devices.
165
166 Args:
167 include_fastboot: Whether to include devices in bootloader mode or not.
168 logger: A logger to be passed to each instance.
169
170 Returns:
171 A list of AndroidDevice objects each representing an android device
172 attached to the computer.
173 """
174 if include_fastboot:
175 serial_list = list_adb_devices() + list_fastboot_devices()
176 return get_instances(serial_list, logger=logger)
177 return get_instances(list_adb_devices(), logger=logger)
178
179def filter_devices(ads, func):
180 """Finds the AndroidDevice instances from a list that match certain
181 conditions.
182
183 Args:
184 ads: A list of AndroidDevice instances.
185 func: A function that takes an AndroidDevice object and returns True
186 if the device satisfies the filter condition.
187
188 Returns:
189 A list of AndroidDevice instances that satisfy the filter condition.
190 """
191 results = []
192 for ad in ads:
193 if func(ad):
194 results.append(ad)
195 return results
196
197def get_device(ads, **kwargs):
198 """Finds a unique AndroidDevice instance from a list that has specific
199 attributes of certain values.
200
201 Example:
202 get_device(android_devices, label="foo", phone_number="1234567890")
203 get_device(android_devices, model="angler")
204
205 Args:
206 ads: A list of AndroidDevice instances.
207 kwargs: keyword arguments used to filter AndroidDevice instances.
208
209 Returns:
210 The target AndroidDevice instance.
211
212 Raises:
213 AndroidDeviceError is raised if none or more than one device is
214 matched.
215 """
216 def _get_device_filter(ad):
217 for k, v in kwargs.items():
218 if not hasattr(ad, k):
219 return False
220 elif getattr(ad, k) != v:
221 return False
222 return True
223 filtered = filter_devices(ads, _get_device_filter)
224 if not filtered:
225 raise AndroidDeviceError(("Could not find a target device that matches"
226 " condition: %s.") % kwargs)
227 elif len(filtered) == 1:
228 return filtered[0]
229 else:
230 serials = [ad.serial for ad in filtered]
231 raise AndroidDeviceError("More than one device matched: %s" % serials)
232
233def takeBugReports(ads, test_name, begin_time):
234 """Takes bug reports on a list of android devices.
235
236 If you want to take a bug report, call this function with a list of
237 android_device objects in on_fail. But reports will be taken on all the
238 devices in the list concurrently. Bug report takes a relative long
239 time to take, so use this cautiously.
240
241 Args:
242 ads: A list of AndroidDevice instances.
243 test_name: Name of the test case that triggered this bug report.
244 begin_time: Logline format timestamp taken when the test started.
245 """
246 begin_time = vts_logger.normalizeLogLineTimestamp(begin_time)
247 def take_br(test_name, begin_time, ad):
248 ad.takeBugReport(test_name, begin_time)
249 args = [(test_name, begin_time, ad) for ad in ads]
250 utils.concurrent_exec(take_br, args)
251
252class AndroidDevice:
253 """Class representing an android device.
254
255 Each object of this class represents one Android device in ACTS, including
256 handles to adb, fastboot, and sl4a clients. In addition to direct adb
257 commands, this object also uses adb port forwarding to talk to the Android
258 device.
259
260 Attributes:
261 serial: A string that's the serial number of the Androi device.
262 h_port: An integer that's the port number for adb port forwarding used
263 on the computer the Android device is connected
264 d_port: An integer that's the port number used on the Android device
265 for adb port forwarding.
266 log: A LoggerProxy object used for the class's internal logging.
267 log_path: A string that is the path where all logs collected on this
268 android device should be stored.
269 adb_logcat_process: A process that collects the adb logcat.
270 adb_logcat_file_path: A string that's the full path to the adb logcat
271 file collected, if any.
272 adb: An AdbProxy object used for interacting with the device via adb.
273 fastboot: A FastbootProxy object used for interacting with the device
274 via fastboot.
275 """
276
277 def __init__(self, serial="", host_port=None, device_port=8080,
278 logger=None):
279 self.serial = serial
280 self.h_port = host_port
281 self.d_port = device_port
282 self.log = logging.getLogger()
283 lp = self.log.log_path
284 self.log_path = os.path.join(lp, "AndroidDevice%s" % serial)
285 self._droid_sessions = {}
286 self._event_dispatchers = {}
287 self.adb_logcat_process = None
288 self.adb_logcat_file_path = None
289 self.adb = adb.AdbProxy(serial)
290 self.fastboot = fastboot.FastbootProxy(serial)
291 if not self.isBootloaderMode:
292 self.rootAdb()
293
294 def __del__(self):
295 if self.h_port:
296 self.adb.forward("--remove tcp:%d" % self.h_port)
297 if self.adb_logcat_process:
298 self.stopAdbLogcat()
299
300 @property
301 def isBootloaderMode(self):
302 """True if the device is in bootloader mode.
303 """
304 return self.serial in list_fastboot_devices()
305
306 @property
307 def isAdbRoot(self):
308 """True if adb is running as root for this device.
309 """
310 return "root" in self.adb.shell("id -u").decode("utf-8")
311
312 @property
313 def model(self):
314 """The Android code name for the device.
315 """
316 # If device is in bootloader mode, get mode name from fastboot.
317 if self.isBootloaderMode:
318 out = self.fastboot.getvar("product").strip()
319 # "out" is never empty because of the "total time" message fastboot
320 # writes to stderr.
321 lines = out.decode("utf-8").split('\n', 1)
322 if lines:
323 tokens = lines[0].split(' ')
324 if len(tokens) > 1:
325 return tokens[1].lower()
326 return None
327 out = self.adb.shell('getprop | grep ro.build.product')
328 model = out.decode("utf-8").strip().split('[')[-1][:-1].lower()
329 if model == "sprout":
330 return model
331 else:
332 out = self.adb.shell('getprop | grep ro.product.name')
333 model = out.decode("utf-8").strip().split('[')[-1][:-1].lower()
334 return model
335
336 @property
337 def droid(self):
338 """The first sl4a session initiated on this device. None if there isn't
339 one.
340 """
341 try:
342 session_id = sorted(self._droid_sessions)[0]
343 return self._droid_sessions[session_id][0]
344 except IndexError:
345 return None
346
347 @property
348 def ed(self):
349 """The first event_dispatcher instance created on this device. None if
350 there isn't one.
351 """
352 try:
353 session_id = sorted(self._event_dispatchers)[0]
354 return self._event_dispatchers[session_id]
355 except IndexError:
356 return None
357
358 @property
359 def droids(self):
360 """A list of the active sl4a sessions on this device.
361
362 If multiple connections exist for the same session, only one connection
363 is listed.
364 """
365 keys = sorted(self._droid_sessions)
366 results = []
367 for k in keys:
368 results.append(self._droid_sessions[k][0])
369 return results
370
371 @property
372 def eds(self):
373 """A list of the event_dispatcher objects on this device.
374
375 The indexing of the list matches that of the droids property.
376 """
377 keys = sorted(self._event_dispatchers)
378 results = []
379 for k in keys:
380 results.append(self._event_dispatchers[k])
381 return results
382
383 @property
384 def isAdbLogcatOn(self):
385 """Whether there is an ongoing adb logcat collection.
386 """
387 if self.adb_logcat_process:
388 return True
389 return False
390
391 def loadConfig(self, config):
392 """Add attributes to the AndroidDevice object based on json config.
393
394 Args:
395 config: A dictionary representing the configs.
396
397 Raises:
398 AndroidDeviceError is raised if the config is trying to overwrite
399 an existing attribute.
400 """
401 for k, v in config.items():
402 if hasattr(self, k):
403 raise AndroidDeviceError(("Attempting to set existing "
404 "attribute %s on %s") % (k, self.serial))
405 setattr(self, k, v)
406
407 def rootAdb(self):
408 """Change adb to root mode for this device.
409 """
410 if not self.isAdbRoot:
411 self.adb.root()
412 self.adb.wait_for_device()
413 self.adb.remount()
414 self.adb.wait_for_device()
415
416 def getSl4aClient(self, handle_event=True):
417 """Create an sl4a connection to the device.
418
419 Return the connection handler 'droid'. By default, another connection
420 on the same session is made for EventDispatcher, and the dispatcher is
421 returned to the caller as well.
422 If sl4a server is not started on the device, try to start it.
423
424 Args:
425 handle_event: True if this droid session will need to handle
426 events.
427
428 Returns:
429 droid: Android object used to communicate with sl4a on the android
430 device.
431 ed: An optional EventDispatcher to organize events for this droid.
432
433 Examples:
434 Don't need event handling:
435 >>> ad = AndroidDevice()
436 >>> droid = ad.getSl4aClient(False)
437
438 Need event handling:
439 >>> ad = AndroidDevice()
440 >>> droid, ed = ad.getSl4aClient()
441 """
442 if not self.h_port or not adb.is_port_available(self.h_port):
443 self.h_port = adb.get_available_host_port()
444 self.adb.tcp_forward(self.h_port, self.d_port)
445 try:
446 droid = self.start_new_session()
447 except:
448 self.adb.start_sl4a()
449 droid = self.start_new_session()
450 if handle_event:
451 ed = self.getSl4aEventDispatcher(droid)
452 return droid, ed
453 return droid
454
455 def getSl4aEventDispatcher(self, droid):
456 """Return an EventDispatcher for an sl4a session
457
458 Args:
459 droid: Session to create EventDispatcher for.
460
461 Returns:
462 ed: An EventDispatcher for specified session.
463 """
464 ed_key = self.serial + str(droid.uid)
465 if ed_key in self._event_dispatchers:
466 if self._event_dispatchers[ed_key] is None:
467 raise AndroidDeviceError("EventDispatcher Key Empty")
468 self.log.debug("Returning existing key %s for event dispatcher!",
469 ed_key)
470 return self._event_dispatchers[ed_key]
471 event_droid = self.add_new_connection_to_session(droid.uid)
472 ed = event_dispatcher.EventDispatcher(event_droid)
473 self._event_dispatchers[ed_key] = ed
474 return ed
475
476 def _is_timestamp_in_range(self, target, begin_time, end_time):
477 low = vts_logger.logLineTimestampComparator(begin_time, target) <= 0
478 high = vts_logger.logLineTimestampComparator(end_time, target) >= 0
479 return low and high
480
481 def takeAdbLogExcerpt(self, tag, begin_time):
482 """Takes an excerpt of the adb logcat log from a certain time point to
483 current time.
484
485 Args:
486 tag: An identifier of the time period, usualy the name of a test.
487 begin_time: Logline format timestamp of the beginning of the time
488 period.
489 """
490 if not self.adb_logcat_file_path:
491 raise AndroidDeviceError(("Attempting to cat adb log when none has"
492 " been collected on Android device %s."
493 ) % self.serial)
494 end_time = vts_logger.getLogLineTimestamp()
495 self.log.debug("Extracting adb log from logcat.")
496 adb_excerpt_path = os.path.join(self.log_path, "AdbLogExcerpts")
497 utils.create_dir(adb_excerpt_path)
498 f_name = os.path.basename(self.adb_logcat_file_path)
499 out_name = f_name.replace("adblog,", "").replace(".txt", "")
500 out_name = ",{},{}.txt".format(begin_time, out_name)
501 tag_len = utils.MAX_FILENAME_LEN - len(out_name)
502 tag = tag[:tag_len]
503 out_name = tag + out_name
504 full_adblog_path = os.path.join(adb_excerpt_path, out_name)
505 with open(full_adblog_path, 'w', encoding='utf-8') as out:
506 in_file = self.adb_logcat_file_path
507 with open(in_file, 'r', encoding='utf-8', errors='replace') as f:
508 in_range = False
509 while True:
510 line = None
511 try:
512 line = f.readline()
513 if not line:
514 break
515 except:
516 continue
517 line_time = line[:vts_logger.log_line_timestamp_len]
518 if not vts_logger.isValidLogLineTimestamp(line_time):
519 continue
520 if self._is_timestamp_in_range(line_time, begin_time,
521 end_time):
522 in_range = True
523 if not line.endswith('\n'):
524 line += '\n'
525 out.write(line)
526 else:
527 if in_range:
528 break
529
530 def startAdbLogcat(self):
531 """Starts a standing adb logcat collection in separate subprocesses and
532 save the logcat in a file.
533 """
534 if self.isAdbLogcatOn:
535 raise AndroidDeviceError(("Android device {} already has an adb "
536 "logcat thread going on. Cannot start "
537 "another one.").format(self.serial))
538 # Disable adb log spam filter.
539 self.adb.shell("logpersist.start")
540 f_name = "adblog,{},{}.txt".format(self.model, self.serial)
541 utils.create_dir(self.log_path)
542 logcat_file_path = os.path.join(self.log_path, f_name)
543 try:
544 extra_params = self.adb_logcat_param
545 except AttributeError:
546 extra_params = "-b all"
547 cmd = "adb -s {} logcat -v threadtime {} >> {}".format(
548 self.serial, extra_params, logcat_file_path)
549 self.adb_logcat_process = utils.start_standing_subprocess(cmd)
550 self.adb_logcat_file_path = logcat_file_path
551
552 def stopAdbLogcat(self):
553 """Stops the adb logcat collection subprocess.
554 """
555 if not self.isAdbLogcatOn:
556 raise AndroidDeviceError(("Android device {} does not have an "
557 "ongoing adb logcat collection."
558 ).format(self.serial))
559 utils.stop_standing_subprocess(self.adb_logcat_process)
560 self.adb_logcat_process = None
561
562 def takeBugReport(self, test_name, begin_time):
563 """Takes a bug report on the device and stores it in a file.
564
565 Args:
566 test_name: Name of the test case that triggered this bug report.
567 begin_time: Logline format timestamp taken when the test started.
568 """
569 br_path = os.path.join(self.log_path, "BugReports")
570 utils.create_dir(br_path)
571 base_name = ",{},{}.txt".format(begin_time, self.serial)
572 test_name_len = utils.MAX_FILENAME_LEN - len(base_name)
573 out_name = test_name[:test_name_len] + base_name
574 full_out_path = os.path.join(br_path, out_name.replace(' ', '\ '))
575 self.log.info("Taking bugreport for %s on %s", test_name, self.serial)
576 self.adb.bugreport(" > {}".format(full_out_path))
577 self.log.info("Bugreport for %s taken at %s", test_name, full_out_path)
578
579 def start_new_session(self):
580 """Start a new session in sl4a.
581
582 Also caches the droid in a dict with its uid being the key.
583
584 Returns:
585 An Android object used to communicate with sl4a on the android
586 device.
587
588 Raises:
589 SL4AException: Something is wrong with sl4a and it returned an
590 existing uid to a new session.
591 """
592 droid = android.Android(port=self.h_port)
593 if droid.uid in self._droid_sessions:
594 raise android.SL4AException(("SL4A returned an existing uid for a "
595 "new session. Abort."))
596 self._droid_sessions[droid.uid] = [droid]
597 return droid
598
599 def add_new_connection_to_session(self, session_id):
600 """Create a new connection to an existing sl4a session.
601
602 Args:
603 session_id: UID of the sl4a session to add connection to.
604
605 Returns:
606 An Android object used to communicate with sl4a on the android
607 device.
608
609 Raises:
610 AndroidDeviceError: Raised if the session it's trying to connect to
611 does not exist.
612 """
613 if session_id not in self._droid_sessions:
614 raise AndroidDeviceError("Session %d doesn't exist." % session_id)
615 droid = android.Android(cmd='continue', uid=session_id,
616 port=self.h_port)
617 return droid
618
619 def closeOneSl4aSession(self, session_id):
620 """Terminate a session in sl4a.
621
622 Send terminate signal to sl4a server; stop dispatcher associated with
623 the session. Clear corresponding droids and dispatchers from cache.
624
625 Args:
626 session_id: UID of the sl4a session to terminate.
627 """
628 if self._droid_sessions and (session_id in self._droid_sessions):
629 for droid in self._droid_sessions[session_id]:
630 droid.closeSl4aSession()
631 droid.close()
632 del self._droid_sessions[session_id]
633 ed_key = self.serial + str(session_id)
634 if ed_key in self._event_dispatchers:
635 self._event_dispatchers[ed_key].clean_up()
636 del self._event_dispatchers[ed_key]
637
638 def closeAllSl4aSession(self):
639 """Terminate all sl4a sessions on the AndroidDevice instance.
640
641 Terminate all sessions and clear caches.
642 """
643 if self._droid_sessions:
644 session_ids = list(self._droid_sessions.keys())
645 for session_id in session_ids:
646 try:
647 self.closeOneSl4aSession(session_id)
648 except:
649 msg = "Failed to terminate session %d." % session_id
650 self.log.exception(msg)
651 self.log.error(traceback.format_exc())
652 if self.h_port:
653 self.adb.forward("--remove tcp:%d" % self.h_port)
654 self.h_port = None
655
656 def runIperfClient(self, server_host, extra_args=""):
657 """Start iperf client on the device.
658
659 Return status as true if iperf client start successfully.
660 And data flow information as results.
661
662 Args:
663 server_host: Address of the iperf server.
664 extra_args: A string representing extra arguments for iperf client,
665 e.g. "-i 1 -t 30".
666
667 Returns:
668 status: true if iperf client start successfully.
669 results: results have data flow information
670 """
671 out = self.adb.shell("iperf3 -c {} {}".format(server_host, extra_args))
672 clean_out = str(out,'utf-8').strip().split('\n')
673 if "error" in clean_out[0].lower():
674 return False, clean_out
675 return True, clean_out
676
677 @utils.timeout(15 * 60)
678 def waitForBootCompletion(self):
679 """Waits for Android framework to broadcast ACTION_BOOT_COMPLETED.
680
681 This function times out after 15 minutes.
682 """
683 self.adb.wait_for_device()
684 while True:
685 try:
686 out = self.adb.shell("getprop sys.boot_completed")
687 completed = out.decode('utf-8').strip()
688 if completed == '1':
689 return
690 except adb.AdbError:
691 # adb shell calls may fail during certain period of booting
692 # process, which is normal. Ignoring these errors.
693 pass
694 time.sleep(5)
695
696 def reboot(self):
697 """Reboots the device.
698
699 Terminate all sl4a sessions, reboot the device, wait for device to
700 complete booting, and restart an sl4a session.
701
702 This is a blocking method.
703
704 This is probably going to print some error messages in console. Only
705 use if there's no other option.
706
707 Example:
708 droid, ed = ad.reboot()
709
710 Returns:
711 An sl4a session with an event_dispatcher.
712
713 Raises:
714 AndroidDeviceError is raised if waiting for completion timed
715 out.
716 """
717 if self.isBootloaderMode:
718 self.fastboot.reboot()
719 return
720 has_adb_log = self.isAdbLogcatOn
721 if has_adb_log:
722 self.stopAdbLogcat()
723 self.closeAllSl4aSession()
724 self.adb.reboot()
725 self.waitForBootCompletion()
726 self.rootAdb()
727 droid, ed = self.getSl4aClient()
728 ed.start()
729 if has_adb_log:
730 self.startAdbLogcat()
731 return droid, ed