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