1 # 2 # Copyright (C) 2016 The Android Open Source Project 3 # 4 # Licensed under the Apache License, Version 2.0 (the "License"); 5 # you may not use this file except in compliance with the License. 6 # You may obtain a copy of the License at 7 # 8 # http://www.apache.org/licenses/LICENSE-2.0 9 # 10 # Unless required by applicable law or agreed to in writing, software 11 # distributed under the License is distributed on an "AS IS" BASIS, 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 # See the License for the specific language governing permissions and 14 # limitations under the License. 15 # 16 17 import base64 18 import concurrent.futures 19 import datetime 20 import functools 21 import json 22 import logging 23 import os 24 import random 25 import re 26 import signal 27 import string 28 import subprocess 29 import time 30 import traceback 31 32 # File name length is limited to 255 chars on some OS, so we need to make sure 33 # the file names we output fits within the limit. 34 MAX_FILENAME_LEN = 255 35 # Path length is limited to 4096 chars on some OS, so we need to make sure 36 # the path we output fits within the limit. 37 MAX_PATH_LEN = 4096 38 39 40 class VTSUtilsError(Exception): 41 """Generic error raised for exceptions in ACTS utils.""" 42 43 44 class NexusModelNames: 45 # TODO(angli): This will be fixed later by angli. 46 ONE = 'sprout' 47 N5 = 'hammerhead' 48 N5v2 = 'bullhead' 49 N6 = 'shamu' 50 N6v2 = 'angler' 51 52 53 ascii_letters_and_digits = string.ascii_letters + string.digits 54 valid_filename_chars = "-_." + ascii_letters_and_digits 55 56 models = ("sprout", "occam", "hammerhead", "bullhead", "razor", "razorg", 57 "shamu", "angler", "volantis", "volantisg", "mantaray", "fugu", 58 "ryu") 59 60 manufacture_name_to_model = { 61 "flo": "razor", 62 "flo_lte": "razorg", 63 "flounder": "volantis", 64 "flounder_lte": "volantisg", 65 "dragon": "ryu" 66 } 67 68 GMT_to_olson = { 69 "GMT-9": "America/Anchorage", 70 "GMT-8": "US/Pacific", 71 "GMT-7": "US/Mountain", 72 "GMT-6": "US/Central", 73 "GMT-5": "US/Eastern", 74 "GMT-4": "America/Barbados", 75 "GMT-3": "America/Buenos_Aires", 76 "GMT-2": "Atlantic/South_Georgia", 77 "GMT-1": "Atlantic/Azores", 78 "GMT+0": "Africa/Casablanca", 79 "GMT+1": "Europe/Amsterdam", 80 "GMT+2": "Europe/Athens", 81 "GMT+3": "Europe/Moscow", 82 "GMT+4": "Asia/Baku", 83 "GMT+5": "Asia/Oral", 84 "GMT+6": "Asia/Almaty", 85 "GMT+7": "Asia/Bangkok", 86 "GMT+8": "Asia/Hong_Kong", 87 "GMT+9": "Asia/Tokyo", 88 "GMT+10": "Pacific/Guam", 89 "GMT+11": "Pacific/Noumea", 90 "GMT+12": "Pacific/Fiji", 91 "GMT+13": "Pacific/Tongatapu", 92 "GMT-11": "Pacific/Midway", 93 "GMT-10": "Pacific/Honolulu" 94 } 95 96 97 def abs_path(path): 98 """Resolve the '.' and '~' in a path to get the absolute path. 99 100 Args: 101 path: The path to expand. 102 103 Returns: 104 The absolute path of the input path. 105 """ 106 return os.path.abspath(os.path.expanduser(path)) 107 108 109 def create_dir(path): 110 """Creates a directory if it does not exist already. 111 112 Args: 113 path: The path of the directory to create. 114 """ 115 full_path = abs_path(path) 116 if not os.path.exists(full_path): 117 os.makedirs(full_path) 118 119 120 def get_current_epoch_time(): 121 """Current epoch time in milliseconds. 122 123 Returns: 124 An integer representing the current epoch time in milliseconds. 125 """ 126 return int(round(time.time() * 1000)) 127 128 129 def get_current_human_time(): 130 """Returns the current time in human readable format. 131 132 Returns: 133 The current time stamp in Month-Day-Year Hour:Min:Sec format. 134 """ 135 return time.strftime("%m-%d-%Y %H:%M:%S ") 136 137 138 def epoch_to_human_time(epoch_time): 139 """Converts an epoch timestamp to human readable time. 140 141 This essentially converts an output of get_current_epoch_time to an output 142 of get_current_human_time 143 144 Args: 145 epoch_time: An integer representing an epoch timestamp in milliseconds. 146 147 Returns: 148 A time string representing the input time. 149 None if input param is invalid. 150 """ 151 if isinstance(epoch_time, int): 152 try: 153 d = datetime.datetime.fromtimestamp(epoch_time / 1000) 154 return d.strftime("%m-%d-%Y %H:%M:%S ") 155 except ValueError: 156 return None 157 158 159 def get_timezone_olson_id(): 160 """Return the Olson ID of the local (non-DST) timezone. 161 162 Returns: 163 A string representing one of the Olson IDs of the local (non-DST) 164 timezone. 165 """ 166 tzoffset = int(time.timezone / 3600) 167 gmt = None 168 if tzoffset <= 0: 169 gmt = "GMT+{}".format(-tzoffset) 170 else: 171 gmt = "GMT-{}".format(tzoffset) 172 return GMT_to_olson[gmt] 173 174 175 def find_files(paths, file_predicate): 176 """Locate files whose names and extensions match the given predicate in 177 the specified directories. 178 179 Args: 180 paths: A list of directory paths where to find the files. 181 file_predicate: A function that returns True if the file name and 182 extension are desired. 183 184 Returns: 185 A list of files that match the predicate. 186 """ 187 file_list = [] 188 for path in paths: 189 p = abs_path(path) 190 for dirPath, subdirList, fileList in os.walk(p): 191 for fname in fileList: 192 name, ext = os.path.splitext(fname) 193 if file_predicate(name, ext): 194 file_list.append((dirPath, name, ext)) 195 return file_list 196 197 198 def iterate_files(dir_path): 199 """A generator yielding regular files in a directory recursively. 200 201 Args: 202 dir_path: A string representing the path to search. 203 204 Yields: 205 A tuple of strings (directory, file). The directory containing 206 the file and the file name. 207 """ 208 for root_dir, dir_names, file_names in os.walk(dir_path): 209 for file_name in file_names: 210 yield root_dir, file_name 211 212 213 def load_config(file_full_path): 214 """Loads a JSON config file. 215 216 Returns: 217 A JSON object. 218 """ 219 if not os.path.isfile(file_full_path): 220 logging.warning('cwd: %s', os.getcwd()) 221 pypath = os.environ['PYTHONPATH'] 222 if pypath: 223 for base_path in pypath.split(':'): 224 logging.info('checking %s', base_path) 225 new_path = os.path.join(base_path, file_full_path) 226 if os.path.isfile(new_path): 227 logging.info('found') 228 file_full_path = new_path 229 break 230 231 with open(file_full_path, 'r') as f: 232 conf = json.load(f) 233 return conf 234 235 236 def load_file_to_base64_str(f_path): 237 """Loads the content of a file into a base64 string. 238 239 Args: 240 f_path: full path to the file including the file name. 241 242 Returns: 243 A base64 string representing the content of the file in utf-8 encoding. 244 """ 245 path = abs_path(f_path) 246 with open(path, 'rb') as f: 247 f_bytes = f.read() 248 base64_str = base64.b64encode(f_bytes).decode("utf-8") 249 return base64_str 250 251 252 def find_field(item_list, cond, comparator, target_field): 253 """Finds the value of a field in a dict object that satisfies certain 254 conditions. 255 256 Args: 257 item_list: A list of dict objects. 258 cond: A param that defines the condition. 259 comparator: A function that checks if an dict satisfies the condition. 260 target_field: Name of the field whose value to be returned if an item 261 satisfies the condition. 262 263 Returns: 264 Target value or None if no item satisfies the condition. 265 """ 266 for item in item_list: 267 if comparator(item, cond) and target_field in item: 268 return item[target_field] 269 return None 270 271 272 def rand_ascii_str(length): 273 """Generates a random string of specified length, composed of ascii letters 274 and digits. 275 276 Args: 277 length: The number of characters in the string. 278 279 Returns: 280 The random string generated. 281 """ 282 letters = [random.choice(ascii_letters_and_digits) for i in range(length)] 283 return ''.join(letters) 284 285 286 # Thead/Process related functions. 287 def concurrent_exec(func, param_list): 288 """Executes a function with different parameters pseudo-concurrently. 289 290 This is basically a map function. Each element (should be an iterable) in 291 the param_list is unpacked and passed into the function. Due to Python's 292 GIL, there's no true concurrency. This is suited for IO-bound tasks. 293 294 Args: 295 func: The function that parforms a task. 296 param_list: A list of iterables, each being a set of params to be 297 passed into the function. 298 299 Returns: 300 A list of return values from each function execution. If an execution 301 caused an exception, the exception object will be the corresponding 302 result. 303 """ 304 with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor: 305 # Start the load operations and mark each future with its params 306 future_to_params = {executor.submit(func, *p): p for p in param_list} 307 return_vals = [] 308 for future in concurrent.futures.as_completed(future_to_params): 309 params = future_to_params[future] 310 try: 311 return_vals.append(future.result()) 312 except Exception as exc: 313 print("{} generated an exception: {}".format( 314 params, traceback.format_exc())) 315 return_vals.append(exc) 316 return return_vals 317 318 319 def exe_cmd(*cmds): 320 """Executes commands in a new shell. 321 322 Args: 323 cmds: A sequence of commands and arguments. 324 325 Returns: 326 The output of the command run. 327 328 Raises: 329 OSError is raised if an error occurred during the command execution. 330 """ 331 cmd = ' '.join(cmds) 332 proc = subprocess.Popen( 333 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 334 (out, err) = proc.communicate() 335 if not err: 336 return out 337 raise OSError(err) 338 339 340 def _assert_subprocess_running(proc): 341 """Checks if a subprocess has terminated on its own. 342 343 Args: 344 proc: A subprocess returned by subprocess.Popen. 345 346 Raises: 347 VTSUtilsError is raised if the subprocess has stopped. 348 """ 349 ret = proc.poll() 350 if ret is not None: 351 out, err = proc.communicate() 352 raise VTSUtilsError("Process %d has terminated. ret: %d, stderr: %s," 353 " stdout: %s" % (proc.pid, ret, err, out)) 354 355 356 def is_on_windows(): 357 """Checks whether the OS is Windows. 358 359 Returns: 360 A boolean representing whether the OS is Windows. 361 """ 362 return os.name == "nt" 363 364 365 def start_standing_subprocess(cmd, check_health_delay=0): 366 """Starts a long-running subprocess. 367 368 This is not a blocking call and the subprocess started by it should be 369 explicitly terminated with stop_standing_subprocess. 370 371 For short-running commands, you should use exe_cmd, which blocks. 372 373 You can specify a health check after the subprocess is started to make sure 374 it did not stop prematurely. 375 376 Args: 377 cmd: string, the command to start the subprocess with. 378 check_health_delay: float, the number of seconds to wait after the 379 subprocess starts to check its health. Default is 0, 380 which means no check. 381 382 Returns: 383 The subprocess that got started. 384 """ 385 if not is_on_windows(): 386 proc = subprocess.Popen( 387 cmd, 388 stdout=subprocess.PIPE, 389 stderr=subprocess.PIPE, 390 shell=True, 391 preexec_fn=os.setpgrp) 392 else: 393 proc = subprocess.Popen( 394 cmd, 395 stdout=subprocess.PIPE, 396 stderr=subprocess.PIPE, 397 shell=True, 398 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) 399 logging.debug("Start standing subprocess with cmd: %s", cmd) 400 if check_health_delay > 0: 401 time.sleep(check_health_delay) 402 _assert_subprocess_running(proc) 403 return proc 404 405 406 def stop_standing_subprocess(proc, kill_signal=signal.SIGTERM): 407 """Stops a subprocess started by start_standing_subprocess. 408 409 Before killing the process, we check if the process is running, if it has 410 terminated, VTSUtilsError is raised. 411 412 Catches and logs the PermissionError which only happens on Macs. 413 414 On Windows, SIGABRT, SIGINT, and SIGTERM are replaced with CTRL_BREAK_EVENT 415 so as to kill every subprocess in the group. 416 417 Args: 418 proc: Subprocess to terminate. 419 kill_signal: The signal sent to the subprocess group. 420 """ 421 pid = proc.pid 422 logging.debug("Stop standing subprocess %d", pid) 423 _assert_subprocess_running(proc) 424 if not is_on_windows(): 425 try: 426 os.killpg(pid, kill_signal) 427 except PermissionError as e: 428 logging.warning("os.killpg(%d, %s) PermissionError: %s", 429 pid, str(kill_signal), str(e)) 430 else: 431 if kill_signal in [signal.SIGABRT, 432 signal.SIGINT, 433 signal.SIGTERM]: 434 windows_signal = signal.CTRL_BREAK_EVENT 435 else: 436 windows_signal = kill_signal 437 os.kill(pid, windows_signal) 438 439 440 def wait_for_standing_subprocess(proc, timeout=None): 441 """Waits for a subprocess started by start_standing_subprocess to finish 442 or times out. 443 444 Propagates the exception raised by the subprocess.wait(.) function. 445 The subprocess.TimeoutExpired exception is raised if the process timed-out 446 rather then terminating. 447 448 If no exception is raised: the subprocess terminated on its own. No need 449 to call stop_standing_subprocess() to kill it. 450 451 If an exception is raised: the subprocess is still alive - it did not 452 terminate. Either call stop_standing_subprocess() to kill it, or call 453 wait_for_standing_subprocess() to keep waiting for it to terminate on its 454 own. 455 456 Args: 457 p: Subprocess to wait for. 458 timeout: An integer number of seconds to wait before timing out. 459 """ 460 proc.wait(timeout) 461 462 463 def sync_device_time(ad): 464 """Sync the time of an android device with the current system time. 465 466 Both epoch time and the timezone will be synced. 467 468 Args: 469 ad: The android device to sync time on. 470 """ 471 droid = ad.droid 472 droid.setTimeZone(get_timezone_olson_id()) 473 droid.setTime(get_current_epoch_time()) 474 475 476 # Timeout decorator block 477 class TimeoutError(Exception): 478 """Exception for timeout decorator related errors. 479 """ 480 pass 481 482 483 def _timeout_handler(signum, frame): 484 """Handler function used by signal to terminate a timed out function. 485 """ 486 raise TimeoutError() 487 488 489 def timeout(sec): 490 """A decorator used to add time out check to a function. 491 492 Args: 493 sec: Number of seconds to wait before the function times out. 494 No timeout if set to 0 495 496 Returns: 497 What the decorated function returns. 498 499 Raises: 500 TimeoutError is raised when time out happens. 501 """ 502 503 def decorator(func): 504 @functools.wraps(func) 505 def wrapper(*args, **kwargs): 506 if sec: 507 signal.signal(signal.SIGALRM, _timeout_handler) 508 signal.alarm(sec) 509 try: 510 return func(*args, **kwargs) 511 except TimeoutError: 512 raise TimeoutError(("Function {} timed out after {} " 513 "seconds.").format(func.__name__, sec)) 514 finally: 515 signal.alarm(0) 516 517 return wrapper 518 519 return decorator 520 521 522 def trim_model_name(model): 523 """Trim any prefix and postfix and return the android designation of the 524 model name. 525 526 e.g. "m_shamu" will be trimmed to "shamu". 527 528 Args: 529 model: model name to be trimmed. 530 531 Returns 532 Trimmed model name if one of the known model names is found. 533 None otherwise. 534 """ 535 # Directly look up first. 536 if model in models: 537 return model 538 if model in manufacture_name_to_model: 539 return manufacture_name_to_model[model] 540 # If not found, try trimming off prefix/postfix and look up again. 541 tokens = re.split("_|-", model) 542 for t in tokens: 543 if t in models: 544 return t 545 if t in manufacture_name_to_model: 546 return manufacture_name_to_model[t] 547 return None 548 549 550 def force_airplane_mode(ad, new_state, timeout_value=60): 551 """Force the device to set airplane mode on or off by adb shell command. 552 553 Args: 554 ad: android device object. 555 new_state: Turn on airplane mode if True. 556 Turn off airplane mode if False. 557 timeout_value: max wait time for 'adb wait-for-device' 558 559 Returns: 560 True if success. 561 False if timeout. 562 """ 563 # Using timeout decorator. 564 # Wait for device with timeout. If after <timeout_value> seconds, adb 565 # is still waiting for device, throw TimeoutError exception. 566 @timeout(timeout_value) 567 def wait_for_device_with_timeout(ad): 568 ad.adb.wait_for_device() 569 570 try: 571 wait_for_device_with_timeout(ad) 572 ad.adb.shell("settings put global airplane_mode_on {}".format( 573 1 if new_state else 0)) 574 except TimeoutError: 575 # adb wait for device timeout 576 return False 577 return True 578 579 580 def enable_doze(ad): 581 """Force the device into doze mode. 582 583 Args: 584 ad: android device object. 585 586 Returns: 587 True if device is in doze mode. 588 False otherwise. 589 """ 590 ad.adb.shell("dumpsys battery unplug") 591 ad.adb.shell("dumpsys deviceidle enable") 592 if (ad.adb.shell("dumpsys deviceidle force-idle") != 593 b'Now forced in to idle mode\r\n'): 594 return False 595 ad.droid.goToSleepNow() 596 time.sleep(5) 597 adb_shell_result = ad.adb.shell("dumpsys deviceidle step") 598 if adb_shell_result not in [b'Stepped to: IDLE_MAINTENANCE\r\n', 599 b'Stepped to: IDLE\r\n']: 600 info = ("dumpsys deviceidle step: {}dumpsys battery: {}" 601 "dumpsys deviceidle: {}".format( 602 adb_shell_result.decode('utf-8'), 603 ad.adb.shell("dumpsys battery").decode('utf-8'), 604 ad.adb.shell("dumpsys deviceidle").decode('utf-8'))) 605 print(info) 606 return False 607 return True 608 609 610 def disable_doze(ad): 611 """Force the device not in doze mode. 612 613 Args: 614 ad: android device object. 615 616 Returns: 617 True if device is not in doze mode. 618 False otherwise. 619 """ 620 ad.adb.shell("dumpsys deviceidle disable") 621 ad.adb.shell("dumpsys battery reset") 622 adb_shell_result = ad.adb.shell("dumpsys deviceidle step") 623 if (adb_shell_result != b'Stepped to: ACTIVE\r\n'): 624 info = ("dumpsys deviceidle step: {}dumpsys battery: {}" 625 "dumpsys deviceidle: {}".format( 626 adb_shell_result.decode('utf-8'), 627 ad.adb.shell("dumpsys battery").decode('utf-8'), 628 ad.adb.shell("dumpsys deviceidle").decode('utf-8'))) 629 print(info) 630 return False 631 return True 632 633 634 def set_ambient_display(ad, new_state): 635 """Set "Ambient Display" in Settings->Display 636 637 Args: 638 ad: android device object. 639 new_state: new state for "Ambient Display". True or False. 640 """ 641 ad.adb.shell("settings put secure doze_enabled {}".format(1 if new_state 642 else 0)) 643 644 645 def set_adaptive_brightness(ad, new_state): 646 """Set "Adaptive Brightness" in Settings->Display 647 648 Args: 649 ad: android device object. 650 new_state: new state for "Adaptive Brightness". True or False. 651 """ 652 ad.adb.shell("settings put system screen_brightness_mode {}".format( 653 1 if new_state else 0)) 654 655 656 def set_auto_rotate(ad, new_state): 657 """Set "Auto-rotate" in QuickSetting 658 659 Args: 660 ad: android device object. 661 new_state: new state for "Auto-rotate". True or False. 662 """ 663 ad.adb.shell("settings put system accelerometer_rotation {}".format( 664 1 if new_state else 0)) 665 666 667 def set_location_service(ad, new_state): 668 """Set Location service on/off in Settings->Location 669 670 Args: 671 ad: android device object. 672 new_state: new state for "Location service". 673 If new_state is False, turn off location service. 674 If new_state if True, set location service to "High accuracy". 675 """ 676 if new_state: 677 ad.adb.shell("settings put secure location_providers_allowed +gps") 678 ad.adb.shell("settings put secure location_providers_allowed +network") 679 else: 680 ad.adb.shell("settings put secure location_providers_allowed -gps") 681 ad.adb.shell("settings put secure location_providers_allowed -network") 682 683 684 def set_mobile_data_always_on(ad, new_state): 685 """Set Mobile_Data_Always_On feature bit 686 687 Args: 688 ad: android device object. 689 new_state: new state for "mobile_data_always_on" 690 if new_state is False, set mobile_data_always_on disabled. 691 if new_state if True, set mobile_data_always_on enabled. 692 """ 693 ad.adb.shell("settings put global mobile_data_always_on {}".format( 694 1 if new_state else 0)) 695