1#!/usr/bin/env python3
2
3r"""
4This module provides many valuable functions such as my_parm_file.
5"""
6
7# sys and os are needed to get the program dir path and program name.
8import sys
9import errno
10import os
11import shutil
12import collections
13import json
14import time
15import inspect
16import random
17try:
18    import ConfigParser
19except ImportError:
20    import configparser
21try:
22    import StringIO
23except ImportError:
24    import io
25import re
26import socket
27import tempfile
28try:
29    import psutil
30    psutil_imported = True
31except ImportError:
32    psutil_imported = False
33
34import gen_print as gp
35import gen_cmd as gc
36
37robot_env = gp.robot_env
38if robot_env:
39    from robot.libraries.BuiltIn import BuiltIn
40    from robot.utils import DotDict
41
42
43def add_trailing_slash(dir_path):
44    r"""
45    Add a trailing slash to the directory path if it doesn't already have one
46    and return it.
47
48    Description of arguments:
49    dir_path                        A directory path.
50    """
51
52    return os.path.normpath(dir_path) + os.path.sep
53
54
55def makedirs(path, mode=0o777, quiet=None):
56    r"""
57    Call os.makedirs with the caller's arguments.
58
59    This function offers 2 advantages over the base os.makedirs function:
60    1) It will not fail if the directory already exists.
61    2) It will print an "Issuing: os.makedirs" message.
62
63    Description of argument(s):
64    path                            The path containing the directories to be created.
65    mode                            The mode or permissions to be granted to the created directories.
66    quiet                           Indicates whether this function should run the print_issuing() function.
67    """
68    quiet = int(dft(quiet, gp.get_stack_var('quiet', 0)))
69    gp.qprint_issuing("os.makedirs('" + path + "', mode=" + oct(mode) + ")")
70    try:
71        os.makedirs(path, mode)
72    except OSError:
73        pass
74
75
76def rmtree(path, ignore_errors=False, onerror=None, quiet=None):
77    r"""
78    Call shutil.rmtree with the caller's arguments.
79
80    This function offers this advantage over the base function:
81    - It will print an "Issuing: shutil.rmtree" message.
82
83    Description of argument(s):
84    (All parms are passed directly to shutil.rmtree.  See its prolog for details)
85    quiet                           Indicates whether this function should run the print_issuing() function.
86    """
87    quiet = int(dft(quiet, gp.get_stack_var('quiet', 0)))
88    print_string = gp.sprint_executing(max_width=2000)
89    print_string = re.sub(r"Executing: ", "Issuing: shutil.", print_string.rstrip("\n"))
90    gp.qprintn(re.sub(r", quiet[ ]?=.*", ")", print_string))
91    shutil.rmtree(path, ignore_errors, onerror)
92
93
94def chdir(path, quiet=None):
95    r"""
96    Call os.chdir with the caller's arguments.
97
98    This function offers this advantage over the base os.chdir function:
99    - It will print an "Issuing: os.chdir" message.
100
101    Description of argument(s):
102    path                            The path of the directory to change to.
103    quiet                           Indicates whether this function should run the print_issuing() function.
104    """
105    quiet = int(dft(quiet, gp.get_stack_var('quiet', 0)))
106    gp.qprint_issuing("os.chdir('" + path + "')")
107    os.chdir(path)
108
109
110def which(file_path):
111    r"""
112    Find the full path of an executable file and return it.
113
114    The PATH environment variable dictates the results of this function.
115
116    Description of arguments:
117    file_path                       The relative file path (e.g. "my_file" or "lib/my_file").
118    """
119
120    shell_rc, out_buf = gc.cmd_fnc_u("which " + file_path, quiet=1,
121                                     print_output=0, show_err=0)
122    if shell_rc != 0:
123        error_message = "Failed to find complete path for file \"" +\
124                        file_path + "\".\n"
125        error_message += gp.sprint_var(shell_rc, gp.hexa())
126        error_message += out_buf
127        if robot_env:
128            BuiltIn().fail(gp.sprint_error(error_message))
129        else:
130            gp.print_error_report(error_message)
131            return False
132
133    file_path = out_buf.rstrip("\n")
134
135    return file_path
136
137
138def add_path(new_path,
139             path,
140             position=0):
141    r"""
142    Add new_path to path, provided that path doesn't already contain new_path, and return the result.
143
144    Example:
145    If PATH has a value of "/bin/user:/lib/user".  The following code:
146
147    PATH = add_path("/tmp/new_path", PATH)
148
149    will change PATH to "/tmp/new_path:/bin/user:/lib/user".
150
151    Description of argument(s):
152    new_path                        The path to be added.  This function will strip the trailing slash.
153    path                            The path value to which the new_path should be added.
154    position                        The position in path where the new_path should be added.  0 means it
155                                    should be added to the beginning, 1 means add it as the 2nd item, etc.
156                                    sys.maxsize means it should be added to the end.
157    """
158
159    path_list = list(filter(None, path.split(":")))
160    new_path = new_path.rstrip("/")
161    if new_path not in path_list:
162        path_list.insert(int(position), new_path)
163    return ":".join(path_list)
164
165
166def dft(value, default):
167    r"""
168    Return default if value is None.  Otherwise, return value.
169
170    This is really just shorthand as shown below.
171
172    dft(value, default)
173
174    vs
175
176    default if value is None else value
177
178    Description of arguments:
179    value                           The value to be returned.
180    default                         The default value to return if value is None.
181    """
182
183    return default if value is None else value
184
185
186def get_mod_global(var_name,
187                   default=None,
188                   mod_name="__main__"):
189    r"""
190    Get module global variable value and return it.
191
192    If we are running in a robot environment, the behavior will default to
193    calling get_variable_value.
194
195    Description of arguments:
196    var_name                        The name of the variable whose value is sought.
197    default                         The value to return if the global does not exist.
198    mod_name                        The name of the module containing the global variable.
199    """
200
201    if robot_env:
202        return BuiltIn().get_variable_value("${" + var_name + "}", default)
203
204    try:
205        module = sys.modules[mod_name]
206    except KeyError:
207        gp.print_error_report("Programmer error - The mod_name passed to"
208                              + " this function is invalid:\n"
209                              + gp.sprint_var(mod_name))
210        raise ValueError('Programmer error.')
211
212    if default is None:
213        return getattr(module, var_name)
214    else:
215        return getattr(module, var_name, default)
216
217
218def global_default(var_value,
219                   default=0):
220    r"""
221    If var_value is not None, return it.  Otherwise, return the global
222    variable of the same name, if it exists.  If not, return default.
223
224    This is meant for use by functions needing help assigning dynamic default
225    values to their parms.  Example:
226
227    def func1(parm1=None):
228
229        parm1 = global_default(parm1, 0)
230
231    Description of arguments:
232    var_value                       The value being evaluated.
233    default                         The value to be returned if var_value is None AND the global variable of
234                                    the same name does not exist.
235    """
236
237    var_name = gp.get_arg_name(0, 1, stack_frame_ix=2)
238
239    return dft(var_value, get_mod_global(var_name, 0))
240
241
242def set_mod_global(var_value,
243                   mod_name="__main__",
244                   var_name=None):
245    r"""
246    Set a global variable for a given module.
247
248    Description of arguments:
249    var_value                       The value to set in the variable.
250    mod_name                        The name of the module whose variable is to be set.
251    var_name                        The name of the variable to set.  This defaults to the name of the
252                                    variable used for var_value when calling this function.
253    """
254
255    try:
256        module = sys.modules[mod_name]
257    except KeyError:
258        gp.print_error_report("Programmer error - The mod_name passed to"
259                              + " this function is invalid:\n"
260                              + gp.sprint_var(mod_name))
261        raise ValueError('Programmer error.')
262
263    if var_name is None:
264        var_name = gp.get_arg_name(None, 1, 2)
265
266    setattr(module, var_name, var_value)
267
268
269def my_parm_file(prop_file_path):
270    r"""
271    Read a properties file, put the keys/values into a dictionary and return the dictionary.
272
273    The properties file must have the following format:
274    var_name<= or :>var_value
275    Comment lines (those beginning with a "#") and blank lines are allowed and will be ignored.  Leading and
276    trailing single or double quotes will be stripped from the value.  E.g.
277    var1="This one"
278    Quotes are stripped so the resulting value for var1 is:
279    This one
280
281    Description of arguments:
282    prop_file_path                  The caller should pass the path to the properties file.
283    """
284
285    # ConfigParser expects at least one section header in the file (or you get
286    # ConfigParser.MissingSectionHeaderError).  Properties files don't need those so I'll write a dummy
287    # section header.
288
289    try:
290        string_file = StringIO.StringIO()
291    except NameError:
292        string_file = io.StringIO()
293
294    # Write the dummy section header to the string file.
295    string_file.write('[dummysection]\n')
296    # Write the entire contents of the properties file to the string file.
297    string_file.write(open(prop_file_path).read())
298    # Rewind the string file.
299    string_file.seek(0, os.SEEK_SET)
300
301    # Create the ConfigParser object.
302    try:
303        config_parser = ConfigParser.ConfigParser()
304    except NameError:
305        config_parser = configparser.ConfigParser(strict=False)
306    # Make the property names case-sensitive.
307    config_parser.optionxform = str
308    # Read the properties from the string file.
309    config_parser.readfp(string_file)
310    # Return the properties as a dictionary.
311    if robot_env:
312        return DotDict(config_parser.items('dummysection'))
313    else:
314        return collections.OrderedDict(config_parser.items('dummysection'))
315
316
317def file_to_list(file_path,
318                 newlines=0,
319                 comments=1,
320                 trim=0):
321    r"""
322    Return the contents of a file as a list.  Each element of the resulting
323    list is one line from the file.
324
325    Description of arguments:
326    file_path                       The path to the file (relative or absolute).
327    newlines                        Include newlines from the file in the results.
328    comments                        Include comment lines and blank lines in the results.  Comment lines are
329                                    any that begin with 0 or more spaces followed by the pound sign ("#").
330    trim                            Trim white space from the beginning and end of each line.
331    """
332
333    lines = []
334    file = open(file_path)
335    for line in file:
336        if not comments:
337            if re.match(r"[ ]*#|^$", line):
338                continue
339        if not newlines:
340            line = line.rstrip("\n")
341        if trim:
342            line = line.strip()
343        lines.append(line)
344    file.close()
345
346    return lines
347
348
349def file_to_str(*args, **kwargs):
350    r"""
351    Return the contents of a file as a string.
352
353    Description of arguments:
354    See file_to_list defined above for description of arguments.
355    """
356
357    return '\n'.join(file_to_list(*args, **kwargs))
358
359
360def append_file(file_path, buffer):
361    r"""
362    Append the data in buffer to the file named in file_path.
363
364    Description of argument(s):
365    file_path                       The path to a file (e.g. "/tmp/root/file1").
366    buffer                          The buffer of data to be written to the file (e.g. "this and that").
367    """
368
369    with open(file_path, "a") as file:
370        file.write(buffer)
371
372
373def return_path_list():
374    r"""
375    This function will split the PATH environment variable into a PATH_LIST and return it.  Each element in
376    the list will be normalized and have a trailing slash added.
377    """
378
379    PATH_LIST = os.environ['PATH'].split(":")
380    PATH_LIST = [os.path.normpath(path) + os.sep for path in PATH_LIST]
381
382    return PATH_LIST
383
384
385def escape_bash_quotes(buffer):
386    r"""
387    Escape quotes in string and return it.
388
389    The escape style implemented will be for use on the bash command line.
390
391    Example:
392    That's all.
393
394    Result:
395    That'\''s all.
396
397    The result may then be single quoted on a bash command.  Example:
398
399    echo 'That'\''s all.'
400
401    Description of argument(s):
402    buffer                          The string whose quotes are to be escaped.
403    """
404
405    return re.sub("\'", "\'\\\'\'", buffer)
406
407
408def quote_bash_parm(parm):
409    r"""
410    Return the bash command line parm with single quotes if they are needed.
411
412    Description of arguments:
413    parm                            The string to be quoted.
414    """
415
416    # If any of these characters are found in the parm string, then the string should be quoted.  This list
417    # is by no means complete and should be expanded as needed by the developer of this function.
418    # Spaces
419    # Single or double quotes.
420    # Bash variables (therefore, any string with a "$" may need quoting).
421    # Glob characters: *, ?, []
422    # Extended Glob characters: +, @, !
423    # Bash brace expansion: {}
424    # Tilde expansion: ~
425    # Piped commands: |
426    # Bash re-direction: >, <
427    bash_special_chars = set(' \'"$*?[]+@!{}~|><')
428
429    if any((char in bash_special_chars) for char in parm):
430        return "'" + escape_bash_quotes(parm) + "'"
431
432    if parm == '':
433        parm = "''"
434
435    return parm
436
437
438def get_host_name_ip(host=None,
439                     short_name=0):
440    r"""
441    Get the host name and the IP address for the given host and return them as a tuple.
442
443    Description of argument(s):
444    host                            The host name or IP address to be obtained.
445    short_name                      Include the short host name in the returned tuple, i.e. return host, ip
446                                    and short_host.
447    """
448
449    host = dft(host, socket.gethostname())
450    host_name = socket.getfqdn(host)
451    try:
452        host_ip = socket.gethostbyname(host)
453    except socket.gaierror as my_gaierror:
454        message = "Unable to obtain the host name for the following host:" +\
455                  "\n" + gp.sprint_var(host)
456        gp.print_error_report(message)
457        raise my_gaierror
458
459    if short_name:
460        host_short_name = host_name.split(".")[0]
461        return host_name, host_ip, host_short_name
462    else:
463        return host_name, host_ip
464
465
466def pid_active(pid):
467    r"""
468    Return true if pid represents an active pid and false otherwise.
469
470    Description of argument(s):
471    pid                             The pid whose status is being sought.
472    """
473
474    try:
475        os.kill(int(pid), 0)
476    except OSError as err:
477        if err.errno == errno.ESRCH:
478            # ESRCH == No such process
479            return False
480        elif err.errno == errno.EPERM:
481            # EPERM clearly means there's a process to deny access to
482            return True
483        else:
484            # According to "man 2 kill" possible error values are
485            # (EINVAL, EPERM, ESRCH)
486            raise
487
488    return True
489
490
491def to_signed(number,
492              bit_width=None):
493    r"""
494    Convert number to a signed number and return the result.
495
496    Examples:
497
498    With the following code:
499
500    var1 = 0xfffffffffffffff1
501    print_var(var1)
502    print_var(var1, hexa())
503    var1 = to_signed(var1)
504    print_var(var1)
505    print_var(var1, hexa())
506
507    The following is written to stdout:
508    var1:  18446744073709551601
509    var1:  0x00000000fffffffffffffff1
510    var1:  -15
511    var1:  0xfffffffffffffff1
512
513    The same code but with var1 set to 0x000000000000007f produces the following:
514    var1:  127
515    var1:  0x000000000000007f
516    var1:  127
517    var1:  0x000000000000007f
518
519    Description of argument(s):
520    number                          The number to be converted.
521    bit_width                       The number of bits that defines a complete hex value.  Typically, this
522                                    would be a multiple of 32.
523    """
524
525    if bit_width is None:
526        try:
527            bit_width = gp.bit_length(long(sys.maxsize)) + 1
528        except NameError:
529            bit_width = gp.bit_length(int(sys.maxsize)) + 1
530
531    if number < 0:
532        return number
533    neg_bit_mask = 2**(bit_width - 1)
534    if number & neg_bit_mask:
535        return ((2**bit_width) - number) * -1
536    else:
537        return number
538
539
540def get_child_pids(quiet=1):
541
542    r"""
543    Get and return a list of pids representing all first-generation processes that are the children of the
544    current process.
545
546    Example:
547
548    children = get_child_pids()
549    print_var(children)
550
551    Output:
552    children:
553      children[0]:           9123
554
555    Description of argument(s):
556    quiet                           Display output to stdout detailing how this child pids are obtained.
557    """
558
559    if psutil_imported:
560        # If "import psutil" worked, find child pids using psutil.
561        current_process = psutil.Process()
562        return [x.pid for x in current_process.children(recursive=False)]
563    else:
564        # Otherwise, find child pids using shell commands.
565        print_output = not quiet
566
567        ps_cmd_buf = "ps --no-headers --ppid " + str(os.getpid()) +\
568            " -o pid,args"
569        # Route the output of ps to a temporary file for later grepping.  Avoid using " | grep" in the ps
570        # command string because it creates yet another process which is of no interest to the caller.
571        temp = tempfile.NamedTemporaryFile()
572        temp_file_path = temp.name
573        gc.shell_cmd(ps_cmd_buf + " > " + temp_file_path,
574                     print_output=print_output)
575        # Sample contents of the temporary file:
576        # 30703 sleep 2
577        # 30795 /bin/bash -c ps --no-headers --ppid 30672 -o pid,args > /tmp/tmpqqorWY
578        # Use egrep to exclude the "ps" process itself from the results collected with the prior shell_cmd
579        # invocation.  Only the other children are of interest to the caller.  Use cut on the grep results to
580        # obtain only the pid column.
581        rc, output = \
582            gc.shell_cmd("egrep -v '" + re.escape(ps_cmd_buf) + "' "
583                         + temp_file_path + " | cut -c1-5",
584                         print_output=print_output)
585        # Split the output buffer by line into a list.  Strip each element of extra spaces and convert each
586        # element to an integer.
587        return map(int, map(str.strip, filter(None, output.split("\n"))))
588
589
590def json_loads_multiple(buffer):
591    r"""
592    Convert the contents of the buffer to a JSON array, run json.loads() on it and return the result.
593
594    The buffer is expected to contain one or more JSON objects.
595
596    Description of argument(s):
597    buffer                          A string containing several JSON objects.
598    """
599
600    # Any line consisting of just "}", which indicates the end of an object, should have a comma appended.
601    regex = "([\\r\\n])[\\}]([\\r\\n])"
602    buffer = re.sub(regex, "\\1},\\2", buffer, 1)
603    # Remove the comma from after the final object and place the whole buffer inside square brackets.
604    buffer = "[" + re.sub(",([\r\n])$", "\\1}", buffer, 1) + "]"
605    if gp.robot_env:
606        return json.loads(buffer, object_pairs_hook=DotDict)
607    else:
608        return json.loads(buffer, object_pairs_hook=collections.OrderedDict)
609
610
611def file_date_time_stamp():
612    r"""
613    Return a date/time stamp in the following format: yymmdd.HHMMSS
614
615    This value is suitable for including in file names.  Example file1.181001.171716.status
616    """
617
618    return time.strftime("%y%m%d.%H%M%S", time.localtime(time.time()))
619
620
621def get_function_stack():
622    r"""
623    Return a list of all the function names currently in the call stack.
624
625    This function's name will be at offset 0.  This function's caller's name will be at offset 1 and so on.
626    """
627
628    return [str(stack_frame[3]) for stack_frame in inspect.stack()]
629
630
631def username():
632    r"""
633    Return the username for the current process.
634    """
635
636    username = os.environ.get("USER", "")
637    if username != "":
638        return username
639    user_num = str(os.geteuid())
640    try:
641        username = os.getlogin()
642    except OSError:
643        if user_num == "0":
644            username = "root"
645        else:
646            username = "?"
647
648    return username
649
650
651def version_tuple(version):
652    r"""
653    Convert the version string to a tuple and return it.
654
655    Description of argument(s):
656    version                         A version string whose format is "n[.n]" (e.g. "3.6.3", "3", etc.).
657    """
658
659    return tuple(map(int, (version.split("."))))
660
661
662def get_python_version():
663    r"""
664    Get and return the python version.
665    """
666
667    sys_version = sys.version
668    # Strip out any revision code data (e.g. "3.6.3rc1" will become "3.6.3").
669    sys_version = re.sub("rc[^ ]+", "", sys_version).split(" ")[0]
670    # Remove any non-numerics, etc. (e.g. "2.7.15+" becomes ""2.7.15").
671    return re.sub("[^0-9\\.]", "", sys_version)
672
673
674python_version = \
675    version_tuple(get_python_version())
676ordered_dict_version = version_tuple("3.6")
677
678
679def create_temp_file_path(delim=":", suffix=""):
680    r"""
681    Create a temporary file path and return it.
682
683    This function is appropriate for users who with to create a temporary file and:
684    1) Have control over when and whether the file is deleted.
685    2) Have the name of the file indicate information such as program name, function name, line, pid, etc.
686    This can be an aid in debugging, cleanup, etc.
687
688    The dir path portion of the file path will be /tmp/<username>/.  This function will create this directory
689    if it doesn't already exist.
690
691    This function will NOT create the file.  The file will NOT automatically get deleted.  It is the
692    responsibility of the caller to dispose of it.
693
694    Example:
695
696    pgm123.py is run by user 'joe'.  It calls func1 which contains this code:
697
698    temp_file_path = create_temp_file_path(suffix='suffix1')
699    print_var(temp_file_path)
700
701    Output:
702
703    temp_file_path:                 /tmp/joe/pgm123.py:func1:line_55:pid_8199:831848:suffix1
704
705    Description of argument(s):
706    delim                           A delimiter to be used to separate the sub-components of the file name.
707    suffix                          A suffix to include as the last sub-component of the file name.
708    """
709
710    temp_dir_path = "/tmp/" + username() + "/"
711    try:
712        os.mkdir(temp_dir_path)
713    except FileExistsError:
714        pass
715
716    callers_stack_frame = inspect.stack()[1]
717    file_name_elements = \
718        [
719            gp.pgm_name, callers_stack_frame.function, "line_" + str(callers_stack_frame.lineno),
720            "pid_" + str(os.getpid()), str(random.randint(0, 1000000)), suffix
721        ]
722    temp_file_name = delim.join(file_name_elements)
723
724    temp_file_path = temp_dir_path + temp_file_name
725
726    return temp_file_path
727
728
729def pause(message="Hit enter to continue..."):
730    r"""
731    Print the message, with time stamp, and pause until the user hits enter.
732
733    Description of argument(s):
734    message                         The message to be printed to stdout.
735    """
736    gp.print_time(message)
737    try:
738        input()
739    except SyntaxError:
740        pass
741
742    return
743