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 kill_process_group(proc, signal_no=signal.SIGTERM): 366 """Sends signal to a process group. 367 368 Logs when there is an OSError or PermissionError. The latter one only 369 happens on Mac. 370 371 On Windows, SIGABRT, SIGINT, and SIGTERM are replaced with CTRL_BREAK_EVENT 372 so as to kill every subprocess in the group. 373 374 Args: 375 proc: The Popen object whose pid is the group id. 376 signal_no: The signal sent to the subprocess group. 377 """ 378 pid = proc.pid 379 try: 380 if not is_on_windows(): 381 os.killpg(pid, signal_no) 382 else: 383 if signal_no in [signal.SIGABRT, 384 signal.SIGINT, 385 signal.SIGTERM]: 386 windows_signal_no = signal.CTRL_BREAK_EVENT 387 else: 388 windows_signal_no = signal_no 389 os.kill(pid, windows_signal_no) 390 except (OSError, PermissionError) as e: 391 logging.exception("Cannot send signal %s to process group %d: %s", 392 signal_no, pid, str(e)) 393 394 395 def start_standing_subprocess(cmd, check_health_delay=0): 396 """Starts a long-running subprocess. 397 398 This is not a blocking call and the subprocess started by it should be 399 explicitly terminated with stop_standing_subprocess. 400 401 For short-running commands, you should use exe_cmd, which blocks. 402 403 You can specify a health check after the subprocess is started to make sure 404 it did not stop prematurely. 405 406 Args: 407 cmd: string, the command to start the subprocess with. 408 check_health_delay: float, the number of seconds to wait after the 409 subprocess starts to check its health. Default is 0, 410 which means no check. 411 412 Returns: 413 The subprocess that got started. 414 """ 415 if not is_on_windows(): 416 proc = subprocess.Popen( 417 cmd, 418 stdout=subprocess.PIPE, 419 stderr=subprocess.PIPE, 420 shell=True, 421 preexec_fn=os.setpgrp) 422 else: 423 proc = subprocess.Popen( 424 cmd, 425 stdout=subprocess.PIPE, 426 stderr=subprocess.PIPE, 427 shell=True, 428 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) 429 logging.debug("Start standing subprocess with cmd: %s", cmd) 430 if check_health_delay > 0: 431 time.sleep(check_health_delay) 432 _assert_subprocess_running(proc) 433 return proc 434 435 436 def stop_standing_subprocess(proc, signal_no=signal.SIGTERM): 437 """Stops a subprocess started by start_standing_subprocess. 438 439 Before killing the process, we check if the process is running, if it has 440 terminated, VTSUtilsError is raised. 441 442 Args: 443 proc: Subprocess to terminate. 444 signal_no: The signal sent to the subprocess group. 445 """ 446 logging.debug("Stop standing subprocess %d", proc.pid) 447 _assert_subprocess_running(proc) 448 kill_process_group(proc, signal_no) 449 450 451 def wait_for_standing_subprocess(proc, timeout=None): 452 """Waits for a subprocess started by start_standing_subprocess to finish 453 or times out. 454 455 Propagates the exception raised by the subprocess.wait(.) function. 456 The subprocess.TimeoutExpired exception is raised if the process timed-out 457 rather then terminating. 458 459 If no exception is raised: the subprocess terminated on its own. No need 460 to call stop_standing_subprocess() to kill it. 461 462 If an exception is raised: the subprocess is still alive - it did not 463 terminate. Either call stop_standing_subprocess() to kill it, or call 464 wait_for_standing_subprocess() to keep waiting for it to terminate on its 465 own. 466 467 Args: 468 p: Subprocess to wait for. 469 timeout: An integer number of seconds to wait before timing out. 470 """ 471 proc.wait(timeout) 472 473 474 def sync_device_time(ad): 475 """Sync the time of an android device with the current system time. 476 477 Both epoch time and the timezone will be synced. 478 479 Args: 480 ad: The android device to sync time on. 481 """ 482 droid = ad.droid 483 droid.setTimeZone(get_timezone_olson_id()) 484 droid.setTime(get_current_epoch_time()) 485 486 487 # Timeout decorator block 488 class TimeoutError(Exception): 489 """Exception for timeout decorator related errors. 490 """ 491 pass 492 493 494 def _timeout_handler(signum, frame): 495 """Handler function used by signal to terminate a timed out function. 496 """ 497 raise TimeoutError() 498 499 500 def timeout(sec): 501 """A decorator used to add time out check to a function. 502 503 Args: 504 sec: Number of seconds to wait before the function times out. 505 No timeout if set to 0 506 507 Returns: 508 What the decorated function returns. 509 510 Raises: 511 TimeoutError is raised when time out happens. 512 """ 513 514 def decorator(func): 515 @functools.wraps(func) 516 def wrapper(*args, **kwargs): 517 if sec: 518 signal.signal(signal.SIGALRM, _timeout_handler) 519 signal.alarm(sec) 520 try: 521 return func(*args, **kwargs) 522 except TimeoutError: 523 raise TimeoutError(("Function {} timed out after {} " 524 "seconds.").format(func.__name__, sec)) 525 finally: 526 signal.alarm(0) 527 528 return wrapper 529 530 return decorator 531 532 533 def trim_model_name(model): 534 """Trim any prefix and postfix and return the android designation of the 535 model name. 536 537 e.g. "m_shamu" will be trimmed to "shamu". 538 539 Args: 540 model: model name to be trimmed. 541 542 Returns 543 Trimmed model name if one of the known model names is found. 544 None otherwise. 545 """ 546 # Directly look up first. 547 if model in models: 548 return model 549 if model in manufacture_name_to_model: 550 return manufacture_name_to_model[model] 551 # If not found, try trimming off prefix/postfix and look up again. 552 tokens = re.split("_|-", model) 553 for t in tokens: 554 if t in models: 555 return t 556 if t in manufacture_name_to_model: 557 return manufacture_name_to_model[t] 558 return None 559 560 561 def force_airplane_mode(ad, new_state, timeout_value=60): 562 """Force the device to set airplane mode on or off by adb shell command. 563 564 Args: 565 ad: android device object. 566 new_state: Turn on airplane mode if True. 567 Turn off airplane mode if False. 568 timeout_value: max wait time for 'adb wait-for-device' 569 570 Returns: 571 True if success. 572 False if timeout. 573 """ 574 # Using timeout decorator. 575 # Wait for device with timeout. If after <timeout_value> seconds, adb 576 # is still waiting for device, throw TimeoutError exception. 577 @timeout(timeout_value) 578 def wait_for_device_with_timeout(ad): 579 ad.adb.wait_for_device() 580 581 try: 582 wait_for_device_with_timeout(ad) 583 ad.adb.shell("settings put global airplane_mode_on {}".format( 584 1 if new_state else 0)) 585 except TimeoutError: 586 # adb wait for device timeout 587 return False 588 return True 589 590 591 def enable_doze(ad): 592 """Force the device into doze mode. 593 594 Args: 595 ad: android device object. 596 597 Returns: 598 True if device is in doze mode. 599 False otherwise. 600 """ 601 ad.adb.shell("dumpsys battery unplug") 602 ad.adb.shell("dumpsys deviceidle enable") 603 if (ad.adb.shell("dumpsys deviceidle force-idle") != 604 b'Now forced in to idle mode\r\n'): 605 return False 606 ad.droid.goToSleepNow() 607 time.sleep(5) 608 adb_shell_result = ad.adb.shell("dumpsys deviceidle step") 609 if adb_shell_result not in [b'Stepped to: IDLE_MAINTENANCE\r\n', 610 b'Stepped to: IDLE\r\n']: 611 info = ("dumpsys deviceidle step: {}dumpsys battery: {}" 612 "dumpsys deviceidle: {}".format( 613 adb_shell_result.decode('utf-8'), 614 ad.adb.shell("dumpsys battery").decode('utf-8'), 615 ad.adb.shell("dumpsys deviceidle").decode('utf-8'))) 616 print(info) 617 return False 618 return True 619 620 621 def disable_doze(ad): 622 """Force the device not in doze mode. 623 624 Args: 625 ad: android device object. 626 627 Returns: 628 True if device is not in doze mode. 629 False otherwise. 630 """ 631 ad.adb.shell("dumpsys deviceidle disable") 632 ad.adb.shell("dumpsys battery reset") 633 adb_shell_result = ad.adb.shell("dumpsys deviceidle step") 634 if (adb_shell_result != b'Stepped to: ACTIVE\r\n'): 635 info = ("dumpsys deviceidle step: {}dumpsys battery: {}" 636 "dumpsys deviceidle: {}".format( 637 adb_shell_result.decode('utf-8'), 638 ad.adb.shell("dumpsys battery").decode('utf-8'), 639 ad.adb.shell("dumpsys deviceidle").decode('utf-8'))) 640 print(info) 641 return False 642 return True 643 644 645 def set_ambient_display(ad, new_state): 646 """Set "Ambient Display" in Settings->Display 647 648 Args: 649 ad: android device object. 650 new_state: new state for "Ambient Display". True or False. 651 """ 652 ad.adb.shell("settings put secure doze_enabled {}".format(1 if new_state 653 else 0)) 654 655 656 def set_adaptive_brightness(ad, new_state): 657 """Set "Adaptive Brightness" in Settings->Display 658 659 Args: 660 ad: android device object. 661 new_state: new state for "Adaptive Brightness". True or False. 662 """ 663 ad.adb.shell("settings put system screen_brightness_mode {}".format( 664 1 if new_state else 0)) 665 666 667 def set_auto_rotate(ad, new_state): 668 """Set "Auto-rotate" in QuickSetting 669 670 Args: 671 ad: android device object. 672 new_state: new state for "Auto-rotate". True or False. 673 """ 674 ad.adb.shell("settings put system accelerometer_rotation {}".format( 675 1 if new_state else 0)) 676 677 678 def set_location_service(ad, new_state): 679 """Set Location service on/off in Settings->Location 680 681 Args: 682 ad: android device object. 683 new_state: new state for "Location service". 684 If new_state is False, turn off location service. 685 If new_state if True, set location service to "High accuracy". 686 """ 687 if new_state: 688 ad.adb.shell("settings put secure location_providers_allowed +gps") 689 ad.adb.shell("settings put secure location_providers_allowed +network") 690 else: 691 ad.adb.shell("settings put secure location_providers_allowed -gps") 692 ad.adb.shell("settings put secure location_providers_allowed -network") 693 694 695 def set_mobile_data_always_on(ad, new_state): 696 """Set Mobile_Data_Always_On feature bit 697 698 Args: 699 ad: android device object. 700 new_state: new state for "mobile_data_always_on" 701 if new_state is False, set mobile_data_always_on disabled. 702 if new_state if True, set mobile_data_always_on enabled. 703 """ 704 ad.adb.shell("settings put global mobile_data_always_on {}".format( 705 1 if new_state else 0)) 706