blob: a2129c1475d3bfa16090c215ba5f82eb894906e2 [file] [log] [blame]
Michael Walshde791732016-09-06 14:25:24 -05001#!/usr/bin/env python
2
Michael Walsh7423c012016-10-04 10:27:21 -05003r"""
4This module provides many valuable functions such as my_parm_file.
5"""
Michael Walshde791732016-09-06 14:25:24 -05006
7# sys and os are needed to get the program dir path and program name.
8import sys
Michael Walsheaa16852017-09-19 16:30:43 -05009import errno
Michael Walshde791732016-09-06 14:25:24 -050010import os
Michael Walshdece16c2018-08-07 15:01:05 -050011import collections
Michael Walsh6aa69802018-09-21 16:38:34 -050012import json
Michael Walshdece16c2018-08-07 15:01:05 -050013try:
14 import ConfigParser
15except ImportError:
16 import configparser
George Keishing3b7115a2018-08-02 10:48:17 -050017try:
Michael Walsh61f5e8f2018-08-03 11:16:00 -050018 import StringIO
George Keishing3b7115a2018-08-02 10:48:17 -050019except ImportError:
Michael Walshdece16c2018-08-07 15:01:05 -050020 import io
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060021import re
Michael Walsh1c85bab2017-05-04 14:29:24 -050022import socket
Michael Walsh3b621fe2018-07-24 16:27:53 -050023import tempfile
24try:
25 import psutil
26 psutil_imported = True
27except ImportError:
28 psutil_imported = False
Michael Walshde791732016-09-06 14:25:24 -050029
Michael Walsh7423c012016-10-04 10:27:21 -050030import gen_print as gp
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060031import gen_cmd as gc
32
Michael Walsh93a09f22017-11-13 15:34:46 -060033robot_env = gp.robot_env
34if robot_env:
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060035 from robot.libraries.BuiltIn import BuiltIn
Michael Walshdece16c2018-08-07 15:01:05 -050036 from robot.utils import DotDict
Michael Walsh7423c012016-10-04 10:27:21 -050037
Michael Walshde791732016-09-06 14:25:24 -050038
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060039def add_trailing_slash(dir_path):
Michael Walsh7db77942017-01-10 11:37:06 -060040 r"""
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060041 Add a trailing slash to the directory path if it doesn't already have one
42 and return it.
Michael Walsh7db77942017-01-10 11:37:06 -060043
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060044 Description of arguments:
45 dir_path A directory path.
Michael Walsh7db77942017-01-10 11:37:06 -060046 """
47
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060048 return os.path.normpath(dir_path) + os.path.sep
49
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060050
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060051def which(file_path):
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060052 r"""
53 Find the full path of an executable file and return it.
54
55 The PATH environment variable dictates the results of this function.
56
57 Description of arguments:
58 file_path The relative file path (e.g. "my_file" or
59 "lib/my_file").
60 """
61
62 shell_rc, out_buf = gc.cmd_fnc_u("which " + file_path, quiet=1,
63 print_output=0, show_err=0)
64 if shell_rc != 0:
65 error_message = "Failed to find complete path for file \"" +\
66 file_path + "\".\n"
67 error_message += gp.sprint_var(shell_rc, 1)
68 error_message += out_buf
69 if robot_env:
70 BuiltIn().fail(gp.sprint_error(error_message))
71 else:
72 gp.print_error_report(error_message)
73 return False
74
75 file_path = out_buf.rstrip("\n")
76
77 return file_path
78
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060079
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060080def dft(value, default):
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060081 r"""
82 Return default if value is None. Otherwise, return value.
83
84 This is really just shorthand as shown below.
85
86 dft(value, default)
87
88 vs
89
90 default if value is None else value
91
92 Description of arguments:
93 value The value to be returned.
94 default The default value to return if value is
95 None.
96 """
97
98 return default if value is None else value
99
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600100
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600101def get_mod_global(var_name,
102 default=None,
103 mod_name="__main__"):
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600104 r"""
105 Get module global variable value and return it.
106
107 If we are running in a robot environment, the behavior will default to
108 calling get_variable_value.
109
110 Description of arguments:
111 var_name The name of the variable whose value is
112 sought.
113 default The value to return if the global does not
114 exist.
115 mod_name The name of the module containing the
116 global variable.
117 """
118
119 if robot_env:
120 return BuiltIn().get_variable_value("${" + var_name + "}", default)
121
122 try:
123 module = sys.modules[mod_name]
124 except KeyError:
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500125 gp.print_error_report("Programmer error - The mod_name passed to"
126 + " this function is invalid:\n"
127 + gp.sprint_var(mod_name))
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600128 raise ValueError('Programmer error.')
129
130 if default is None:
131 return getattr(module, var_name)
132 else:
133 return getattr(module, var_name, default)
134
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600135
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600136def global_default(var_value,
137 default=0):
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600138 r"""
139 If var_value is not None, return it. Otherwise, return the global
140 variable of the same name, if it exists. If not, return default.
141
142 This is meant for use by functions needing help assigning dynamic default
143 values to their parms. Example:
144
145 def func1(parm1=None):
146
147 parm1 = global_default(parm1, 0)
148
149 Description of arguments:
150 var_value The value being evaluated.
151 default The value to be returned if var_value is
152 None AND the global variable of the same
153 name does not exist.
154 """
155
156 var_name = gp.get_arg_name(0, 1, stack_frame_ix=2)
157
158 return dft(var_value, get_mod_global(var_name, 0))
Michael Walsh7db77942017-01-10 11:37:06 -0600159
Michael Walsh7db77942017-01-10 11:37:06 -0600160
Michael Walsh7db77942017-01-10 11:37:06 -0600161def set_mod_global(var_value,
162 mod_name="__main__",
163 var_name=None):
Michael Walsh7db77942017-01-10 11:37:06 -0600164 r"""
165 Set a global variable for a given module.
166
167 Description of arguments:
168 var_value The value to set in the variable.
169 mod_name The name of the module whose variable is
170 to be set.
171 var_name The name of the variable to set. This
172 defaults to the name of the variable used
173 for var_value when calling this function.
174 """
175
176 try:
177 module = sys.modules[mod_name]
178 except KeyError:
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500179 gp.print_error_report("Programmer error - The mod_name passed to"
180 + " this function is invalid:\n"
181 + gp.sprint_var(mod_name))
Michael Walsh7db77942017-01-10 11:37:06 -0600182 raise ValueError('Programmer error.')
183
184 if var_name is None:
185 var_name = gp.get_arg_name(None, 1, 2)
186
187 setattr(module, var_name, var_value)
188
Michael Walsh7db77942017-01-10 11:37:06 -0600189
Michael Walshde791732016-09-06 14:25:24 -0500190def my_parm_file(prop_file_path):
Michael Walshde791732016-09-06 14:25:24 -0500191 r"""
192 Read a properties file, put the keys/values into a dictionary and return
193 the dictionary.
194
195 The properties file must have the following format:
196 var_name<= or :>var_value
197 Comment lines (those beginning with a "#") and blank lines are allowed and
198 will be ignored. Leading and trailing single or double quotes will be
199 stripped from the value. E.g.
200 var1="This one"
201 Quotes are stripped so the resulting value for var1 is:
202 This one
203
204 Description of arguments:
Michael Walsh7423c012016-10-04 10:27:21 -0500205 prop_file_path The caller should pass the path to the
206 properties file.
Michael Walshde791732016-09-06 14:25:24 -0500207 """
208
209 # ConfigParser expects at least one section header in the file (or you
210 # get ConfigParser.MissingSectionHeaderError). Properties files don't
211 # need those so I'll write a dummy section header.
212
Michael Walshdece16c2018-08-07 15:01:05 -0500213 try:
214 string_file = StringIO.StringIO()
215 except NameError:
216 string_file = io.StringIO()
217
Michael Walshde791732016-09-06 14:25:24 -0500218 # Write the dummy section header to the string file.
219 string_file.write('[dummysection]\n')
220 # Write the entire contents of the properties file to the string file.
221 string_file.write(open(prop_file_path).read())
222 # Rewind the string file.
223 string_file.seek(0, os.SEEK_SET)
224
225 # Create the ConfigParser object.
Michael Walshdece16c2018-08-07 15:01:05 -0500226 try:
227 config_parser = ConfigParser.ConfigParser()
228 except NameError:
229 config_parser = configparser.ConfigParser()
Michael Walshde791732016-09-06 14:25:24 -0500230 # Make the property names case-sensitive.
231 config_parser.optionxform = str
232 # Read the properties from the string file.
233 config_parser.readfp(string_file)
234 # Return the properties as a dictionary.
Michael Walshdece16c2018-08-07 15:01:05 -0500235 if robot_env:
236 return DotDict(config_parser.items('dummysection'))
237 else:
238 return collections.OrderedDict(config_parser.items('dummysection'))
Michael Walshde791732016-09-06 14:25:24 -0500239
Michael Walsh7423c012016-10-04 10:27:21 -0500240
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600241def file_to_list(file_path,
242 newlines=0,
243 comments=1,
244 trim=0):
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600245 r"""
246 Return the contents of a file as a list. Each element of the resulting
247 list is one line from the file.
248
249 Description of arguments:
250 file_path The path to the file (relative or
251 absolute).
252 newlines Include newlines from the file in the
253 results.
254 comments Include comment lines and blank lines in
255 the results. Comment lines are any that
256 begin with 0 or more spaces followed by
257 the pound sign ("#").
258 trim Trim white space from the beginning and
259 end of each line.
260 """
261
262 lines = []
263 file = open(file_path)
264 for line in file:
265 if not comments:
266 if re.match(r"[ ]*#|^$", line):
267 continue
268 if not newlines:
269 line = line.rstrip("\n")
270 if trim:
271 line = line.strip()
272 lines.append(line)
273
274 return lines
275
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600276
Michael Walsh7423c012016-10-04 10:27:21 -0500277def return_path_list():
Michael Walsh7423c012016-10-04 10:27:21 -0500278 r"""
279 This function will split the PATH environment variable into a PATH_LIST
280 and return it. Each element in the list will be normalized and have a
281 trailing slash added.
282 """
283
284 PATH_LIST = os.environ['PATH'].split(":")
285 PATH_LIST = [os.path.normpath(path) + os.sep for path in PATH_LIST]
286
287 return PATH_LIST
288
Michael Walsh7db77942017-01-10 11:37:06 -0600289
Michael Walsh9fac55c2017-09-29 16:53:56 -0500290def escape_bash_quotes(buffer):
Michael Walsh9fac55c2017-09-29 16:53:56 -0500291 r"""
292 Escape quotes in string and return it.
293
294 The escape style implemented will be for use on the bash command line.
295
296 Example:
297 That's all.
298
299 Result:
300 That'\''s all.
301
302 The result may then be single quoted on a bash command. Example:
303
304 echo 'That'\''s all.'
305
306 Description of argument(s):
307 buffer The string whose quotes are to be escaped.
308 """
309
310 return re.sub("\'", "\'\\\'\'", buffer)
311
312
Michael Walsh7db77942017-01-10 11:37:06 -0600313def quote_bash_parm(parm):
Michael Walsh7db77942017-01-10 11:37:06 -0600314 r"""
315 Return the bash command line parm with single quotes if they are needed.
316
317 Description of arguments:
318 parm The string to be quoted.
319 """
320
321 # If any of these characters are found in the parm string, then the
322 # string should be quoted. This list is by no means complete and should
323 # be expanded as needed by the developer of this function.
324 bash_special_chars = set(' $')
325
326 if any((char in bash_special_chars) for char in parm):
327 return "'" + parm + "'"
328
329 return parm
330
Michael Walsh1c85bab2017-05-04 14:29:24 -0500331
Michael Walsh74427232018-08-31 12:54:24 -0500332def get_host_name_ip(host=None,
Michael Walshf74b3e42018-01-10 11:11:54 -0600333 short_name=0):
Michael Walsh1c85bab2017-05-04 14:29:24 -0500334 r"""
335 Get the host name and the IP address for the given host and return them as
336 a tuple.
337
338 Description of argument(s):
Michael Walshd1b6c702017-05-30 17:54:30 -0500339 host The host name or IP address to be obtained.
Michael Walshf74b3e42018-01-10 11:11:54 -0600340 short_name Include the short host name in the
341 returned tuple, i.e. return host, ip and
342 short_host.
Michael Walsh1c85bab2017-05-04 14:29:24 -0500343 """
344
Michael Walsh74427232018-08-31 12:54:24 -0500345 host = dft(host, socket.gethostname())
Michael Walshf74b3e42018-01-10 11:11:54 -0600346 host_name = socket.getfqdn(host)
Michael Walshd1b6c702017-05-30 17:54:30 -0500347 try:
348 host_ip = socket.gethostbyname(host)
349 except socket.gaierror as my_gaierror:
350 message = "Unable to obtain the host name for the following host:" +\
351 "\n" + gp.sprint_var(host)
352 gp.print_error_report(message)
353 raise my_gaierror
Michael Walsh1c85bab2017-05-04 14:29:24 -0500354
Michael Walshf74b3e42018-01-10 11:11:54 -0600355 if short_name:
356 host_short_name = host_name.split(".")[0]
357 return host_name, host_ip, host_short_name
358 else:
359 return host_name, host_ip
Michael Walsh1c85bab2017-05-04 14:29:24 -0500360
Michael Walsheaa16852017-09-19 16:30:43 -0500361
362def pid_active(pid):
Michael Walsheaa16852017-09-19 16:30:43 -0500363 r"""
364 Return true if pid represents an active pid and false otherwise.
365
366 Description of argument(s):
367 pid The pid whose status is being sought.
368 """
369
370 try:
371 os.kill(int(pid), 0)
372 except OSError as err:
373 if err.errno == errno.ESRCH:
374 # ESRCH == No such process
375 return False
376 elif err.errno == errno.EPERM:
377 # EPERM clearly means there's a process to deny access to
378 return True
379 else:
380 # According to "man 2 kill" possible error values are
381 # (EINVAL, EPERM, ESRCH)
382 raise
383
384 return True
Michael Walsh112c3592018-06-01 14:15:58 -0500385
386
387def to_signed(number,
Michael Walshdece16c2018-08-07 15:01:05 -0500388 bit_width=None):
Michael Walsh112c3592018-06-01 14:15:58 -0500389 r"""
390 Convert number to a signed number and return the result.
391
392 Examples:
393
394 With the following code:
395
396 var1 = 0xfffffffffffffff1
397 print_var(var1)
398 print_var(var1, 1)
399 var1 = to_signed(var1)
400 print_var(var1)
401 print_var(var1, 1)
402
403 The following is written to stdout:
404 var1: 18446744073709551601
405 var1: 0x00000000fffffffffffffff1
406 var1: -15
407 var1: 0xfffffffffffffff1
408
409 The same code but with var1 set to 0x000000000000007f produces the
410 following:
411 var1: 127
412 var1: 0x000000000000007f
413 var1: 127
414 var1: 0x000000000000007f
415
416 Description of argument(s):
417 number The number to be converted.
418 bit_width The number of bits that defines a complete
419 hex value. Typically, this would be a
420 multiple of 32.
421 """
422
Michael Walshdece16c2018-08-07 15:01:05 -0500423 if bit_width is None:
424 try:
425 bit_width = gp.bit_length(long(sys.maxsize)) + 1
426 except NameError:
427 bit_width = gp.bit_length(int(sys.maxsize)) + 1
428
Michael Walsh112c3592018-06-01 14:15:58 -0500429 if number < 0:
430 return number
431 neg_bit_mask = 2**(bit_width - 1)
432 if number & neg_bit_mask:
433 return ((2**bit_width) - number) * -1
434 else:
435 return number
Michael Walsh3b621fe2018-07-24 16:27:53 -0500436
437
438def get_child_pids(quiet=1):
439
440 r"""
441 Get and return a list of pids representing all first-generation processes
442 that are the children of the current process.
443
444 Example:
445
446 children = get_child_pids()
447 print_var(children)
448
449 Output:
450 children:
451 children[0]: 9123
452
453 Description of argument(s):
454 quiet Display output to stdout detailing how
455 this child pids are obtained.
456 """
457
458 if psutil_imported:
459 # If "import psutil" worked, find child pids using psutil.
460 current_process = psutil.Process()
461 return [x.pid for x in current_process.children(recursive=False)]
462 else:
463 # Otherwise, find child pids using shell commands.
464 print_output = not quiet
465
466 ps_cmd_buf = "ps --no-headers --ppid " + str(os.getpid()) +\
467 " -o pid,args"
468 # Route the output of ps to a temporary file for later grepping.
469 # Avoid using " | grep" in the ps command string because it creates
470 # yet another process which is of no interest to the caller.
471 temp = tempfile.NamedTemporaryFile()
472 temp_file_path = temp.name
473 gc.shell_cmd(ps_cmd_buf + " > " + temp_file_path,
474 print_output=print_output)
475 # Sample contents of the temporary file:
476 # 30703 sleep 2
477 # 30795 /bin/bash -c ps --no-headers --ppid 30672 -o pid,args >
478 # /tmp/tmpqqorWY
479 # Use egrep to exclude the "ps" process itself from the results
480 # collected with the prior shell_cmd invocation. Only the other
481 # children are of interest to the caller. Use cut on the grep results
482 # to obtain only the pid column.
483 rc, output = \
484 gc.shell_cmd("egrep -v '" + re.escape(ps_cmd_buf) + "' "
485 + temp_file_path + " | cut -c1-5",
486 print_output=print_output)
487 # Split the output buffer by line into a list. Strip each element of
488 # extra spaces and convert each element to an integer.
489 return map(int, map(str.strip, filter(None, output.split("\n"))))
Michael Walsh6aa69802018-09-21 16:38:34 -0500490
491
492def json_loads_multiple(buffer):
493 r"""
494 Convert the contents of the buffer to a JSON array, run json.loads() on it
495 and return the result.
496
497 The buffer is expected to contain one or more JSON objects.
498
499 Description of argument(s):
500 buffer A string containing several JSON objects.
501 """
502
503 # Any line consisting of just "}", which indicates the end of an object,
504 # should have a comma appended.
505 regex = "([\\r\\n])[\\}]([\\r\\n])"
506 buffer = re.sub(regex, "\\1},\\2", buffer, 1)
507 # Remove the comma from after the final object and place the whole buffer
508 # inside square brackets.
509 buffer = "[" + re.sub(",([\r\n])$", "\\1}", buffer, 1) + "]"
510 if gp.robot_env:
511 return json.loads(buffer, object_pairs_hook=DotDict)
512 else:
513 return json.loads(buffer, object_pairs_hook=collections.OrderedDict)