| #!/usr/bin/env python3 |
| |
| r""" |
| This module provides many valuable functions such as my_parm_file. |
| """ |
| |
| import collections |
| import errno |
| import inspect |
| import json |
| import os |
| import random |
| import shutil |
| |
| # sys and os are needed to get the program dir path and program name. |
| import sys |
| import time |
| |
| try: |
| import ConfigParser |
| except ImportError: |
| import configparser |
| try: |
| import StringIO |
| except ImportError: |
| import io |
| |
| import re |
| import socket |
| import tempfile |
| |
| try: |
| import psutil |
| |
| psutil_imported = True |
| except ImportError: |
| psutil_imported = False |
| |
| import gen_cmd as gc |
| import gen_print as gp |
| |
| robot_env = gp.robot_env |
| if robot_env: |
| from robot.libraries.BuiltIn import BuiltIn |
| from robot.utils import DotDict |
| |
| |
| def add_trailing_slash(dir_path): |
| r""" |
| Add a trailing slash to the directory path if it doesn't already have one |
| and return it. |
| |
| Description of arguments: |
| dir_path A directory path. |
| """ |
| |
| return os.path.normpath(dir_path) + os.path.sep |
| |
| |
| def makedirs(path, mode=0o777, quiet=None): |
| r""" |
| Call os.makedirs with the caller's arguments. |
| |
| This function offers 2 advantages over the base os.makedirs function: |
| 1) It will not fail if the directory already exists. |
| 2) It will print an "Issuing: os.makedirs" message. |
| |
| Description of argument(s): |
| path The path containing the directories to be created. |
| mode The mode or permissions to be granted to the created directories. |
| quiet Indicates whether this function should run the print_issuing() function. |
| """ |
| quiet = int(dft(quiet, gp.get_stack_var("quiet", 0))) |
| gp.qprint_issuing("os.makedirs('" + path + "', mode=" + oct(mode) + ")") |
| try: |
| os.makedirs(path, mode) |
| except OSError: |
| pass |
| |
| |
| def rmtree(path, ignore_errors=False, onerror=None, quiet=None): |
| r""" |
| Call shutil.rmtree with the caller's arguments. |
| |
| This function offers this advantage over the base function: |
| - It will print an "Issuing: shutil.rmtree" message. |
| |
| Description of argument(s): |
| (All parms are passed directly to shutil.rmtree. See its prolog for details) |
| quiet Indicates whether this function should run the print_issuing() function. |
| """ |
| quiet = int(dft(quiet, gp.get_stack_var("quiet", 0))) |
| print_string = gp.sprint_executing(max_width=2000) |
| print_string = re.sub( |
| r"Executing: ", "Issuing: shutil.", print_string.rstrip("\n") |
| ) |
| gp.qprintn(re.sub(r", quiet[ ]?=.*", ")", print_string)) |
| shutil.rmtree(path, ignore_errors, onerror) |
| |
| |
| def chdir(path, quiet=None): |
| r""" |
| Call os.chdir with the caller's arguments. |
| |
| This function offers this advantage over the base os.chdir function: |
| - It will print an "Issuing: os.chdir" message. |
| |
| Description of argument(s): |
| path The path of the directory to change to. |
| quiet Indicates whether this function should run the print_issuing() function. |
| """ |
| quiet = int(dft(quiet, gp.get_stack_var("quiet", 0))) |
| gp.qprint_issuing("os.chdir('" + path + "')") |
| os.chdir(path) |
| |
| |
| def which(file_path): |
| r""" |
| Find the full path of an executable file and return it. |
| |
| The PATH environment variable dictates the results of this function. |
| |
| Description of arguments: |
| file_path The relative file path (e.g. "my_file" or "lib/my_file"). |
| """ |
| |
| shell_rc, out_buf = gc.cmd_fnc_u( |
| "which " + file_path, quiet=1, print_output=0, show_err=0 |
| ) |
| if shell_rc != 0: |
| error_message = ( |
| 'Failed to find complete path for file "' + file_path + '".\n' |
| ) |
| error_message += gp.sprint_var(shell_rc, gp.hexa()) |
| error_message += out_buf |
| if robot_env: |
| BuiltIn().fail(gp.sprint_error(error_message)) |
| else: |
| gp.print_error_report(error_message) |
| return False |
| |
| file_path = out_buf.rstrip("\n") |
| |
| return file_path |
| |
| |
| def add_path(new_path, path, position=0): |
| r""" |
| Add new_path to path, provided that path doesn't already contain new_path, and return the result. |
| |
| Example: |
| If PATH has a value of "/bin/user:/lib/user". The following code: |
| |
| PATH = add_path("/tmp/new_path", PATH) |
| |
| will change PATH to "/tmp/new_path:/bin/user:/lib/user". |
| |
| Description of argument(s): |
| new_path The path to be added. This function will strip the trailing slash. |
| path The path value to which the new_path should be added. |
| position The position in path where the new_path should be added. 0 means it |
| should be added to the beginning, 1 means add it as the 2nd item, etc. |
| sys.maxsize means it should be added to the end. |
| """ |
| |
| path_list = list(filter(None, path.split(":"))) |
| new_path = new_path.rstrip("/") |
| if new_path not in path_list: |
| path_list.insert(int(position), new_path) |
| return ":".join(path_list) |
| |
| |
| def dft(value, default): |
| r""" |
| Return default if value is None. Otherwise, return value. |
| |
| This is really just shorthand as shown below. |
| |
| dft(value, default) |
| |
| vs |
| |
| default if value is None else value |
| |
| Description of arguments: |
| value The value to be returned. |
| default The default value to return if value is None. |
| """ |
| |
| return default if value is None else value |
| |
| |
| def get_mod_global(var_name, default=None, mod_name="__main__"): |
| r""" |
| Get module global variable value and return it. |
| |
| If we are running in a robot environment, the behavior will default to |
| calling get_variable_value. |
| |
| Description of arguments: |
| var_name The name of the variable whose value is sought. |
| default The value to return if the global does not exist. |
| mod_name The name of the module containing the global variable. |
| """ |
| |
| if robot_env: |
| return BuiltIn().get_variable_value("${" + var_name + "}", default) |
| |
| try: |
| module = sys.modules[mod_name] |
| except KeyError: |
| gp.print_error_report( |
| "Programmer error - The mod_name passed to" |
| + " this function is invalid:\n" |
| + gp.sprint_var(mod_name) |
| ) |
| raise ValueError("Programmer error.") |
| |
| if default is None: |
| return getattr(module, var_name) |
| else: |
| return getattr(module, var_name, default) |
| |
| |
| def global_default(var_value, default=0): |
| r""" |
| If var_value is not None, return it. Otherwise, return the global |
| variable of the same name, if it exists. If not, return default. |
| |
| This is meant for use by functions needing help assigning dynamic default |
| values to their parms. Example: |
| |
| def func1(parm1=None): |
| |
| parm1 = global_default(parm1, 0) |
| |
| Description of arguments: |
| var_value The value being evaluated. |
| default The value to be returned if var_value is None AND the global variable of |
| the same name does not exist. |
| """ |
| |
| var_name = gp.get_arg_name(0, 1, stack_frame_ix=2) |
| |
| return dft(var_value, get_mod_global(var_name, 0)) |
| |
| |
| def set_mod_global(var_value, mod_name="__main__", var_name=None): |
| r""" |
| Set a global variable for a given module. |
| |
| Description of arguments: |
| var_value The value to set in the variable. |
| mod_name The name of the module whose variable is to be set. |
| var_name The name of the variable to set. This defaults to the name of the |
| variable used for var_value when calling this function. |
| """ |
| |
| try: |
| module = sys.modules[mod_name] |
| except KeyError: |
| gp.print_error_report( |
| "Programmer error - The mod_name passed to" |
| + " this function is invalid:\n" |
| + gp.sprint_var(mod_name) |
| ) |
| raise ValueError("Programmer error.") |
| |
| if var_name is None: |
| var_name = gp.get_arg_name(None, 1, 2) |
| |
| setattr(module, var_name, var_value) |
| |
| |
| def my_parm_file(prop_file_path): |
| r""" |
| Read a properties file, put the keys/values into a dictionary and return the dictionary. |
| |
| The properties file must have the following format: |
| var_name<= or :>var_value |
| Comment lines (those beginning with a "#") and blank lines are allowed and will be ignored. Leading and |
| trailing single or double quotes will be stripped from the value. E.g. |
| var1="This one" |
| Quotes are stripped so the resulting value for var1 is: |
| This one |
| |
| Description of arguments: |
| prop_file_path The caller should pass the path to the properties file. |
| """ |
| |
| # ConfigParser expects at least one section header in the file (or you get |
| # ConfigParser.MissingSectionHeaderError). Properties files don't need those so I'll write a dummy |
| # section header. |
| |
| try: |
| string_file = StringIO.StringIO() |
| except NameError: |
| string_file = io.StringIO() |
| |
| # Write the dummy section header to the string file. |
| string_file.write("[dummysection]\n") |
| # Write the entire contents of the properties file to the string file. |
| string_file.write(open(prop_file_path).read()) |
| # Rewind the string file. |
| string_file.seek(0, os.SEEK_SET) |
| |
| # Create the ConfigParser object. |
| try: |
| config_parser = ConfigParser.ConfigParser() |
| except NameError: |
| config_parser = configparser.ConfigParser(strict=False) |
| # Make the property names case-sensitive. |
| config_parser.optionxform = str |
| # Read the properties from the string file. |
| config_parser.readfp(string_file) |
| # Return the properties as a dictionary. |
| if robot_env: |
| return DotDict(config_parser.items("dummysection")) |
| else: |
| return collections.OrderedDict(config_parser.items("dummysection")) |
| |
| |
| def file_to_list(file_path, newlines=0, comments=1, trim=0): |
| r""" |
| Return the contents of a file as a list. Each element of the resulting |
| list is one line from the file. |
| |
| Description of arguments: |
| file_path The path to the file (relative or absolute). |
| newlines Include newlines from the file in the results. |
| comments Include comment lines and blank lines in the results. Comment lines are |
| any that begin with 0 or more spaces followed by the pound sign ("#"). |
| trim Trim white space from the beginning and end of each line. |
| """ |
| |
| lines = [] |
| file = open(file_path) |
| for line in file: |
| if not comments: |
| if re.match(r"[ ]*#|^$", line): |
| continue |
| if not newlines: |
| line = line.rstrip("\n") |
| if trim: |
| line = line.strip() |
| lines.append(line) |
| file.close() |
| |
| return lines |
| |
| |
| def file_to_str(*args, **kwargs): |
| r""" |
| Return the contents of a file as a string. |
| |
| Description of arguments: |
| See file_to_list defined above for description of arguments. |
| """ |
| |
| return "\n".join(file_to_list(*args, **kwargs)) |
| |
| |
| def append_file(file_path, buffer): |
| r""" |
| Append the data in buffer to the file named in file_path. |
| |
| Description of argument(s): |
| file_path The path to a file (e.g. "/tmp/root/file1"). |
| buffer The buffer of data to be written to the file (e.g. "this and that"). |
| """ |
| |
| with open(file_path, "a") as file: |
| file.write(buffer) |
| |
| |
| def return_path_list(): |
| r""" |
| This function will split the PATH environment variable into a PATH_LIST and return it. Each element in |
| the list will be normalized and have a trailing slash added. |
| """ |
| |
| PATH_LIST = os.environ["PATH"].split(":") |
| PATH_LIST = [os.path.normpath(path) + os.sep for path in PATH_LIST] |
| |
| return PATH_LIST |
| |
| |
| def escape_bash_quotes(buffer): |
| r""" |
| Escape quotes in string and return it. |
| |
| The escape style implemented will be for use on the bash command line. |
| |
| Example: |
| That's all. |
| |
| Result: |
| That'\''s all. |
| |
| The result may then be single quoted on a bash command. Example: |
| |
| echo 'That'\''s all.' |
| |
| Description of argument(s): |
| buffer The string whose quotes are to be escaped. |
| """ |
| |
| return re.sub("'", "'\\''", buffer) |
| |
| |
| def quote_bash_parm(parm): |
| r""" |
| Return the bash command line parm with single quotes if they are needed. |
| |
| Description of arguments: |
| parm The string to be quoted. |
| """ |
| |
| # If any of these characters are found in the parm string, then the string should be quoted. This list |
| # is by no means complete and should be expanded as needed by the developer of this function. |
| # Spaces |
| # Single or double quotes. |
| # Bash variables (therefore, any string with a "$" may need quoting). |
| # Glob characters: *, ?, [] |
| # Extended Glob characters: +, @, ! |
| # Bash brace expansion: {} |
| # Tilde expansion: ~ |
| # Piped commands: | |
| # Bash re-direction: >, < |
| bash_special_chars = set(" '\"$*?[]+@!{}~|><") |
| |
| if any((char in bash_special_chars) for char in parm): |
| return "'" + escape_bash_quotes(parm) + "'" |
| |
| if parm == "": |
| parm = "''" |
| |
| return parm |
| |
| |
| def get_host_name_ip(host=None, short_name=0): |
| r""" |
| Get the host name and the IP address for the given host and return them as a tuple. |
| |
| Description of argument(s): |
| host The host name or IP address to be obtained. |
| short_name Include the short host name in the returned tuple, i.e. return host, ip |
| and short_host. |
| """ |
| |
| host = dft(host, socket.gethostname()) |
| host_name = socket.getfqdn(host) |
| try: |
| host_ip = socket.gethostbyname(host) |
| except socket.gaierror as my_gaierror: |
| message = ( |
| "Unable to obtain the host name for the following host:" |
| + "\n" |
| + gp.sprint_var(host) |
| ) |
| gp.print_error_report(message) |
| raise my_gaierror |
| |
| if short_name: |
| host_short_name = host_name.split(".")[0] |
| return host_name, host_ip, host_short_name |
| else: |
| return host_name, host_ip |
| |
| |
| def pid_active(pid): |
| r""" |
| Return true if pid represents an active pid and false otherwise. |
| |
| Description of argument(s): |
| pid The pid whose status is being sought. |
| """ |
| |
| try: |
| os.kill(int(pid), 0) |
| except OSError as err: |
| if err.errno == errno.ESRCH: |
| # ESRCH == No such process |
| return False |
| elif err.errno == errno.EPERM: |
| # EPERM clearly means there's a process to deny access to |
| return True |
| else: |
| # According to "man 2 kill" possible error values are |
| # (EINVAL, EPERM, ESRCH) |
| raise |
| |
| return True |
| |
| |
| def to_signed(number, bit_width=None): |
| r""" |
| Convert number to a signed number and return the result. |
| |
| Examples: |
| |
| With the following code: |
| |
| var1 = 0xfffffffffffffff1 |
| print_var(var1) |
| print_var(var1, hexa()) |
| var1 = to_signed(var1) |
| print_var(var1) |
| print_var(var1, hexa()) |
| |
| The following is written to stdout: |
| var1: 18446744073709551601 |
| var1: 0x00000000fffffffffffffff1 |
| var1: -15 |
| var1: 0xfffffffffffffff1 |
| |
| The same code but with var1 set to 0x000000000000007f produces the following: |
| var1: 127 |
| var1: 0x000000000000007f |
| var1: 127 |
| var1: 0x000000000000007f |
| |
| Description of argument(s): |
| number The number to be converted. |
| bit_width The number of bits that defines a complete hex value. Typically, this |
| would be a multiple of 32. |
| """ |
| |
| if bit_width is None: |
| try: |
| bit_width = gp.bit_length(long(sys.maxsize)) + 1 |
| except NameError: |
| bit_width = gp.bit_length(int(sys.maxsize)) + 1 |
| |
| if number < 0: |
| return number |
| neg_bit_mask = 2 ** (bit_width - 1) |
| if number & neg_bit_mask: |
| return ((2**bit_width) - number) * -1 |
| else: |
| return number |
| |
| |
| def get_child_pids(quiet=1): |
| r""" |
| Get and return a list of pids representing all first-generation processes that are the children of the |
| current process. |
| |
| Example: |
| |
| children = get_child_pids() |
| print_var(children) |
| |
| Output: |
| children: |
| children[0]: 9123 |
| |
| Description of argument(s): |
| quiet Display output to stdout detailing how this child pids are obtained. |
| """ |
| |
| if psutil_imported: |
| # If "import psutil" worked, find child pids using psutil. |
| current_process = psutil.Process() |
| return [x.pid for x in current_process.children(recursive=False)] |
| else: |
| # Otherwise, find child pids using shell commands. |
| print_output = not quiet |
| |
| ps_cmd_buf = ( |
| "ps --no-headers --ppid " + str(os.getpid()) + " -o pid,args" |
| ) |
| # Route the output of ps to a temporary file for later grepping. Avoid using " | grep" in the ps |
| # command string because it creates yet another process which is of no interest to the caller. |
| temp = tempfile.NamedTemporaryFile() |
| temp_file_path = temp.name |
| gc.shell_cmd( |
| ps_cmd_buf + " > " + temp_file_path, print_output=print_output |
| ) |
| # Sample contents of the temporary file: |
| # 30703 sleep 2 |
| # 30795 /bin/bash -c ps --no-headers --ppid 30672 -o pid,args > /tmp/tmpqqorWY |
| # Use egrep to exclude the "ps" process itself from the results collected with the prior shell_cmd |
| # invocation. Only the other children are of interest to the caller. Use cut on the grep results to |
| # obtain only the pid column. |
| rc, output = gc.shell_cmd( |
| "egrep -v '" |
| + re.escape(ps_cmd_buf) |
| + "' " |
| + temp_file_path |
| + " | cut -c1-5", |
| print_output=print_output, |
| ) |
| # Split the output buffer by line into a list. Strip each element of extra spaces and convert each |
| # element to an integer. |
| return map(int, map(str.strip, filter(None, output.split("\n")))) |
| |
| |
| def json_loads_multiple(buffer): |
| r""" |
| Convert the contents of the buffer to a JSON array, run json.loads() on it and return the result. |
| |
| The buffer is expected to contain one or more JSON objects. |
| |
| Description of argument(s): |
| buffer A string containing several JSON objects. |
| """ |
| |
| # Any line consisting of just "}", which indicates the end of an object, should have a comma appended. |
| regex = "([\\r\\n])[\\}]([\\r\\n])" |
| buffer = re.sub(regex, "\\1},\\2", buffer, 1) |
| # Remove the comma from after the final object and place the whole buffer inside square brackets. |
| buffer = "[" + re.sub(",([\r\n])$", "\\1}", buffer, 1) + "]" |
| if gp.robot_env: |
| return json.loads(buffer, object_pairs_hook=DotDict) |
| else: |
| return json.loads(buffer, object_pairs_hook=collections.OrderedDict) |
| |
| |
| def file_date_time_stamp(): |
| r""" |
| Return a date/time stamp in the following format: yymmdd.HHMMSS |
| |
| This value is suitable for including in file names. Example file1.181001.171716.status |
| """ |
| |
| return time.strftime("%y%m%d.%H%M%S", time.localtime(time.time())) |
| |
| |
| def get_function_stack(): |
| r""" |
| Return a list of all the function names currently in the call stack. |
| |
| This function's name will be at offset 0. This function's caller's name will be at offset 1 and so on. |
| """ |
| |
| return [str(stack_frame[3]) for stack_frame in inspect.stack()] |
| |
| |
| def username(): |
| r""" |
| Return the username for the current process. |
| """ |
| |
| username = os.environ.get("USER", "") |
| if username != "": |
| return username |
| user_num = str(os.geteuid()) |
| try: |
| username = os.getlogin() |
| except OSError: |
| if user_num == "0": |
| username = "root" |
| else: |
| username = "?" |
| |
| return username |
| |
| |
| def version_tuple(version): |
| r""" |
| Convert the version string to a tuple and return it. |
| |
| Description of argument(s): |
| version A version string whose format is "n[.n]" (e.g. "3.6.3", "3", etc.). |
| """ |
| |
| return tuple(map(int, (version.split(".")))) |
| |
| |
| def get_python_version(): |
| r""" |
| Get and return the python version. |
| """ |
| |
| sys_version = sys.version |
| # Strip out any revision code data (e.g. "3.6.3rc1" will become "3.6.3"). |
| sys_version = re.sub("rc[^ ]+", "", sys_version).split(" ")[0] |
| # Remove any non-numerics, etc. (e.g. "2.7.15+" becomes ""2.7.15"). |
| return re.sub("[^0-9\\.]", "", sys_version) |
| |
| |
| python_version = version_tuple(get_python_version()) |
| ordered_dict_version = version_tuple("3.6") |
| |
| |
| def create_temp_file_path(delim=":", suffix=""): |
| r""" |
| Create a temporary file path and return it. |
| |
| This function is appropriate for users who with to create a temporary file and: |
| 1) Have control over when and whether the file is deleted. |
| 2) Have the name of the file indicate information such as program name, function name, line, pid, etc. |
| This can be an aid in debugging, cleanup, etc. |
| |
| The dir path portion of the file path will be /tmp/<username>/. This function will create this directory |
| if it doesn't already exist. |
| |
| This function will NOT create the file. The file will NOT automatically get deleted. It is the |
| responsibility of the caller to dispose of it. |
| |
| Example: |
| |
| pgm123.py is run by user 'joe'. It calls func1 which contains this code: |
| |
| temp_file_path = create_temp_file_path(suffix='suffix1') |
| print_var(temp_file_path) |
| |
| Output: |
| |
| temp_file_path: /tmp/joe/pgm123.py:func1:line_55:pid_8199:831848:suffix1 |
| |
| Description of argument(s): |
| delim A delimiter to be used to separate the sub-components of the file name. |
| suffix A suffix to include as the last sub-component of the file name. |
| """ |
| |
| temp_dir_path = "/tmp/" + username() + "/" |
| try: |
| os.mkdir(temp_dir_path) |
| except FileExistsError: |
| pass |
| |
| callers_stack_frame = inspect.stack()[1] |
| file_name_elements = [ |
| gp.pgm_name, |
| callers_stack_frame.function, |
| "line_" + str(callers_stack_frame.lineno), |
| "pid_" + str(os.getpid()), |
| str(random.randint(0, 1000000)), |
| suffix, |
| ] |
| temp_file_name = delim.join(file_name_elements) |
| |
| temp_file_path = temp_dir_path + temp_file_name |
| |
| return temp_file_path |
| |
| |
| def pause(message="Hit enter to continue..."): |
| r""" |
| Print the message, with time stamp, and pause until the user hits enter. |
| |
| Description of argument(s): |
| message The message to be printed to stdout. |
| """ |
| gp.print_time(message) |
| try: |
| input() |
| except SyntaxError: |
| pass |
| |
| return |