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