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