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