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