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 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