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()
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    bash_special_chars = set(' $')
371
372    if any((char in bash_special_chars) for char in parm):
373        return "'" + parm + "'"
374
375    return parm
376
377
378def get_host_name_ip(host=None,
379                     short_name=0):
380    r"""
381    Get the host name and the IP address for the given host and return them as
382    a tuple.
383
384    Description of argument(s):
385    host                            The host name or IP address to be obtained.
386    short_name                      Include the short host name in the
387                                    returned tuple, i.e. return host, ip and
388                                    short_host.
389    """
390
391    host = dft(host, socket.gethostname())
392    host_name = socket.getfqdn(host)
393    try:
394        host_ip = socket.gethostbyname(host)
395    except socket.gaierror as my_gaierror:
396        message = "Unable to obtain the host name for the following host:" +\
397                  "\n" + gp.sprint_var(host)
398        gp.print_error_report(message)
399        raise my_gaierror
400
401    if short_name:
402        host_short_name = host_name.split(".")[0]
403        return host_name, host_ip, host_short_name
404    else:
405        return host_name, host_ip
406
407
408def pid_active(pid):
409    r"""
410    Return true if pid represents an active pid and false otherwise.
411
412    Description of argument(s):
413    pid                             The pid whose status is being sought.
414    """
415
416    try:
417        os.kill(int(pid), 0)
418    except OSError as err:
419        if err.errno == errno.ESRCH:
420            # ESRCH == No such process
421            return False
422        elif err.errno == errno.EPERM:
423            # EPERM clearly means there's a process to deny access to
424            return True
425        else:
426            # According to "man 2 kill" possible error values are
427            # (EINVAL, EPERM, ESRCH)
428            raise
429
430    return True
431
432
433def to_signed(number,
434              bit_width=None):
435    r"""
436    Convert number to a signed number and return the result.
437
438    Examples:
439
440    With the following code:
441
442    var1 = 0xfffffffffffffff1
443    print_var(var1)
444    print_var(var1, 1)
445    var1 = to_signed(var1)
446    print_var(var1)
447    print_var(var1, 1)
448
449    The following is written to stdout:
450    var1:  18446744073709551601
451    var1:  0x00000000fffffffffffffff1
452    var1:  -15
453    var1:  0xfffffffffffffff1
454
455    The same code but with var1 set to 0x000000000000007f produces the
456    following:
457    var1:  127
458    var1:  0x000000000000007f
459    var1:  127
460    var1:  0x000000000000007f
461
462    Description of argument(s):
463    number                          The number to be converted.
464    bit_width                       The number of bits that defines a complete
465                                    hex value.  Typically, this would be a
466                                    multiple of 32.
467    """
468
469    if bit_width is None:
470        try:
471            bit_width = gp.bit_length(long(sys.maxsize)) + 1
472        except NameError:
473            bit_width = gp.bit_length(int(sys.maxsize)) + 1
474
475    if number < 0:
476        return number
477    neg_bit_mask = 2**(bit_width - 1)
478    if number & neg_bit_mask:
479        return ((2**bit_width) - number) * -1
480    else:
481        return number
482
483
484def get_child_pids(quiet=1):
485
486    r"""
487    Get and return a list of pids representing all first-generation processes
488    that are the children of the current process.
489
490    Example:
491
492    children = get_child_pids()
493    print_var(children)
494
495    Output:
496    children:
497      children[0]:           9123
498
499    Description of argument(s):
500    quiet                           Display output to stdout detailing how
501                                    this child pids are obtained.
502    """
503
504    if psutil_imported:
505        # If "import psutil" worked, find child pids using psutil.
506        current_process = psutil.Process()
507        return [x.pid for x in current_process.children(recursive=False)]
508    else:
509        # Otherwise, find child pids using shell commands.
510        print_output = not quiet
511
512        ps_cmd_buf = "ps --no-headers --ppid " + str(os.getpid()) +\
513            " -o pid,args"
514        # Route the output of ps to a temporary file for later grepping.
515        # Avoid using " | grep" in the ps command string because it creates
516        # yet another process which is of no interest to the caller.
517        temp = tempfile.NamedTemporaryFile()
518        temp_file_path = temp.name
519        gc.shell_cmd(ps_cmd_buf + " > " + temp_file_path,
520                     print_output=print_output)
521        # Sample contents of the temporary file:
522        # 30703 sleep 2
523        # 30795 /bin/bash -c ps --no-headers --ppid 30672 -o pid,args >
524        # /tmp/tmpqqorWY
525        # Use egrep to exclude the "ps" process itself from the results
526        # collected with the prior shell_cmd invocation.  Only the other
527        # children are of interest to the caller.  Use cut on the grep results
528        # to obtain only the pid column.
529        rc, output = \
530            gc.shell_cmd("egrep -v '" + re.escape(ps_cmd_buf) + "' "
531                         + temp_file_path + " | cut -c1-5",
532                         print_output=print_output)
533        # Split the output buffer by line into a list.  Strip each element of
534        # extra spaces and convert each element to an integer.
535        return map(int, map(str.strip, filter(None, output.split("\n"))))
536
537
538def json_loads_multiple(buffer):
539    r"""
540    Convert the contents of the buffer to a JSON array, run json.loads() on it
541    and return the result.
542
543    The buffer is expected to contain one or more JSON objects.
544
545    Description of argument(s):
546    buffer                          A string containing several JSON objects.
547    """
548
549    # Any line consisting of just "}", which indicates the end of an object,
550    # should have a comma appended.
551    regex = "([\\r\\n])[\\}]([\\r\\n])"
552    buffer = re.sub(regex, "\\1},\\2", buffer, 1)
553    # Remove the comma from after the final object and place the whole buffer
554    # inside square brackets.
555    buffer = "[" + re.sub(",([\r\n])$", "\\1}", buffer, 1) + "]"
556    if gp.robot_env:
557        return json.loads(buffer, object_pairs_hook=DotDict)
558    else:
559        return json.loads(buffer, object_pairs_hook=collections.OrderedDict)
560
561
562def file_date_time_stamp():
563    r"""
564    Return a date/time stamp in the following format: yymmdd.HHMMSS
565
566    This value is suitable for including in file names.  Example
567    file1.181001.171716.status
568    """
569
570    return time.strftime("%y%m%d.%H%M%S", time.localtime(time.time()))
571