Home | History | Annotate | Download | only in host
      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