1#!/usr/bin/env python3 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 17import base64 18import concurrent.futures 19import copy 20import datetime 21import functools 22import ipaddress 23import json 24import logging 25import os 26import platform 27import psutil 28import random 29import re 30import signal 31import string 32import socket 33import subprocess 34import time 35import threading 36import traceback 37import zipfile 38from concurrent.futures import ThreadPoolExecutor 39 40from acts import signals 41from acts.controllers.adb_lib.error import AdbError 42from acts.libs.proc import job 43 44# File name length is limited to 255 chars on some OS, so we need to make sure 45# the file names we output fits within the limit. 46MAX_FILENAME_LEN = 255 47 48# All Fuchsia devices use this suffix for link-local mDNS host names. 49FUCHSIA_MDNS_TYPE = '_fuchsia._udp.local.' 50 51# Default max seconds it takes to Duplicate Address Detection to finish before 52# assigning an IPv6 address. 53DAD_TIMEOUT_SEC = 30 54 55 56class ActsUtilsError(Exception): 57 """Generic error raised for exceptions in ACTS utils.""" 58 59 60class NexusModelNames: 61 # TODO(angli): This will be fixed later by angli. 62 ONE = 'sprout' 63 N5 = 'hammerhead' 64 N5v2 = 'bullhead' 65 N6 = 'shamu' 66 N6v2 = 'angler' 67 N6v3 = 'marlin' 68 N5v3 = 'sailfish' 69 70 71class DozeModeStatus: 72 ACTIVE = "ACTIVE" 73 IDLE = "IDLE" 74 75 76ascii_letters_and_digits = string.ascii_letters + string.digits 77valid_filename_chars = "-_." + ascii_letters_and_digits 78 79models = ("sprout", "occam", "hammerhead", "bullhead", "razor", "razorg", 80 "shamu", "angler", "volantis", "volantisg", "mantaray", "fugu", 81 "ryu", "marlin", "sailfish") 82 83manufacture_name_to_model = { 84 "flo": "razor", 85 "flo_lte": "razorg", 86 "flounder": "volantis", 87 "flounder_lte": "volantisg", 88 "dragon": "ryu" 89} 90 91GMT_to_olson = { 92 "GMT-9": "America/Anchorage", 93 "GMT-8": "US/Pacific", 94 "GMT-7": "US/Mountain", 95 "GMT-6": "US/Central", 96 "GMT-5": "US/Eastern", 97 "GMT-4": "America/Barbados", 98 "GMT-3": "America/Buenos_Aires", 99 "GMT-2": "Atlantic/South_Georgia", 100 "GMT-1": "Atlantic/Azores", 101 "GMT+0": "Africa/Casablanca", 102 "GMT+1": "Europe/Amsterdam", 103 "GMT+2": "Europe/Athens", 104 "GMT+3": "Europe/Moscow", 105 "GMT+4": "Asia/Baku", 106 "GMT+5": "Asia/Oral", 107 "GMT+6": "Asia/Almaty", 108 "GMT+7": "Asia/Bangkok", 109 "GMT+8": "Asia/Hong_Kong", 110 "GMT+9": "Asia/Tokyo", 111 "GMT+10": "Pacific/Guam", 112 "GMT+11": "Pacific/Noumea", 113 "GMT+12": "Pacific/Fiji", 114 "GMT+13": "Pacific/Tongatapu", 115 "GMT-11": "Pacific/Midway", 116 "GMT-10": "Pacific/Honolulu" 117} 118 119 120def abs_path(path): 121 """Resolve the '.' and '~' in a path to get the absolute path. 122 123 Args: 124 path: The path to expand. 125 126 Returns: 127 The absolute path of the input path. 128 """ 129 return os.path.abspath(os.path.expanduser(path)) 130 131 132def get_current_epoch_time(): 133 """Current epoch time in milliseconds. 134 135 Returns: 136 An integer representing the current epoch time in milliseconds. 137 """ 138 return int(round(time.time() * 1000)) 139 140 141def get_current_human_time(): 142 """Returns the current time in human readable format. 143 144 Returns: 145 The current time stamp in Month-Day-Year Hour:Min:Sec format. 146 """ 147 return time.strftime("%m-%d-%Y %H:%M:%S ") 148 149 150def epoch_to_human_time(epoch_time): 151 """Converts an epoch timestamp to human readable time. 152 153 This essentially converts an output of get_current_epoch_time to an output 154 of get_current_human_time 155 156 Args: 157 epoch_time: An integer representing an epoch timestamp in milliseconds. 158 159 Returns: 160 A time string representing the input time. 161 None if input param is invalid. 162 """ 163 if isinstance(epoch_time, int): 164 try: 165 d = datetime.datetime.fromtimestamp(epoch_time / 1000) 166 return d.strftime("%m-%d-%Y %H:%M:%S ") 167 except ValueError: 168 return None 169 170 171def get_timezone_olson_id(): 172 """Return the Olson ID of the local (non-DST) timezone. 173 174 Returns: 175 A string representing one of the Olson IDs of the local (non-DST) 176 timezone. 177 """ 178 tzoffset = int(time.timezone / 3600) 179 gmt = None 180 if tzoffset <= 0: 181 gmt = "GMT+{}".format(-tzoffset) 182 else: 183 gmt = "GMT-{}".format(tzoffset) 184 return GMT_to_olson[gmt] 185 186 187def get_next_device(test_bed_controllers, used_devices): 188 """Gets the next device in a list of testbed controllers 189 190 Args: 191 test_bed_controllers: A list of testbed controllers of a particular 192 type, for example a list ACTS Android devices. 193 used_devices: A list of devices that have been used. This can be a 194 mix of devices, for example a fuchsia device and an Android device. 195 Returns: 196 The next device in the test_bed_controllers list or None if there are 197 no items that are not in the used devices list. 198 """ 199 if test_bed_controllers: 200 device_list = test_bed_controllers 201 else: 202 raise ValueError('test_bed_controllers is empty.') 203 for used_device in used_devices: 204 if used_device in device_list: 205 device_list.remove(used_device) 206 if device_list: 207 return device_list[0] 208 else: 209 return None 210 211 212def find_files(paths, file_predicate): 213 """Locate files whose names and extensions match the given predicate in 214 the specified directories. 215 216 Args: 217 paths: A list of directory paths where to find the files. 218 file_predicate: A function that returns True if the file name and 219 extension are desired. 220 221 Returns: 222 A list of files that match the predicate. 223 """ 224 file_list = [] 225 if not isinstance(paths, list): 226 paths = [paths] 227 for path in paths: 228 p = abs_path(path) 229 for dirPath, subdirList, fileList in os.walk(p): 230 for fname in fileList: 231 name, ext = os.path.splitext(fname) 232 if file_predicate(name, ext): 233 file_list.append((dirPath, name, ext)) 234 return file_list 235 236 237def load_config(file_full_path, log_errors=True): 238 """Loads a JSON config file. 239 240 Returns: 241 A JSON object. 242 """ 243 with open(file_full_path, 'r') as f: 244 try: 245 return json.load(f) 246 except Exception as e: 247 if log_errors: 248 logging.error("Exception error to load %s: %s", f, e) 249 raise 250 251 252def load_file_to_base64_str(f_path): 253 """Loads the content of a file into a base64 string. 254 255 Args: 256 f_path: full path to the file including the file name. 257 258 Returns: 259 A base64 string representing the content of the file in utf-8 encoding. 260 """ 261 path = abs_path(f_path) 262 with open(path, 'rb') as f: 263 f_bytes = f.read() 264 base64_str = base64.b64encode(f_bytes).decode("utf-8") 265 return base64_str 266 267 268def dump_string_to_file(content, file_path, mode='w'): 269 """ Dump content of a string to 270 271 Args: 272 content: content to be dumped to file 273 file_path: full path to the file including the file name. 274 mode: file open mode, 'w' (truncating file) by default 275 :return: 276 """ 277 full_path = abs_path(file_path) 278 with open(full_path, mode) as f: 279 f.write(content) 280 281 282def list_of_dict_to_dict_of_dict(list_of_dicts, dict_key): 283 """Transforms a list of dicts to a dict of dicts. 284 285 For instance: 286 >>> list_of_dict_to_dict_of_dict([{'a': '1', 'b':'2'}, 287 >>> {'a': '3', 'b':'4'}], 288 >>> 'b') 289 290 returns: 291 292 >>> {'2': {'a': '1', 'b':'2'}, 293 >>> '4': {'a': '3', 'b':'4'}} 294 295 Args: 296 list_of_dicts: A list of dictionaries. 297 dict_key: The key in the inner dict to be used as the key for the 298 outer dict. 299 Returns: 300 A dict of dicts. 301 """ 302 return {d[dict_key]: d for d in list_of_dicts} 303 304 305def dict_purge_key_if_value_is_none(dictionary): 306 """Removes all pairs with value None from dictionary.""" 307 for k, v in dict(dictionary).items(): 308 if v is None: 309 del dictionary[k] 310 return dictionary 311 312 313def find_field(item_list, cond, comparator, target_field): 314 """Finds the value of a field in a dict object that satisfies certain 315 conditions. 316 317 Args: 318 item_list: A list of dict objects. 319 cond: A param that defines the condition. 320 comparator: A function that checks if an dict satisfies the condition. 321 target_field: Name of the field whose value to be returned if an item 322 satisfies the condition. 323 324 Returns: 325 Target value or None if no item satisfies the condition. 326 """ 327 for item in item_list: 328 if comparator(item, cond) and target_field in item: 329 return item[target_field] 330 return None 331 332 333def rand_ascii_str(length): 334 """Generates a random string of specified length, composed of ascii letters 335 and digits. 336 337 Args: 338 length: The number of characters in the string. 339 340 Returns: 341 The random string generated. 342 """ 343 letters = [random.choice(ascii_letters_and_digits) for i in range(length)] 344 return ''.join(letters) 345 346 347def rand_hex_str(length): 348 """Generates a random string of specified length, composed of hex digits 349 350 Args: 351 length: The number of characters in the string. 352 353 Returns: 354 The random string generated. 355 """ 356 letters = [random.choice(string.hexdigits) for i in range(length)] 357 return ''.join(letters) 358 359 360# Thead/Process related functions. 361def concurrent_exec(func, param_list): 362 """Executes a function with different parameters pseudo-concurrently. 363 364 This is basically a map function. Each element (should be an iterable) in 365 the param_list is unpacked and passed into the function. Due to Python's 366 GIL, there's no true concurrency. This is suited for IO-bound tasks. 367 368 Args: 369 func: The function that parforms a task. 370 param_list: A list of iterables, each being a set of params to be 371 passed into the function. 372 373 Returns: 374 A list of return values from each function execution. If an execution 375 caused an exception, the exception object will be the corresponding 376 result. 377 """ 378 with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor: 379 # Start the load operations and mark each future with its params 380 future_to_params = {executor.submit(func, *p): p for p in param_list} 381 return_vals = [] 382 for future in concurrent.futures.as_completed(future_to_params): 383 params = future_to_params[future] 384 try: 385 return_vals.append(future.result()) 386 except Exception as exc: 387 print("{} generated an exception: {}".format( 388 params, traceback.format_exc())) 389 return_vals.append(exc) 390 return return_vals 391 392 393def exe_cmd(*cmds): 394 """Executes commands in a new shell. 395 396 Args: 397 cmds: A sequence of commands and arguments. 398 399 Returns: 400 The output of the command run. 401 402 Raises: 403 OSError is raised if an error occurred during the command execution. 404 """ 405 cmd = ' '.join(cmds) 406 proc = subprocess.Popen(cmd, 407 stdout=subprocess.PIPE, 408 stderr=subprocess.PIPE, 409 shell=True) 410 (out, err) = proc.communicate() 411 if not err: 412 return out 413 raise OSError(err) 414 415 416def require_sl4a(android_devices): 417 """Makes sure sl4a connection is established on the given AndroidDevice 418 objects. 419 420 Args: 421 android_devices: A list of AndroidDevice objects. 422 423 Raises: 424 AssertionError is raised if any given android device does not have SL4A 425 connection established. 426 """ 427 for ad in android_devices: 428 msg = "SL4A connection not established properly on %s." % ad.serial 429 assert ad.droid, msg 430 431 432def _assert_subprocess_running(proc): 433 """Checks if a subprocess has terminated on its own. 434 435 Args: 436 proc: A subprocess returned by subprocess.Popen. 437 438 Raises: 439 ActsUtilsError is raised if the subprocess has stopped. 440 """ 441 ret = proc.poll() 442 if ret is not None: 443 out, err = proc.communicate() 444 raise ActsUtilsError("Process %d has terminated. ret: %d, stderr: %s," 445 " stdout: %s" % (proc.pid, ret, err, out)) 446 447 448def start_standing_subprocess(cmd, check_health_delay=0, shell=True): 449 """Starts a long-running subprocess. 450 451 This is not a blocking call and the subprocess started by it should be 452 explicitly terminated with stop_standing_subprocess. 453 454 For short-running commands, you should use exe_cmd, which blocks. 455 456 You can specify a health check after the subprocess is started to make sure 457 it did not stop prematurely. 458 459 Args: 460 cmd: string, the command to start the subprocess with. 461 check_health_delay: float, the number of seconds to wait after the 462 subprocess starts to check its health. Default is 0, 463 which means no check. 464 465 Returns: 466 The subprocess that got started. 467 """ 468 proc = subprocess.Popen(cmd, 469 stdout=subprocess.PIPE, 470 stderr=subprocess.PIPE, 471 shell=shell, 472 preexec_fn=os.setpgrp) 473 logging.debug("Start standing subprocess with cmd: %s", cmd) 474 if check_health_delay > 0: 475 time.sleep(check_health_delay) 476 _assert_subprocess_running(proc) 477 return proc 478 479 480def stop_standing_subprocess(proc, kill_signal=signal.SIGTERM): 481 """Stops a subprocess started by start_standing_subprocess. 482 483 Before killing the process, we check if the process is running, if it has 484 terminated, ActsUtilsError is raised. 485 486 Catches and ignores the PermissionError which only happens on Macs. 487 488 Args: 489 proc: Subprocess to terminate. 490 """ 491 pid = proc.pid 492 logging.debug("Stop standing subprocess %d", pid) 493 _assert_subprocess_running(proc) 494 try: 495 os.killpg(pid, kill_signal) 496 except PermissionError: 497 pass 498 499 500def wait_for_standing_subprocess(proc, timeout=None): 501 """Waits for a subprocess started by start_standing_subprocess to finish 502 or times out. 503 504 Propagates the exception raised by the subprocess.wait(.) function. 505 The subprocess.TimeoutExpired exception is raised if the process timed-out 506 rather then terminating. 507 508 If no exception is raised: the subprocess terminated on its own. No need 509 to call stop_standing_subprocess() to kill it. 510 511 If an exception is raised: the subprocess is still alive - it did not 512 terminate. Either call stop_standing_subprocess() to kill it, or call 513 wait_for_standing_subprocess() to keep waiting for it to terminate on its 514 own. 515 516 Args: 517 p: Subprocess to wait for. 518 timeout: An integer number of seconds to wait before timing out. 519 """ 520 proc.wait(timeout) 521 522 523def sync_device_time(ad): 524 """Sync the time of an android device with the current system time. 525 526 Both epoch time and the timezone will be synced. 527 528 Args: 529 ad: The android device to sync time on. 530 """ 531 ad.adb.shell("settings put global auto_time 0", ignore_status=True) 532 ad.adb.shell("settings put global auto_time_zone 0", ignore_status=True) 533 droid = ad.droid 534 droid.setTimeZone(get_timezone_olson_id()) 535 droid.setTime(get_current_epoch_time()) 536 537 538# Timeout decorator block 539class TimeoutError(Exception): 540 """Exception for timeout decorator related errors. 541 """ 542 543 544def _timeout_handler(signum, frame): 545 """Handler function used by signal to terminate a timed out function. 546 """ 547 raise TimeoutError() 548 549 550def timeout(sec): 551 """A decorator used to add time out check to a function. 552 553 This only works in main thread due to its dependency on signal module. 554 Do NOT use it if the decorated funtion does not run in the Main thread. 555 556 Args: 557 sec: Number of seconds to wait before the function times out. 558 No timeout if set to 0 559 560 Returns: 561 What the decorated function returns. 562 563 Raises: 564 TimeoutError is raised when time out happens. 565 """ 566 567 def decorator(func): 568 569 @functools.wraps(func) 570 def wrapper(*args, **kwargs): 571 if sec: 572 signal.signal(signal.SIGALRM, _timeout_handler) 573 signal.alarm(sec) 574 try: 575 return func(*args, **kwargs) 576 except TimeoutError: 577 raise TimeoutError(("Function {} timed out after {} " 578 "seconds.").format(func.__name__, sec)) 579 finally: 580 signal.alarm(0) 581 582 return wrapper 583 584 return decorator 585 586 587def trim_model_name(model): 588 """Trim any prefix and postfix and return the android designation of the 589 model name. 590 591 e.g. "m_shamu" will be trimmed to "shamu". 592 593 Args: 594 model: model name to be trimmed. 595 596 Returns 597 Trimmed model name if one of the known model names is found. 598 None otherwise. 599 """ 600 # Directly look up first. 601 if model in models: 602 return model 603 if model in manufacture_name_to_model: 604 return manufacture_name_to_model[model] 605 # If not found, try trimming off prefix/postfix and look up again. 606 tokens = re.split("_|-", model) 607 for t in tokens: 608 if t in models: 609 return t 610 if t in manufacture_name_to_model: 611 return manufacture_name_to_model[t] 612 return None 613 614 615def force_airplane_mode(ad, new_state, timeout_value=60): 616 """Force the device to set airplane mode on or off by adb shell command. 617 618 Args: 619 ad: android device object. 620 new_state: Turn on airplane mode if True. 621 Turn off airplane mode if False. 622 timeout_value: max wait time for 'adb wait-for-device' 623 624 Returns: 625 True if success. 626 False if timeout. 627 """ 628 629 # Using timeout decorator. 630 # Wait for device with timeout. If after <timeout_value> seconds, adb 631 # is still waiting for device, throw TimeoutError exception. 632 @timeout(timeout_value) 633 def wait_for_device_with_timeout(ad): 634 ad.adb.wait_for_device() 635 636 try: 637 wait_for_device_with_timeout(ad) 638 ad.adb.shell("settings put global airplane_mode_on {}".format( 639 1 if new_state else 0)) 640 ad.adb.shell("am broadcast -a android.intent.action.AIRPLANE_MODE") 641 except TimeoutError: 642 # adb wait for device timeout 643 return False 644 return True 645 646 647def get_battery_level(ad): 648 """Gets battery level from device 649 650 Returns: 651 battery_level: int indicating battery level 652 """ 653 output = ad.adb.shell("dumpsys battery") 654 match = re.search(r"level: (?P<battery_level>\S+)", output) 655 battery_level = int(match.group("battery_level")) 656 return battery_level 657 658 659def get_device_usb_charging_status(ad): 660 """ Returns the usb charging status of the device. 661 662 Args: 663 ad: android device object 664 665 Returns: 666 True if charging 667 False if not charging 668 """ 669 adb_shell_result = ad.adb.shell("dumpsys deviceidle get charging") 670 ad.log.info("Device Charging State: {}".format(adb_shell_result)) 671 return adb_shell_result == 'true' 672 673 674def disable_usb_charging(ad): 675 """ Unplug device from usb charging. 676 677 Args: 678 ad: android device object 679 680 Returns: 681 True if device is unplugged 682 False otherwise 683 """ 684 ad.adb.shell("dumpsys battery unplug") 685 if not get_device_usb_charging_status(ad): 686 return True 687 else: 688 ad.log.info("Could not disable USB charging") 689 return False 690 691 692def enable_usb_charging(ad): 693 """ Plug device to usb charging. 694 695 Args: 696 ad: android device object 697 698 Returns: 699 True if device is Plugged 700 False otherwise 701 """ 702 ad.adb.shell("dumpsys battery reset") 703 if get_device_usb_charging_status(ad): 704 return True 705 else: 706 ad.log.info("Could not enable USB charging") 707 return False 708 709 710def enable_doze(ad): 711 """Force the device into doze mode. 712 713 Args: 714 ad: android device object. 715 716 Returns: 717 True if device is in doze mode. 718 False otherwise. 719 """ 720 ad.adb.shell("dumpsys battery unplug") 721 ad.adb.shell("dumpsys deviceidle enable") 722 ad.adb.shell("dumpsys deviceidle force-idle") 723 ad.droid.goToSleepNow() 724 time.sleep(5) 725 adb_shell_result = ad.adb.shell("dumpsys deviceidle get deep") 726 if not adb_shell_result.startswith(DozeModeStatus.IDLE): 727 info = ("dumpsys deviceidle get deep: {}".format(adb_shell_result)) 728 print(info) 729 return False 730 return True 731 732 733def disable_doze(ad): 734 """Force the device not in doze mode. 735 736 Args: 737 ad: android device object. 738 739 Returns: 740 True if device is not in doze mode. 741 False otherwise. 742 """ 743 ad.adb.shell("dumpsys deviceidle disable") 744 ad.adb.shell("dumpsys battery reset") 745 adb_shell_result = ad.adb.shell("dumpsys deviceidle get deep") 746 if not adb_shell_result.startswith(DozeModeStatus.ACTIVE): 747 info = ("dumpsys deviceidle get deep: {}".format(adb_shell_result)) 748 print(info) 749 return False 750 return True 751 752 753def enable_doze_light(ad): 754 """Force the device into doze light mode. 755 756 Args: 757 ad: android device object. 758 759 Returns: 760 True if device is in doze light mode. 761 False otherwise. 762 """ 763 ad.adb.shell("dumpsys battery unplug") 764 ad.droid.goToSleepNow() 765 time.sleep(5) 766 ad.adb.shell("cmd deviceidle enable light") 767 ad.adb.shell("cmd deviceidle step light") 768 adb_shell_result = ad.adb.shell("dumpsys deviceidle get light") 769 if not adb_shell_result.startswith(DozeModeStatus.IDLE): 770 info = ("dumpsys deviceidle get light: {}".format(adb_shell_result)) 771 print(info) 772 return False 773 return True 774 775 776def disable_doze_light(ad): 777 """Force the device not in doze light mode. 778 779 Args: 780 ad: android device object. 781 782 Returns: 783 True if device is not in doze light mode. 784 False otherwise. 785 """ 786 ad.adb.shell("dumpsys battery reset") 787 ad.adb.shell("cmd deviceidle disable light") 788 adb_shell_result = ad.adb.shell("dumpsys deviceidle get light") 789 if not adb_shell_result.startswith(DozeModeStatus.ACTIVE): 790 info = ("dumpsys deviceidle get light: {}".format(adb_shell_result)) 791 print(info) 792 return False 793 return True 794 795 796def set_ambient_display(ad, new_state): 797 """Set "Ambient Display" in Settings->Display 798 799 Args: 800 ad: android device object. 801 new_state: new state for "Ambient Display". True or False. 802 """ 803 ad.adb.shell( 804 "settings put secure doze_enabled {}".format(1 if new_state else 0)) 805 806 807def set_adaptive_brightness(ad, new_state): 808 """Set "Adaptive Brightness" in Settings->Display 809 810 Args: 811 ad: android device object. 812 new_state: new state for "Adaptive Brightness". True or False. 813 """ 814 ad.adb.shell("settings put system screen_brightness_mode {}".format( 815 1 if new_state else 0)) 816 817 818def set_auto_rotate(ad, new_state): 819 """Set "Auto-rotate" in QuickSetting 820 821 Args: 822 ad: android device object. 823 new_state: new state for "Auto-rotate". True or False. 824 """ 825 ad.adb.shell("settings put system accelerometer_rotation {}".format( 826 1 if new_state else 0)) 827 828 829def set_location_service(ad, new_state): 830 """Set Location service on/off in Settings->Location 831 832 Args: 833 ad: android device object. 834 new_state: new state for "Location service". 835 If new_state is False, turn off location service. 836 If new_state if True, set location service to "High accuracy". 837 """ 838 ad.adb.shell("content insert --uri " 839 " content://com.google.settings/partner --bind " 840 "name:s:network_location_opt_in --bind value:s:1") 841 ad.adb.shell("content insert --uri " 842 " content://com.google.settings/partner --bind " 843 "name:s:use_location_for_services --bind value:s:1") 844 if new_state: 845 ad.adb.shell("settings put secure location_mode 3") 846 else: 847 ad.adb.shell("settings put secure location_mode 0") 848 849 850def set_mobile_data_always_on(ad, new_state): 851 """Set Mobile_Data_Always_On feature bit 852 853 Args: 854 ad: android device object. 855 new_state: new state for "mobile_data_always_on" 856 if new_state is False, set mobile_data_always_on disabled. 857 if new_state if True, set mobile_data_always_on enabled. 858 """ 859 ad.adb.shell("settings put global mobile_data_always_on {}".format( 860 1 if new_state else 0)) 861 862 863def bypass_setup_wizard(ad): 864 """Bypass the setup wizard on an input Android device 865 866 Args: 867 ad: android device object. 868 869 Returns: 870 True if Android device successfully bypassed the setup wizard. 871 False if failed. 872 """ 873 try: 874 ad.adb.shell("am start -n \"com.google.android.setupwizard/" 875 ".SetupWizardExitActivity\"") 876 logging.debug("No error during default bypass call.") 877 except AdbError as adb_error: 878 if adb_error.stdout == "ADB_CMD_OUTPUT:0": 879 if adb_error.stderr and \ 880 not adb_error.stderr.startswith("Error type 3\n"): 881 logging.error("ADB_CMD_OUTPUT:0, but error is %s " % 882 adb_error.stderr) 883 raise adb_error 884 logging.debug("Bypass wizard call received harmless error 3: " 885 "No setup to bypass.") 886 elif adb_error.stdout == "ADB_CMD_OUTPUT:255": 887 # Run it again as root. 888 ad.adb.root_adb() 889 logging.debug("Need root access to bypass setup wizard.") 890 try: 891 ad.adb.shell("am start -n \"com.google.android.setupwizard/" 892 ".SetupWizardExitActivity\"") 893 logging.debug("No error during rooted bypass call.") 894 except AdbError as adb_error: 895 if adb_error.stdout == "ADB_CMD_OUTPUT:0": 896 if adb_error.stderr and \ 897 not adb_error.stderr.startswith("Error type 3\n"): 898 logging.error("Rooted ADB_CMD_OUTPUT:0, but error is " 899 "%s " % adb_error.stderr) 900 raise adb_error 901 logging.debug( 902 "Rooted bypass wizard call received harmless " 903 "error 3: No setup to bypass.") 904 905 # magical sleep to wait for the gservices override broadcast to complete 906 time.sleep(3) 907 908 provisioned_state = int( 909 ad.adb.shell("settings get global device_provisioned")) 910 if provisioned_state != 1: 911 logging.error("Failed to bypass setup wizard.") 912 return False 913 logging.debug("Setup wizard successfully bypassed.") 914 return True 915 916 917def parse_ping_ouput(ad, count, out, loss_tolerance=20): 918 """Ping Parsing util. 919 920 Args: 921 ad: Android Device Object. 922 count: Number of ICMP packets sent 923 out: shell output text of ping operation 924 loss_tolerance: Threshold after which flag test as false 925 Returns: 926 False: if packet loss is more than loss_tolerance% 927 True: if all good 928 """ 929 result = re.search( 930 r"(\d+) packets transmitted, (\d+) received, (\d+)% packet loss", out) 931 if not result: 932 ad.log.info("Ping failed with %s", out) 933 return False 934 935 packet_loss = int(result.group(3)) 936 packet_xmit = int(result.group(1)) 937 packet_rcvd = int(result.group(2)) 938 min_packet_xmit_rcvd = (100 - loss_tolerance) * 0.01 939 if (packet_loss > loss_tolerance 940 or packet_xmit < count * min_packet_xmit_rcvd 941 or packet_rcvd < count * min_packet_xmit_rcvd): 942 ad.log.error("%s, ping failed with loss more than tolerance %s%%", 943 result.group(0), loss_tolerance) 944 return False 945 ad.log.info("Ping succeed with %s", result.group(0)) 946 return True 947 948 949def adb_shell_ping(ad, 950 count=120, 951 dest_ip="www.google.com", 952 timeout=200, 953 loss_tolerance=20): 954 """Ping utility using adb shell. 955 956 Args: 957 ad: Android Device Object. 958 count: Number of ICMP packets to send 959 dest_ip: hostname or IP address 960 default www.google.com 961 timeout: timeout for icmp pings to complete. 962 """ 963 ping_cmd = "ping -W 1" 964 if count: 965 ping_cmd += " -c %d" % count 966 if dest_ip: 967 ping_cmd += " %s" % dest_ip 968 try: 969 ad.log.info("Starting ping test to %s using adb command %s", dest_ip, 970 ping_cmd) 971 out = ad.adb.shell(ping_cmd, timeout=timeout, ignore_status=True) 972 if not parse_ping_ouput(ad, count, out, loss_tolerance): 973 return False 974 return True 975 except Exception as e: 976 ad.log.warning("Ping Test to %s failed with exception %s", dest_ip, e) 977 return False 978 979 980def zip_directory(zip_name, src_dir): 981 """Compress a directory to a .zip file. 982 983 This implementation is thread-safe. 984 985 Args: 986 zip_name: str, name of the generated archive 987 src_dir: str, path to the source directory 988 """ 989 with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zip: 990 for root, dirs, files in os.walk(src_dir): 991 for file in files: 992 path = os.path.join(root, file) 993 zip.write(path, os.path.relpath(path, src_dir)) 994 995 996def unzip_maintain_permissions(zip_path, extract_location): 997 """Unzip a .zip file while maintaining permissions. 998 999 Args: 1000 zip_path: The path to the zipped file. 1001 extract_location: the directory to extract to. 1002 """ 1003 with zipfile.ZipFile(zip_path, 'r') as zip_file: 1004 for info in zip_file.infolist(): 1005 _extract_file(zip_file, info, extract_location) 1006 1007 1008def _extract_file(zip_file, zip_info, extract_location): 1009 """Extracts a single entry from a ZipFile while maintaining permissions. 1010 1011 Args: 1012 zip_file: A zipfile.ZipFile. 1013 zip_info: A ZipInfo object from zip_file. 1014 extract_location: The directory to extract to. 1015 """ 1016 out_path = zip_file.extract(zip_info.filename, path=extract_location) 1017 perm = zip_info.external_attr >> 16 1018 os.chmod(out_path, perm) 1019 1020 1021def get_directory_size(path): 1022 """Computes the total size of the files in a directory, including subdirectories. 1023 1024 Args: 1025 path: The path of the directory. 1026 Returns: 1027 The size of the provided directory. 1028 """ 1029 total = 0 1030 for dirpath, dirnames, filenames in os.walk(path): 1031 for filename in filenames: 1032 total += os.path.getsize(os.path.join(dirpath, filename)) 1033 return total 1034 1035 1036def get_command_uptime(command_regex): 1037 """Returns the uptime for a given command. 1038 1039 Args: 1040 command_regex: A regex that matches the command line given. Must be 1041 pgrep compatible. 1042 """ 1043 pid = job.run('pgrep -f %s' % command_regex).stdout 1044 runtime = '' 1045 if pid: 1046 runtime = job.run('ps -o etime= -p "%s"' % pid).stdout 1047 return runtime 1048 1049 1050def get_process_uptime(process): 1051 """Returns the runtime in [[dd-]hh:]mm:ss, or '' if not running.""" 1052 pid = job.run('pidof %s' % process, ignore_status=True).stdout 1053 runtime = '' 1054 if pid: 1055 runtime = job.run('ps -o etime= -p "%s"' % pid).stdout 1056 return runtime 1057 1058 1059def get_device_process_uptime(adb, process): 1060 """Returns the uptime of a device process.""" 1061 pid = adb.shell('pidof %s' % process, ignore_status=True) 1062 runtime = '' 1063 if pid: 1064 runtime = adb.shell('ps -o etime= -p "%s"' % pid) 1065 return runtime 1066 1067 1068def wait_until(func, timeout_s, condition=True, sleep_s=1.0): 1069 """Executes a function repeatedly until condition is met. 1070 1071 Args: 1072 func: The function pointer to execute. 1073 timeout_s: Amount of time (in seconds) to wait before raising an 1074 exception. 1075 condition: The ending condition of the WaitUntil loop. 1076 sleep_s: The amount of time (in seconds) to sleep between each function 1077 execution. 1078 1079 Returns: 1080 The time in seconds before detecting a successful condition. 1081 1082 Raises: 1083 TimeoutError: If the condition was never met and timeout is hit. 1084 """ 1085 start_time = time.time() 1086 end_time = start_time + timeout_s 1087 count = 0 1088 while True: 1089 count += 1 1090 if func() == condition: 1091 return time.time() - start_time 1092 if time.time() > end_time: 1093 break 1094 time.sleep(sleep_s) 1095 raise TimeoutError('Failed to complete function %s in %d seconds having ' 1096 'attempted %d times.' % (str(func), timeout_s, count)) 1097 1098 1099# Adapted from 1100# https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Python 1101# Available under the Creative Commons Attribution-ShareAlike License 1102def levenshtein(string1, string2): 1103 """Returns the Levenshtein distance of two strings. 1104 Uses Dynamic Programming approach, only keeping track of 1105 two rows of the DP table at a time. 1106 1107 Args: 1108 string1: String to compare to string2 1109 string2: String to compare to string1 1110 1111 Returns: 1112 distance: the Levenshtein distance between string1 and string2 1113 """ 1114 1115 if len(string1) < len(string2): 1116 return levenshtein(string2, string1) 1117 1118 if len(string2) == 0: 1119 return len(string1) 1120 1121 previous_row = range(len(string2) + 1) 1122 for i, char1 in enumerate(string1): 1123 current_row = [i + 1] 1124 for j, char2 in enumerate(string2): 1125 insertions = previous_row[j + 1] + 1 1126 deletions = current_row[j] + 1 1127 substitutions = previous_row[j] + (char1 != char2) 1128 current_row.append(min(insertions, deletions, substitutions)) 1129 previous_row = current_row 1130 1131 return previous_row[-1] 1132 1133 1134def string_similarity(s1, s2): 1135 """Returns a similarity measurement based on Levenshtein distance. 1136 1137 Args: 1138 s1: the string to compare to s2 1139 s2: the string to compare to s1 1140 1141 Returns: 1142 result: the similarity metric 1143 """ 1144 lev = levenshtein(s1, s2) 1145 try: 1146 lev_ratio = float(lev) / max(len(s1), len(s2)) 1147 result = (1.0 - lev_ratio) * 100 1148 except ZeroDivisionError: 1149 result = 100 if not s2 else 0 1150 return float(result) 1151 1152 1153def run_concurrent_actions_no_raise(*calls): 1154 """Concurrently runs all callables passed in using multithreading. 1155 1156 Example: 1157 1158 >>> def test_function_1(arg1, arg2): 1159 >>> return arg1, arg2 1160 >>> 1161 >>> def test_function_2(arg1, kwarg='kwarg'): 1162 >>> raise arg1(kwarg) 1163 >>> 1164 >>> run_concurrent_actions_no_raise( 1165 >>> lambda: test_function_1('arg1', 'arg2'), 1166 >>> lambda: test_function_2(IndexError, kwarg='kwarg'), 1167 >>> ) 1168 >>> # Output: 1169 >>> [('arg1', 'arg2'), IndexError('kwarg')] 1170 1171 Args: 1172 *calls: A *args list of argumentless callable objects to be called. Note 1173 that if a function has arguments it can be turned into an 1174 argumentless function via the lambda keyword or functools.partial. 1175 1176 Returns: 1177 An array of the returned values or exceptions received from calls, 1178 respective of the order given. 1179 """ 1180 with ThreadPoolExecutor(max_workers=len(calls)) as executor: 1181 futures = [executor.submit(call) for call in calls] 1182 1183 results = [] 1184 for future in futures: 1185 try: 1186 results.append(future.result()) 1187 except Exception as e: 1188 results.append(e) 1189 return results 1190 1191 1192def run_concurrent_actions(*calls): 1193 """Runs all callables passed in concurrently using multithreading. 1194 1195 Examples: 1196 1197 >>> def test_function_1(arg1, arg2): 1198 >>> print(arg1, arg2) 1199 >>> 1200 >>> def test_function_2(arg1, kwarg='kwarg'): 1201 >>> raise arg1(kwarg) 1202 >>> 1203 >>> run_concurrent_actions( 1204 >>> lambda: test_function_1('arg1', 'arg2'), 1205 >>> lambda: test_function_2(IndexError, kwarg='kwarg'), 1206 >>> ) 1207 >>> 'The above line raises IndexError("kwarg")' 1208 1209 Args: 1210 *calls: A *args list of argumentless callable objects to be called. Note 1211 that if a function has arguments it can be turned into an 1212 argumentless function via the lambda keyword or functools.partial. 1213 1214 Returns: 1215 An array of the returned values respective of the order of the calls 1216 argument. 1217 1218 Raises: 1219 If an exception is raised in any of the calls, the first exception 1220 caught will be raised. 1221 """ 1222 first_exception = None 1223 1224 class WrappedException(Exception): 1225 """Raised when a passed-in callable raises an exception.""" 1226 1227 def call_wrapper(call): 1228 nonlocal first_exception 1229 1230 try: 1231 return call() 1232 except Exception as e: 1233 logging.exception(e) 1234 # Note that there is a potential race condition between two 1235 # exceptions setting first_exception. Even if a locking mechanism 1236 # was added to prevent this from happening, it is still possible 1237 # that we capture the second exception as the first exception, as 1238 # the active thread can swap to the thread that raises the second 1239 # exception. There is no way to solve this with the tools we have 1240 # here, so we do not bother. The effects this issue has on the 1241 # system as a whole are negligible. 1242 if first_exception is None: 1243 first_exception = e 1244 raise WrappedException(e) 1245 1246 with ThreadPoolExecutor(max_workers=len(calls)) as executor: 1247 futures = [executor.submit(call_wrapper, call) for call in calls] 1248 1249 results = [] 1250 for future in futures: 1251 try: 1252 results.append(future.result()) 1253 except WrappedException: 1254 # We do not need to raise here, since first_exception will already 1255 # be set to the first exception raised by these callables. 1256 break 1257 1258 if first_exception: 1259 raise first_exception 1260 1261 return results 1262 1263 1264def test_concurrent_actions(*calls, failure_exceptions=(Exception, )): 1265 """Concurrently runs all passed in calls using multithreading. 1266 1267 If any callable raises an Exception found within failure_exceptions, the 1268 test case is marked as a failure. 1269 1270 Example: 1271 >>> def test_function_1(arg1, arg2): 1272 >>> print(arg1, arg2) 1273 >>> 1274 >>> def test_function_2(kwarg='kwarg'): 1275 >>> raise IndexError(kwarg) 1276 >>> 1277 >>> test_concurrent_actions( 1278 >>> lambda: test_function_1('arg1', 'arg2'), 1279 >>> lambda: test_function_2(kwarg='kwarg'), 1280 >>> failure_exceptions=IndexError 1281 >>> ) 1282 >>> 'raises signals.TestFailure due to IndexError being raised.' 1283 1284 Args: 1285 *calls: A *args list of argumentless callable objects to be called. Note 1286 that if a function has arguments it can be turned into an 1287 argumentless function via the lambda keyword or functools.partial. 1288 failure_exceptions: A tuple of all possible Exceptions that will mark 1289 the test as a FAILURE. Any exception that is not in this list will 1290 mark the tests as UNKNOWN. 1291 1292 Returns: 1293 An array of the returned values respective of the order of the calls 1294 argument. 1295 1296 Raises: 1297 signals.TestFailure if any call raises an Exception. 1298 """ 1299 try: 1300 return run_concurrent_actions(*calls) 1301 except signals.TestFailure: 1302 # Do not modify incoming test failures 1303 raise 1304 except failure_exceptions as e: 1305 raise signals.TestFailure(e) 1306 1307 1308class SuppressLogOutput(object): 1309 """Context manager used to suppress all logging output for the specified 1310 logger and level(s). 1311 """ 1312 1313 def __init__(self, logger=logging.getLogger(), log_levels=None): 1314 """Create a SuppressLogOutput context manager 1315 1316 Args: 1317 logger: The logger object to suppress 1318 log_levels: Levels of log handlers to disable. 1319 """ 1320 1321 self._logger = logger 1322 self._log_levels = log_levels or [ 1323 logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, 1324 logging.CRITICAL 1325 ] 1326 if isinstance(self._log_levels, int): 1327 self._log_levels = [self._log_levels] 1328 self._handlers = copy.copy(self._logger.handlers) 1329 1330 def __enter__(self): 1331 for handler in self._handlers: 1332 if handler.level in self._log_levels: 1333 self._logger.removeHandler(handler) 1334 return self 1335 1336 def __exit__(self, *_): 1337 for handler in self._handlers: 1338 self._logger.addHandler(handler) 1339 1340 1341class BlockingTimer(object): 1342 """Context manager used to block until a specified amount of time has 1343 elapsed. 1344 """ 1345 1346 def __init__(self, secs): 1347 """Initializes a BlockingTimer 1348 1349 Args: 1350 secs: Number of seconds to wait before exiting 1351 """ 1352 self._thread = threading.Timer(secs, lambda: None) 1353 1354 def __enter__(self): 1355 self._thread.start() 1356 return self 1357 1358 def __exit__(self, *_): 1359 self._thread.join() 1360 1361 1362def is_valid_ipv4_address(address): 1363 try: 1364 socket.inet_pton(socket.AF_INET, address) 1365 except AttributeError: # no inet_pton here, sorry 1366 try: 1367 socket.inet_aton(address) 1368 except socket.error: 1369 return False 1370 return address.count('.') == 3 1371 except socket.error: # not a valid address 1372 return False 1373 1374 return True 1375 1376 1377def is_valid_ipv6_address(address): 1378 if '%' in address: 1379 address = address.split('%')[0] 1380 try: 1381 socket.inet_pton(socket.AF_INET6, address) 1382 except socket.error: # not a valid address 1383 return False 1384 return True 1385 1386 1387def merge_dicts(*dict_args): 1388 """ Merges args list of dictionaries into a single dictionary. 1389 1390 Args: 1391 dict_args: an args list of dictionaries to be merged. If multiple 1392 dictionaries share a key, the last in the list will appear in the 1393 final result. 1394 """ 1395 result = {} 1396 for dictionary in dict_args: 1397 result.update(dictionary) 1398 return result 1399 1400 1401def ascii_string(uc_string): 1402 """Converts unicode string to ascii""" 1403 return str(uc_string).encode('ASCII') 1404 1405 1406def get_interface_ip_addresses(comm_channel, interface): 1407 """Gets all of the ip addresses, ipv4 and ipv6, associated with a 1408 particular interface name. 1409 1410 Args: 1411 comm_channel: How to send commands to a device. Can be ssh, adb serial, 1412 etc. Must have the run function implemented. 1413 interface: The interface name on the device, ie eth0 1414 1415 Returns: 1416 A list of dictionaries of the the various IP addresses: 1417 ipv4_private: Any 192.168, 172.16, 10, or 169.254 addresses 1418 ipv4_public: Any IPv4 public addresses 1419 ipv6_link_local: Any fe80:: addresses 1420 ipv6_private_local: Any fd00:: addresses 1421 ipv6_public: Any publicly routable addresses 1422 """ 1423 # Local imports are used here to prevent cyclic dependency. 1424 from acts.controllers.android_device import AndroidDevice 1425 from acts.controllers.fuchsia_device import FuchsiaDevice 1426 from acts.controllers.utils_lib.ssh.connection import SshConnection 1427 1428 is_local = comm_channel == job 1429 if type(comm_channel) is AndroidDevice: 1430 addrs = comm_channel.adb.shell( 1431 f'ip -o addr show {interface} | awk \'{{gsub("/", " "); print $4}}\'' 1432 ).splitlines() 1433 elif (type(comm_channel) is SshConnection or is_local): 1434 addrs = comm_channel.run( 1435 f'ip -o addr show {interface} | awk \'{{gsub("/", " "); print $4}}\'' 1436 ).stdout.splitlines() 1437 elif type(comm_channel) is FuchsiaDevice: 1438 interfaces = comm_channel.sl4f.netstack_lib.netstackListInterfaces() 1439 err = interfaces.get('error') 1440 if err is not None: 1441 raise ActsUtilsError(f'Failed get_interface_ip_addresses: {err}') 1442 addrs = [] 1443 for item in interfaces.get('result'): 1444 if item['name'] != interface: 1445 continue 1446 for ipv4_address in item['ipv4_addresses']: 1447 ipv4_address = '.'.join(map(str, ipv4_address)) 1448 addrs.append(ipv4_address) 1449 for ipv6_address in item['ipv6_addresses']: 1450 converted_ipv6_address = [] 1451 for octet in ipv6_address: 1452 converted_ipv6_address.append(format(octet, 'x').zfill(2)) 1453 ipv6_address = ''.join(converted_ipv6_address) 1454 ipv6_address = (':'.join( 1455 ipv6_address[i:i + 4] 1456 for i in range(0, len(ipv6_address), 4))) 1457 addrs.append(str(ipaddress.ip_address(ipv6_address))) 1458 else: 1459 raise ValueError('Unsupported method to send command to device.') 1460 1461 ipv4_private_local_addresses = [] 1462 ipv4_public_addresses = [] 1463 ipv6_link_local_addresses = [] 1464 ipv6_private_local_addresses = [] 1465 ipv6_public_addresses = [] 1466 1467 for addr in addrs: 1468 on_device_ip = ipaddress.ip_address(addr) 1469 if on_device_ip.version == 4: 1470 if on_device_ip.is_private: 1471 ipv4_private_local_addresses.append(str(on_device_ip)) 1472 elif on_device_ip.is_global or ( 1473 # Carrier private doesn't have a property, so we check if 1474 # all other values are left unset. 1475 not on_device_ip.is_reserved 1476 and not on_device_ip.is_unspecified 1477 and not on_device_ip.is_link_local 1478 and not on_device_ip.is_loopback 1479 and not on_device_ip.is_multicast): 1480 ipv4_public_addresses.append(str(on_device_ip)) 1481 elif on_device_ip.version == 6: 1482 if on_device_ip.is_link_local: 1483 ipv6_link_local_addresses.append(str(on_device_ip)) 1484 elif on_device_ip.is_private: 1485 ipv6_private_local_addresses.append(str(on_device_ip)) 1486 elif on_device_ip.is_global: 1487 ipv6_public_addresses.append(str(on_device_ip)) 1488 1489 return { 1490 'ipv4_private': ipv4_private_local_addresses, 1491 'ipv4_public': ipv4_public_addresses, 1492 'ipv6_link_local': ipv6_link_local_addresses, 1493 'ipv6_private_local': ipv6_private_local_addresses, 1494 'ipv6_public': ipv6_public_addresses 1495 } 1496 1497 1498class AddressTimeout(signals.TestError): 1499 pass 1500 1501 1502class MultipleAddresses(signals.TestError): 1503 pass 1504 1505 1506def get_addr(comm_channel, 1507 interface, 1508 addr_type='ipv4_private', 1509 timeout_sec=None): 1510 """Get the requested type of IP address for an interface; if an address is 1511 not available, retry until the timeout has been reached. 1512 1513 Args: 1514 addr_type: Type of address to get as defined by the return value of 1515 utils.get_interface_ip_addresses. 1516 timeout_sec: Seconds to wait to acquire an address if there isn't one 1517 already available. If fetching an IPv4 address, the default is 3 1518 seconds. If IPv6, the default is 30 seconds for Duplicate Address 1519 Detection. 1520 1521 Returns: 1522 A string containing the requested address. 1523 1524 Raises: 1525 TestAbortClass: timeout_sec is None and invalid addr_type 1526 AddressTimeout: No address is available after timeout_sec 1527 MultipleAddresses: Several addresses are available 1528 """ 1529 if not timeout_sec: 1530 if 'ipv4' in addr_type: 1531 timeout_sec = 3 1532 elif 'ipv6' in addr_type: 1533 timeout_sec = DAD_TIMEOUT_SEC 1534 else: 1535 raise signals.TestAbortClass(f'Unknown addr_type "{addr_type}"') 1536 1537 start = time.time() 1538 elapsed = 0 1539 1540 while elapsed <= timeout_sec: 1541 ip_addrs = get_interface_ip_addresses(comm_channel, 1542 interface)[addr_type] 1543 if len(ip_addrs) > 1: 1544 raise MultipleAddresses( 1545 f'Expected only one "{addr_type}" address, got {ip_addrs}') 1546 elif len(ip_addrs) == 1: 1547 return ip_addrs[0] 1548 elapsed = time.time() - start 1549 1550 raise AddressTimeout( 1551 f'No available "{addr_type}" address after {timeout_sec}s') 1552 1553 1554def get_interface_based_on_ip(comm_channel, desired_ip_address): 1555 """Gets the interface for a particular IP 1556 1557 Args: 1558 comm_channel: How to send commands to a device. Can be ssh, adb serial, 1559 etc. Must have the run function implemented. 1560 desired_ip_address: The IP address that is being looked for on a device. 1561 1562 Returns: 1563 The name of the test interface. 1564 """ 1565 1566 desired_ip_address = desired_ip_address.split('%', 1)[0] 1567 all_ips_and_interfaces = comm_channel.run( 1568 '(ip -o -4 addr show; ip -o -6 addr show) | ' 1569 'awk \'{print $2" "$4}\'').stdout 1570 for ip_address_and_interface in all_ips_and_interfaces.split('\n'): 1571 if desired_ip_address in ip_address_and_interface: 1572 return ip_address_and_interface.split()[1][:-1] 1573 return None 1574 1575 1576def renew_linux_ip_address(comm_channel, interface): 1577 comm_channel.run('sudo ip link set %s down' % interface) 1578 comm_channel.run('sudo ip link set %s up' % interface) 1579 comm_channel.run('sudo dhclient -r %s' % interface) 1580 comm_channel.run('sudo dhclient %s' % interface) 1581 1582 1583def get_ping_command(dest_ip, 1584 count=3, 1585 interval=1000, 1586 timeout=1000, 1587 size=56, 1588 os_type='Linux', 1589 additional_ping_params=None): 1590 """Builds ping command string based on address type, os, and params. 1591 1592 Args: 1593 dest_ip: string, address to ping (ipv4 or ipv6) 1594 count: int, number of requests to send 1595 interval: int, time in seconds between requests 1596 timeout: int, time in seconds to wait for response 1597 size: int, number of bytes to send, 1598 os_type: string, os type of the source device (supports 'Linux', 1599 'Darwin') 1600 additional_ping_params: string, command option flags to 1601 append to the command string 1602 1603 Returns: 1604 List of string, represetning the ping command. 1605 """ 1606 if is_valid_ipv4_address(dest_ip): 1607 ping_binary = 'ping' 1608 elif is_valid_ipv6_address(dest_ip): 1609 ping_binary = 'ping6' 1610 else: 1611 raise ValueError('Invalid ip addr: %s' % dest_ip) 1612 1613 if os_type == 'Darwin': 1614 if is_valid_ipv6_address(dest_ip): 1615 # ping6 on MacOS doesn't support timeout 1616 logging.debug( 1617 'Ignoring timeout, as ping6 on MacOS does not support it.') 1618 timeout_flag = [] 1619 else: 1620 timeout_flag = ['-t', str(timeout / 1000)] 1621 elif os_type == 'Linux': 1622 timeout_flag = ['-W', str(timeout / 1000)] 1623 else: 1624 raise ValueError('Invalid OS. Only Linux and MacOS are supported.') 1625 1626 if not additional_ping_params: 1627 additional_ping_params = '' 1628 1629 ping_cmd = [ 1630 ping_binary, *timeout_flag, '-c', 1631 str(count), '-i', 1632 str(interval / 1000), '-s', 1633 str(size), additional_ping_params, dest_ip 1634 ] 1635 return ' '.join(ping_cmd) 1636 1637 1638def ping(comm_channel, 1639 dest_ip, 1640 count=3, 1641 interval=1000, 1642 timeout=1000, 1643 size=56, 1644 additional_ping_params=None): 1645 """ Generic linux ping function, supports local (acts.libs.proc.job) and 1646 SshConnections (acts.libs.proc.job over ssh) to Linux based OSs and MacOS. 1647 1648 NOTES: This will work with Android over SSH, but does not function over ADB 1649 as that has a unique return format. 1650 1651 Args: 1652 comm_channel: communication channel over which to send ping command. 1653 Must have 'run' function that returns at least command, stdout, 1654 stderr, and exit_status (see acts.libs.proc.job) 1655 dest_ip: address to ping (ipv4 or ipv6) 1656 count: int, number of packets to send 1657 interval: int, time in milliseconds between pings 1658 timeout: int, time in milliseconds to wait for response 1659 size: int, size of packets in bytes 1660 additional_ping_params: string, command option flags to 1661 append to the command string 1662 1663 Returns: 1664 Dict containing: 1665 command: string 1666 exit_status: int (0 or 1) 1667 stdout: string 1668 stderr: string 1669 transmitted: int, number of packets transmitted 1670 received: int, number of packets received 1671 packet_loss: int, percentage packet loss 1672 time: int, time of ping command execution (in milliseconds) 1673 rtt_min: float, minimum round trip time 1674 rtt_avg: float, average round trip time 1675 rtt_max: float, maximum round trip time 1676 rtt_mdev: float, round trip time standard deviation 1677 1678 Any values that cannot be parsed are left as None 1679 """ 1680 from acts.controllers.utils_lib.ssh.connection import SshConnection 1681 is_local = comm_channel == job 1682 os_type = platform.system() if is_local else 'Linux' 1683 ping_cmd = get_ping_command(dest_ip, 1684 count=count, 1685 interval=interval, 1686 timeout=timeout, 1687 size=size, 1688 os_type=os_type, 1689 additional_ping_params=additional_ping_params) 1690 1691 if (type(comm_channel) is SshConnection or is_local): 1692 logging.debug( 1693 'Running ping with parameters (count: %s, interval: %s, timeout: ' 1694 '%s, size: %s)' % (count, interval, timeout, size)) 1695 ping_result = comm_channel.run(ping_cmd, ignore_status=True) 1696 else: 1697 raise ValueError('Unsupported comm_channel: %s' % type(comm_channel)) 1698 1699 if isinstance(ping_result, job.Error): 1700 ping_result = ping_result.result 1701 1702 transmitted = None 1703 received = None 1704 packet_loss = None 1705 time = None 1706 rtt_min = None 1707 rtt_avg = None 1708 rtt_max = None 1709 rtt_mdev = None 1710 1711 summary = re.search( 1712 '([0-9]+) packets transmitted.*?([0-9]+) received.*?([0-9]+)% packet ' 1713 'loss.*?time ([0-9]+)', ping_result.stdout) 1714 if summary: 1715 transmitted = summary[1] 1716 received = summary[2] 1717 packet_loss = summary[3] 1718 time = summary[4] 1719 1720 rtt_stats = re.search('= ([0-9.]+)/([0-9.]+)/([0-9.]+)/([0-9.]+)', 1721 ping_result.stdout) 1722 if rtt_stats: 1723 rtt_min = rtt_stats[1] 1724 rtt_avg = rtt_stats[2] 1725 rtt_max = rtt_stats[3] 1726 rtt_mdev = rtt_stats[4] 1727 1728 return { 1729 'command': ping_result.command, 1730 'exit_status': ping_result.exit_status, 1731 'stdout': ping_result.stdout, 1732 'stderr': ping_result.stderr, 1733 'transmitted': transmitted, 1734 'received': received, 1735 'packet_loss': packet_loss, 1736 'time': time, 1737 'rtt_min': rtt_min, 1738 'rtt_avg': rtt_avg, 1739 'rtt_max': rtt_max, 1740 'rtt_mdev': rtt_mdev 1741 } 1742 1743 1744def can_ping(comm_channel, 1745 dest_ip, 1746 count=3, 1747 interval=1000, 1748 timeout=1000, 1749 size=56, 1750 additional_ping_params=None): 1751 """Returns whether device connected via comm_channel can ping a dest 1752 address""" 1753 ping_results = ping(comm_channel, 1754 dest_ip, 1755 count=count, 1756 interval=interval, 1757 timeout=timeout, 1758 size=size, 1759 additional_ping_params=additional_ping_params) 1760 1761 return ping_results['exit_status'] == 0 1762 1763 1764def ip_in_subnet(ip, subnet): 1765 """Validate that ip is in a given subnet. 1766 1767 Args: 1768 ip: string, ip address to verify (eg. '192.168.42.158') 1769 subnet: string, subnet to check (eg. '192.168.42.0/24') 1770 1771 Returns: 1772 True, if ip in subnet, else False 1773 """ 1774 return ipaddress.ip_address(ip) in ipaddress.ip_network(subnet) 1775 1776 1777def mac_address_str_to_list(mac_addr_str): 1778 """Converts mac address string to list of decimal octets. 1779 1780 Args: 1781 mac_addr_string: string, mac address 1782 e.g. '12:34:56:78:9a:bc' 1783 1784 Returns 1785 list, representing mac address octets in decimal 1786 e.g. [18, 52, 86, 120, 154, 188] 1787 """ 1788 return [int(octet, 16) for octet in mac_addr_str.split(':')] 1789 1790 1791def mac_address_list_to_str(mac_addr_list): 1792 """Converts list of decimal octets represeting mac address to string. 1793 1794 Args: 1795 mac_addr_list: list, representing mac address octets in decimal 1796 e.g. [18, 52, 86, 120, 154, 188] 1797 1798 Returns: 1799 string, mac address 1800 e.g. '12:34:56:78:9a:bc' 1801 """ 1802 hex_list = [] 1803 for octet in mac_addr_list: 1804 hex_octet = hex(octet)[2:] 1805 if octet < 16: 1806 hex_list.append('0%s' % hex_octet) 1807 else: 1808 hex_list.append(hex_octet) 1809 1810 return ':'.join(hex_list) 1811 1812 1813def get_fuchsia_mdns_ipv6_address(device_mdns_name): 1814 """Finds the IPv6 link-local address of a Fuchsia device matching a mDNS 1815 name. 1816 1817 Args: 1818 device_mdns_name: name of Fuchsia device (e.g. gig-clone-sugar-slash) 1819 1820 Returns: 1821 string, IPv6 link-local address 1822 """ 1823 from zeroconf import IPVersion, Zeroconf 1824 1825 if not device_mdns_name: 1826 return None 1827 1828 def mdns_query(interface, address): 1829 logging.info( 1830 f'Sending mDNS query for device "{device_mdns_name}" using "{address}"' 1831 ) 1832 try: 1833 zeroconf = Zeroconf(ip_version=IPVersion.V6Only, 1834 interfaces=[address]) 1835 except RuntimeError as e: 1836 if 'No adapter found for IP address' in e.args[0]: 1837 # Most likely, a device went offline and its control 1838 # interface was deleted. This is acceptable since the 1839 # device that went offline isn't guaranteed to be the 1840 # device we're searching for. 1841 logging.warning('No adapter found for "%s"' % address) 1842 return None 1843 raise 1844 1845 device_records = zeroconf.get_service_info( 1846 FUCHSIA_MDNS_TYPE, device_mdns_name + '.' + FUCHSIA_MDNS_TYPE) 1847 1848 if device_records: 1849 for device_address in device_records.parsed_addresses(): 1850 device_ip_address = ipaddress.ip_address(device_address) 1851 scoped_address = '%s%%%s' % (device_address, interface) 1852 if (device_ip_address.version == 6 1853 and device_ip_address.is_link_local 1854 and can_ping(job, dest_ip=scoped_address)): 1855 logging.info('Found device "%s" at "%s"' % 1856 (device_mdns_name, scoped_address)) 1857 zeroconf.close() 1858 del zeroconf 1859 return scoped_address 1860 1861 zeroconf.close() 1862 del zeroconf 1863 return None 1864 1865 with ThreadPoolExecutor() as executor: 1866 futures = [] 1867 1868 interfaces = psutil.net_if_addrs() 1869 for interface in interfaces: 1870 for addr in interfaces[interface]: 1871 address = addr.address.split('%')[0] 1872 if addr.family == socket.AF_INET6 and ipaddress.ip_address( 1873 address).is_link_local and address != 'fe80::1': 1874 futures.append( 1875 executor.submit(mdns_query, interface, address)) 1876 1877 for future in futures: 1878 addr = future.result() 1879 if addr: 1880 return addr 1881 1882 logging.error('Unable to find IP address for device "%s"' % 1883 device_mdns_name) 1884 return None 1885 1886 1887def get_device(devices, device_type): 1888 """Finds a unique device with the specified "device_type" attribute from a 1889 list. If none is found, defaults to the first device in the list. 1890 1891 Example: 1892 get_device(android_devices, device_type="DUT") 1893 get_device(fuchsia_devices, device_type="DUT") 1894 get_device(android_devices + fuchsia_devices, device_type="DUT") 1895 1896 Args: 1897 devices: A list of device controller objects. 1898 device_type: (string) Type of device to find, specified by the 1899 "device_type" attribute. 1900 1901 Returns: 1902 The matching device controller object, or the first device in the list 1903 if not found. 1904 1905 Raises: 1906 ValueError is raised if none or more than one device is 1907 matched. 1908 """ 1909 if not devices: 1910 raise ValueError('No devices available') 1911 1912 matches = [ 1913 d for d in devices 1914 if hasattr(d, 'device_type') and d.device_type == device_type 1915 ] 1916 1917 if len(matches) == 0: 1918 # No matches for the specified "device_type", use the first device 1919 # declared. 1920 return devices[0] 1921 if len(matches) > 1: 1922 # Specifing multiple devices with the same "device_type" is a 1923 # configuration error. 1924 raise ValueError( 1925 'More than one device matching "device_type" == "{}"'.format( 1926 device_type)) 1927 1928 return matches[0] 1929