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