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