| George Keishing | e7e9171 | 2021-09-03 11:28:44 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python3 | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 2 |  | 
|  | 3 | r""" | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 4 | This module provides valuable argument processing functions like gen_get_options and sprint_args. | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 5 | """ | 
|  | 6 |  | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 7 | import os | 
|  | 8 | import re | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 9 | import sys | 
|  | 10 |  | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 11 | try: | 
|  | 12 | import psutil | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 13 |  | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 14 | psutil_imported = True | 
|  | 15 | except ImportError: | 
|  | 16 | psutil_imported = False | 
| George Keishing | 3b7115a | 2018-08-02 10:48:17 -0500 | [diff] [blame] | 17 | try: | 
|  | 18 | import __builtin__ | 
|  | 19 | except ImportError: | 
|  | 20 | import builtins as __builtin__ | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 21 |  | 
|  | 22 | import argparse | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 23 | import atexit | 
|  | 24 | import signal | 
| Michael Walsh | 3f70fc5 | 2020-03-27 12:04:24 -0500 | [diff] [blame] | 25 | import textwrap as textwrap | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 26 |  | 
| George Keishing | e635ddc | 2022-12-08 07:38:02 -0600 | [diff] [blame] | 27 | import gen_cmd as gc | 
|  | 28 | import gen_misc as gm | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 29 | import gen_print as gp | 
|  | 30 | import gen_valid as gv | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 31 |  | 
| Michael Walsh | 3f70fc5 | 2020-03-27 12:04:24 -0500 | [diff] [blame] | 32 |  | 
|  | 33 | class MultilineFormatter(argparse.HelpFormatter): | 
|  | 34 | def _fill_text(self, text, width, indent): | 
|  | 35 | r""" | 
|  | 36 | Split text into formatted lines for every "%%n" encountered in the text and return the result. | 
|  | 37 | """ | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 38 | lines = self._whitespace_matcher.sub(" ", text).strip().split("%n") | 
|  | 39 | formatted_lines = [ | 
|  | 40 | textwrap.fill( | 
|  | 41 | x, width, initial_indent=indent, subsequent_indent=indent | 
|  | 42 | ) | 
|  | 43 | + "\n" | 
|  | 44 | for x in lines | 
|  | 45 | ] | 
|  | 46 | return "".join(formatted_lines) | 
| Michael Walsh | 3f70fc5 | 2020-03-27 12:04:24 -0500 | [diff] [blame] | 47 |  | 
|  | 48 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 49 | class ArgumentDefaultsHelpMultilineFormatter( | 
|  | 50 | MultilineFormatter, argparse.ArgumentDefaultsHelpFormatter | 
|  | 51 | ): | 
| Michael Walsh | 3f70fc5 | 2020-03-27 12:04:24 -0500 | [diff] [blame] | 52 | pass | 
|  | 53 |  | 
|  | 54 |  | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 55 | default_string = '  The default value is "%(default)s".' | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 56 | module = sys.modules["__main__"] | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 57 |  | 
|  | 58 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 59 | def gen_get_options(parser, stock_list=[]): | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 60 | r""" | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 61 | Parse the command line arguments using the parser object passed and return True/False (i.e. pass/fail). | 
|  | 62 | However, if gv.exit_on_error is set, simply exit the program on failure.  Also set the following built in | 
|  | 63 | values: | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 64 |  | 
|  | 65 | __builtin__.quiet      This value is used by the qprint functions. | 
|  | 66 | __builtin__.test_mode  This value is used by command processing functions. | 
|  | 67 | __builtin__.debug      This value is used by the dprint functions. | 
|  | 68 | __builtin__.arg_obj    This value is used by print_program_header, etc. | 
|  | 69 | __builtin__.parser     This value is used by print_program_header, etc. | 
|  | 70 |  | 
|  | 71 | Description of arguments: | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 72 | parser                          A parser object.  See argparse module documentation for details. | 
|  | 73 | stock_list                      The caller can use this parameter to request certain stock parameters | 
|  | 74 | offered by this function.  For example, this function will define a | 
|  | 75 | "quiet" option upon request.  This includes stop help text and parm | 
|  | 76 | checking.  The stock_list is a list of tuples each of which consists of | 
|  | 77 | an arg_name and a default value.  Example: stock_list = [("test_mode", | 
|  | 78 | 0), ("quiet", 1), ("debug", 0)] | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 79 | """ | 
|  | 80 |  | 
|  | 81 | # This is a list of stock parms that we support. | 
|  | 82 | master_stock_list = ["quiet", "test_mode", "debug", "loglevel"] | 
|  | 83 |  | 
|  | 84 | # Process stock_list. | 
|  | 85 | for ix in range(0, len(stock_list)): | 
|  | 86 | if len(stock_list[ix]) < 1: | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 87 | error_message = ( | 
|  | 88 | "Programmer error - stock_list[" | 
|  | 89 | + str(ix) | 
|  | 90 | + "] is supposed to be a tuple containing at" | 
|  | 91 | + " least one element which is the name of" | 
|  | 92 | + " the desired stock parameter:\n" | 
|  | 93 | + gp.sprint_var(stock_list) | 
|  | 94 | ) | 
| Michael Walsh | 69d58ae | 2018-06-01 15:18:57 -0500 | [diff] [blame] | 95 | return gv.process_error_message(error_message) | 
| Joy Onyerikwu | 004ad3c | 2018-06-11 16:29:56 -0500 | [diff] [blame] | 96 | if isinstance(stock_list[ix], tuple): | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 97 | arg_name = stock_list[ix][0] | 
|  | 98 | default = stock_list[ix][1] | 
|  | 99 | else: | 
|  | 100 | arg_name = stock_list[ix] | 
|  | 101 | default = None | 
|  | 102 |  | 
|  | 103 | if arg_name not in master_stock_list: | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 104 | error_message = ( | 
|  | 105 | 'Programmer error - arg_name "' | 
|  | 106 | + arg_name | 
|  | 107 | + '" not found found in stock list:\n' | 
|  | 108 | + gp.sprint_var(master_stock_list) | 
|  | 109 | ) | 
| Michael Walsh | 69d58ae | 2018-06-01 15:18:57 -0500 | [diff] [blame] | 110 | return gv.process_error_message(error_message) | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 111 |  | 
|  | 112 | if arg_name == "quiet": | 
|  | 113 | if default is None: | 
|  | 114 | default = 0 | 
|  | 115 | parser.add_argument( | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 116 | "--quiet", | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 117 | default=default, | 
|  | 118 | type=int, | 
|  | 119 | choices=[1, 0], | 
| Joy Onyerikwu | 004ad3c | 2018-06-11 16:29:56 -0500 | [diff] [blame] | 120 | help='If this parameter is set to "1", %(prog)s' | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 121 | + " will print only essential information, i.e. it will" | 
|  | 122 | + " not echo parameters, echo commands, print the total" | 
|  | 123 | + " run time, etc." | 
|  | 124 | + default_string, | 
|  | 125 | ) | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 126 | elif arg_name == "test_mode": | 
|  | 127 | if default is None: | 
|  | 128 | default = 0 | 
|  | 129 | parser.add_argument( | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 130 | "--test_mode", | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 131 | default=default, | 
|  | 132 | type=int, | 
|  | 133 | choices=[1, 0], | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 134 | help="This means that %(prog)s should go through all the" | 
|  | 135 | + " motions but not actually do anything substantial." | 
|  | 136 | + "  This is mainly to be used by the developer of" | 
|  | 137 | + " %(prog)s." | 
|  | 138 | + default_string, | 
|  | 139 | ) | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 140 | elif arg_name == "debug": | 
|  | 141 | if default is None: | 
|  | 142 | default = 0 | 
|  | 143 | parser.add_argument( | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 144 | "--debug", | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 145 | default=default, | 
|  | 146 | type=int, | 
|  | 147 | choices=[1, 0], | 
| Joy Onyerikwu | 004ad3c | 2018-06-11 16:29:56 -0500 | [diff] [blame] | 148 | help='If this parameter is set to "1", %(prog)s will print' | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 149 | + " additional debug information.  This is mainly to be" | 
|  | 150 | + " used by the developer of %(prog)s." | 
|  | 151 | + default_string, | 
|  | 152 | ) | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 153 | elif arg_name == "loglevel": | 
|  | 154 | if default is None: | 
|  | 155 | default = "info" | 
|  | 156 | parser.add_argument( | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 157 | "--loglevel", | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 158 | default=default, | 
|  | 159 | type=str, | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 160 | choices=[ | 
|  | 161 | "DEBUG", | 
|  | 162 | "INFO", | 
|  | 163 | "WARNING", | 
|  | 164 | "ERROR", | 
|  | 165 | "CRITICAL", | 
|  | 166 | "debug", | 
|  | 167 | "info", | 
|  | 168 | "warning", | 
|  | 169 | "error", | 
|  | 170 | "critical", | 
|  | 171 | ], | 
| Joy Onyerikwu | 004ad3c | 2018-06-11 16:29:56 -0500 | [diff] [blame] | 172 | help='If this parameter is set to "1", %(prog)s will print' | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 173 | + " additional debug information.  This is mainly to be" | 
|  | 174 | + " used by the developer of %(prog)s." | 
|  | 175 | + default_string, | 
|  | 176 | ) | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 177 |  | 
|  | 178 | arg_obj = parser.parse_args() | 
|  | 179 |  | 
|  | 180 | __builtin__.quiet = 0 | 
|  | 181 | __builtin__.test_mode = 0 | 
|  | 182 | __builtin__.debug = 0 | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 183 | __builtin__.loglevel = "WARNING" | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 184 | for ix in range(0, len(stock_list)): | 
| Joy Onyerikwu | 004ad3c | 2018-06-11 16:29:56 -0500 | [diff] [blame] | 185 | if isinstance(stock_list[ix], tuple): | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 186 | arg_name = stock_list[ix][0] | 
|  | 187 | default = stock_list[ix][1] | 
|  | 188 | else: | 
|  | 189 | arg_name = stock_list[ix] | 
|  | 190 | default = None | 
|  | 191 | if arg_name == "quiet": | 
|  | 192 | __builtin__.quiet = arg_obj.quiet | 
|  | 193 | elif arg_name == "test_mode": | 
|  | 194 | __builtin__.test_mode = arg_obj.test_mode | 
|  | 195 | elif arg_name == "debug": | 
|  | 196 | __builtin__.debug = arg_obj.debug | 
|  | 197 | elif arg_name == "loglevel": | 
|  | 198 | __builtin__.loglevel = arg_obj.loglevel | 
|  | 199 |  | 
|  | 200 | __builtin__.arg_obj = arg_obj | 
|  | 201 | __builtin__.parser = parser | 
|  | 202 |  | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 203 | # For each command line parameter, create a corresponding global variable and assign it the appropriate | 
|  | 204 | # value.  For example, if the command line contained "--last_name='Smith', we'll create a global variable | 
|  | 205 | # named "last_name" with the value "Smith". | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 206 | module = sys.modules["__main__"] | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 207 | for key in arg_obj.__dict__: | 
|  | 208 | setattr(module, key, getattr(__builtin__.arg_obj, key)) | 
|  | 209 |  | 
|  | 210 | return True | 
|  | 211 |  | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 212 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 213 | def set_pgm_arg(var_value, var_name=None): | 
| Michael Walsh | c33ef37 | 2017-01-10 11:46:29 -0600 | [diff] [blame] | 214 | r""" | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 215 | Set the value of the arg_obj.__dict__ entry named in var_name with the var_value provided.  Also, set | 
|  | 216 | corresponding global variable. | 
| Michael Walsh | c33ef37 | 2017-01-10 11:46:29 -0600 | [diff] [blame] | 217 |  | 
|  | 218 | Description of arguments: | 
|  | 219 | var_value                       The value to set in the variable. | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 220 | var_name                        The name of the variable to set.  This defaults to the name of the | 
|  | 221 | variable used for var_value when calling this function. | 
| Michael Walsh | c33ef37 | 2017-01-10 11:46:29 -0600 | [diff] [blame] | 222 | """ | 
|  | 223 |  | 
|  | 224 | if var_name is None: | 
|  | 225 | var_name = gp.get_arg_name(None, 1, 2) | 
|  | 226 |  | 
|  | 227 | arg_obj.__dict__[var_name] = var_value | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 228 | module = sys.modules["__main__"] | 
| Michael Walsh | c33ef37 | 2017-01-10 11:46:29 -0600 | [diff] [blame] | 229 | setattr(module, var_name, var_value) | 
|  | 230 | if var_name == "quiet": | 
|  | 231 | __builtin__.quiet = var_value | 
|  | 232 | elif var_name == "debug": | 
|  | 233 | __builtin__.debug = var_value | 
|  | 234 | elif var_name == "test_mode": | 
|  | 235 | __builtin__.test_mode = var_value | 
|  | 236 |  | 
| Michael Walsh | c33ef37 | 2017-01-10 11:46:29 -0600 | [diff] [blame] | 237 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 238 | def sprint_args(arg_obj, indent=0): | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 239 | r""" | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 240 | sprint_var all of the arguments found in arg_obj and return the result as a string. | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 241 |  | 
|  | 242 | Description of arguments: | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 243 | arg_obj                         An argument object such as is returned by the argparse parse_args() | 
|  | 244 | method. | 
|  | 245 | indent                          The number of spaces to indent each line of output. | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 246 | """ | 
|  | 247 |  | 
| Michael Walsh | 0d5f96a | 2019-05-20 10:09:57 -0500 | [diff] [blame] | 248 | col1_width = gp.dft_col1_width + indent | 
| Michael Walsh | bec416d | 2016-11-10 08:54:52 -0600 | [diff] [blame] | 249 |  | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 250 | buffer = "" | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 251 | for key in arg_obj.__dict__: | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 252 | buffer += gp.sprint_varx( | 
|  | 253 | key, getattr(arg_obj, key), 0, indent, col1_width | 
|  | 254 | ) | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 255 | return buffer | 
|  | 256 |  | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 257 |  | 
| Michael Walsh | d339abf | 2020-02-18 11:28:02 -0600 | [diff] [blame] | 258 | def sync_args(): | 
|  | 259 | r""" | 
|  | 260 | Synchronize the argument values to match their corresponding global variable values. | 
|  | 261 |  | 
|  | 262 | The user's validate_parms() function may manipulate global variables that correspond to program | 
|  | 263 | arguments.  After validate_parms() is called, sync_args is called to set the altered values back into the | 
|  | 264 | arg_obj.  This will ensure that the print-out of program arguments reflects the updated values. | 
|  | 265 |  | 
|  | 266 | Example: | 
|  | 267 |  | 
|  | 268 | def validate_parms(): | 
|  | 269 |  | 
|  | 270 | # Set a default value for dir_path argument. | 
|  | 271 | dir_path = gm.add_trailing_slash(gm.dft(dir_path, os.getcwd())) | 
|  | 272 | """ | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 273 | module = sys.modules["__main__"] | 
| Michael Walsh | d339abf | 2020-02-18 11:28:02 -0600 | [diff] [blame] | 274 | for key in arg_obj.__dict__: | 
|  | 275 | arg_obj.__dict__[key] = getattr(module, key) | 
|  | 276 |  | 
|  | 277 |  | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 278 | term_options = None | 
| Michael Walsh | 5328f38 | 2019-09-13 14:18:55 -0500 | [diff] [blame] | 279 |  | 
|  | 280 |  | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 281 | def set_term_options(**kwargs): | 
|  | 282 | r""" | 
|  | 283 | Set the global term_options. | 
|  | 284 |  | 
|  | 285 | If the global term_options is not None, gen_exit_function() will call terminate_descendants(). | 
|  | 286 |  | 
|  | 287 | Description of arguments(): | 
|  | 288 | kwargs                          Supported keyword options follow: | 
|  | 289 | term_requests               Requests to terminate specified descendants of this program.  The | 
|  | 290 | following values for term_requests are supported: | 
|  | 291 | children                Terminate the direct children of this program. | 
|  | 292 | descendants             Terminate all descendants of this program. | 
|  | 293 | <dictionary>            A dictionary with support for the following keys: | 
|  | 294 | pgm_names           A list of program names which will be used to identify which descendant | 
|  | 295 | processes should be terminated. | 
|  | 296 | """ | 
|  | 297 |  | 
|  | 298 | global term_options | 
|  | 299 | # Validation: | 
|  | 300 | arg_names = list(kwargs.keys()) | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 301 | gv.valid_list(arg_names, ["term_requests"]) | 
|  | 302 | if type(kwargs["term_requests"]) is dict: | 
|  | 303 | keys = list(kwargs["term_requests"].keys()) | 
|  | 304 | gv.valid_list(keys, ["pgm_names"]) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 305 | else: | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 306 | gv.valid_value(kwargs["term_requests"], ["children", "descendants"]) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 307 | term_options = kwargs | 
|  | 308 |  | 
|  | 309 |  | 
|  | 310 | if psutil_imported: | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 311 |  | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 312 | def match_process_by_pgm_name(process, pgm_name): | 
|  | 313 | r""" | 
|  | 314 | Return True or False to indicate whether the process matches the program name. | 
|  | 315 |  | 
|  | 316 | Description of argument(s): | 
|  | 317 | process                     A psutil process object such as the one returned by psutil.Process(). | 
|  | 318 | pgm_name                    The name of a program to look for in the cmdline field of the process | 
|  | 319 | object. | 
|  | 320 | """ | 
|  | 321 |  | 
|  | 322 | # This function will examine elements 0 and 1 of the cmdline field of the process object.  The | 
|  | 323 | # following examples will illustrate the reasons for this: | 
|  | 324 |  | 
|  | 325 | # Example 1: Suppose a process was started like this: | 
|  | 326 |  | 
|  | 327 | # shell_cmd('python_pgm_template --quiet=0', fork=1) | 
|  | 328 |  | 
|  | 329 | # And then this function is called as follows: | 
|  | 330 |  | 
|  | 331 | # match_process_by_pgm_name(process, "python_pgm_template") | 
|  | 332 |  | 
|  | 333 | # The process object might contain the following for its cmdline field: | 
|  | 334 |  | 
|  | 335 | # cmdline: | 
|  | 336 | #   [0]:                       /usr/bin/python | 
|  | 337 | #   [1]:                       /my_path/python_pgm_template | 
|  | 338 | #   [2]:                       --quiet=0 | 
|  | 339 |  | 
|  | 340 | # Because "python_pgm_template" is a python program, the python interpreter (e.g. "/usr/bin/python") | 
|  | 341 | # will appear in entry 0 of cmdline and the python_pgm_template will appear in entry 1 (with a | 
|  | 342 | # qualifying dir path). | 
|  | 343 |  | 
|  | 344 | # Example 2: Suppose a process was started like this: | 
|  | 345 |  | 
|  | 346 | # shell_cmd('sleep 5', fork=1) | 
|  | 347 |  | 
|  | 348 | # And then this function is called as follows: | 
|  | 349 |  | 
|  | 350 | # match_process_by_pgm_name(process, "sleep") | 
|  | 351 |  | 
|  | 352 | # The process object might contain the following for its cmdline field: | 
|  | 353 |  | 
|  | 354 | # cmdline: | 
|  | 355 | #   [0]:                       sleep | 
|  | 356 | #   [1]:                       5 | 
|  | 357 |  | 
|  | 358 | # Because "sleep" is a compiled executable, it will appear in entry 0. | 
|  | 359 |  | 
|  | 360 | optional_dir_path_regex = "(.*/)?" | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 361 | cmdline = process.as_dict()["cmdline"] | 
|  | 362 | return re.match( | 
|  | 363 | optional_dir_path_regex + pgm_name + "( |$)", cmdline[0] | 
|  | 364 | ) or re.match(optional_dir_path_regex + pgm_name + "( |$)", cmdline[1]) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 365 |  | 
|  | 366 | def select_processes_by_pgm_name(processes, pgm_name): | 
|  | 367 | r""" | 
|  | 368 | Select the processes that match pgm_name and return the result as a list of process objects. | 
|  | 369 |  | 
|  | 370 | Description of argument(s): | 
|  | 371 | processes                   A list of psutil process objects such as the one returned by | 
|  | 372 | psutil.Process(). | 
|  | 373 | pgm_name                    The name of a program to look for in the cmdline field of each process | 
|  | 374 | object. | 
|  | 375 | """ | 
|  | 376 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 377 | return [ | 
|  | 378 | process | 
|  | 379 | for process in processes | 
|  | 380 | if match_process_by_pgm_name(process, pgm_name) | 
|  | 381 | ] | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 382 |  | 
|  | 383 | def sprint_process_report(pids): | 
|  | 384 | r""" | 
|  | 385 | Create a process report for the given pids and return it as a string. | 
|  | 386 |  | 
|  | 387 | Description of argument(s): | 
|  | 388 | pids                        A list of process IDs for processes to be included in the report. | 
|  | 389 | """ | 
|  | 390 | report = "\n" | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 391 | cmd_buf = ( | 
|  | 392 | "echo ; ps wwo user,pgrp,pid,ppid,lstart,cmd --forest " | 
|  | 393 | + " ".join(pids) | 
|  | 394 | ) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 395 | report += gp.sprint_issuing(cmd_buf) | 
|  | 396 | rc, outbuf = gc.shell_cmd(cmd_buf, quiet=1) | 
|  | 397 | report += outbuf + "\n" | 
|  | 398 |  | 
|  | 399 | return report | 
|  | 400 |  | 
|  | 401 | def get_descendant_info(process=psutil.Process()): | 
|  | 402 | r""" | 
|  | 403 | Get info about the descendants of the given process and return as a tuple of descendants, | 
|  | 404 | descendant_pids and process_report. | 
|  | 405 |  | 
|  | 406 | descendants will be a list of process objects.  descendant_pids will be a list of pids (in str form) | 
|  | 407 | and process_report will be a report produced by a call to sprint_process_report(). | 
|  | 408 |  | 
|  | 409 | Description of argument(s): | 
|  | 410 | process                     A psutil process object such as the one returned by psutil.Process(). | 
|  | 411 | """ | 
|  | 412 | descendants = process.children(recursive=True) | 
|  | 413 | descendant_pids = [str(process.pid) for process in descendants] | 
|  | 414 | if descendants: | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 415 | process_report = sprint_process_report( | 
|  | 416 | [str(process.pid)] + descendant_pids | 
|  | 417 | ) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 418 | else: | 
|  | 419 | process_report = "" | 
|  | 420 | return descendants, descendant_pids, process_report | 
|  | 421 |  | 
|  | 422 | def terminate_descendants(): | 
|  | 423 | r""" | 
|  | 424 | Terminate descendants of the current process according to the requirements layed out in global | 
|  | 425 | term_options variable. | 
|  | 426 |  | 
|  | 427 | Note: If term_options is not null, gen_exit_function() will automatically call this function. | 
|  | 428 |  | 
|  | 429 | When this function gets called, descendant processes may be running and may be printing to the same | 
|  | 430 | stdout stream being used by this process.  If this function writes directly to stdout, its output can | 
|  | 431 | be interspersed with any output generated by descendant processes.  This makes it very difficult to | 
|  | 432 | interpret the output.  In order solve this problem, the activity of this process will be logged to a | 
|  | 433 | temporary file.  After descendant processes have been terminated successfully, the temporary file | 
|  | 434 | will be printed to stdout and then deleted.  However, if this function should fail to complete (i.e. | 
|  | 435 | get hung waiting for descendants to terminate gracefully), the temporary file will not be deleted and | 
|  | 436 | can be used by the developer for debugging.  If no descendant processes are found, this function will | 
|  | 437 | return before creating the temporary file. | 
|  | 438 |  | 
|  | 439 | Note that a general principal being observed here is that each process is responsible for the | 
|  | 440 | children it produces. | 
|  | 441 | """ | 
|  | 442 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 443 | message = ( | 
|  | 444 | "\n" + gp.sprint_dashes(width=120) + gp.sprint_executing() + "\n" | 
|  | 445 | ) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 446 |  | 
|  | 447 | current_process = psutil.Process() | 
|  | 448 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 449 | descendants, descendant_pids, process_report = get_descendant_info( | 
|  | 450 | current_process | 
|  | 451 | ) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 452 | if not descendants: | 
|  | 453 | # If there are no descendants, then we have nothing to do. | 
|  | 454 | return | 
|  | 455 |  | 
|  | 456 | terminate_descendants_temp_file_path = gm.create_temp_file_path() | 
|  | 457 | gp.print_vars(terminate_descendants_temp_file_path) | 
|  | 458 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 459 | message += ( | 
|  | 460 | gp.sprint_varx("pgm_name", gp.pgm_name) | 
|  | 461 | + gp.sprint_vars(term_options) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 462 | + process_report | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 463 | ) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 464 |  | 
|  | 465 | # Process the termination requests: | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 466 | if term_options["term_requests"] == "children": | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 467 | term_processes = current_process.children(recursive=False) | 
|  | 468 | term_pids = [str(process.pid) for process in term_processes] | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 469 | elif term_options["term_requests"] == "descendants": | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 470 | term_processes = descendants | 
|  | 471 | term_pids = descendant_pids | 
|  | 472 | else: | 
|  | 473 | # Process term requests by pgm_names. | 
|  | 474 | term_processes = [] | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 475 | for pgm_name in term_options["term_requests"]["pgm_names"]: | 
|  | 476 | term_processes.extend( | 
|  | 477 | select_processes_by_pgm_name(descendants, pgm_name) | 
|  | 478 | ) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 479 | term_pids = [str(process.pid) for process in term_processes] | 
|  | 480 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 481 | message += gp.sprint_timen( | 
|  | 482 | "Processes to be terminated:" | 
|  | 483 | ) + gp.sprint_var(term_pids) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 484 | for process in term_processes: | 
|  | 485 | process.terminate() | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 486 | message += gp.sprint_timen( | 
|  | 487 | "Waiting on the following pids: " + " ".join(descendant_pids) | 
|  | 488 | ) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 489 | gm.append_file(terminate_descendants_temp_file_path, message) | 
|  | 490 | psutil.wait_procs(descendants) | 
|  | 491 |  | 
|  | 492 | # Checking after the fact to see whether any descendant processes are still alive.  If so, a process | 
|  | 493 | # report showing this will be included in the output. | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 494 | descendants, descendant_pids, process_report = get_descendant_info( | 
|  | 495 | current_process | 
|  | 496 | ) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 497 | if descendants: | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 498 | message = ( | 
|  | 499 | "\n" | 
|  | 500 | + gp.sprint_timen("Not all of the processes terminated:") | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 501 | + process_report | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 502 | ) | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 503 | gm.append_file(terminate_descendants_temp_file_path, message) | 
|  | 504 |  | 
|  | 505 | message = gp.sprint_dashes(width=120) | 
|  | 506 | gm.append_file(terminate_descendants_temp_file_path, message) | 
|  | 507 | gp.print_file(terminate_descendants_temp_file_path) | 
|  | 508 | os.remove(terminate_descendants_temp_file_path) | 
|  | 509 |  | 
|  | 510 |  | 
|  | 511 | def gen_exit_function(): | 
| Michael Walsh | 5328f38 | 2019-09-13 14:18:55 -0500 | [diff] [blame] | 512 | r""" | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 513 | Execute whenever the program ends normally or with the signals that we catch (i.e. TERM, INT). | 
| Michael Walsh | 5328f38 | 2019-09-13 14:18:55 -0500 | [diff] [blame] | 514 | """ | 
|  | 515 |  | 
| Michael Walsh | e15e7e9 | 2019-11-04 10:40:53 -0600 | [diff] [blame] | 516 | # ignore_err influences the way shell_cmd processes errors.  Since we're doing exit processing, we don't | 
|  | 517 | # want to stop the program due to a shell_cmd failure. | 
|  | 518 | ignore_err = 1 | 
|  | 519 |  | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 520 | if psutil_imported and term_options: | 
|  | 521 | terminate_descendants() | 
|  | 522 |  | 
| Michael Walsh | 5328f38 | 2019-09-13 14:18:55 -0500 | [diff] [blame] | 523 | # Call the main module's exit_function if it is defined. | 
|  | 524 | exit_function = getattr(module, "exit_function", None) | 
|  | 525 | if exit_function: | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 526 | exit_function() | 
| Michael Walsh | 5328f38 | 2019-09-13 14:18:55 -0500 | [diff] [blame] | 527 |  | 
|  | 528 | gp.qprint_pgm_footer() | 
|  | 529 |  | 
|  | 530 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 531 | def gen_signal_handler(signal_number, frame): | 
| Michael Walsh | 5328f38 | 2019-09-13 14:18:55 -0500 | [diff] [blame] | 532 | r""" | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 533 | Handle signals.  Without a function to catch a SIGTERM or SIGINT, the program would terminate immediately | 
|  | 534 | with return code 143 and without calling the exit_function. | 
| Michael Walsh | 5328f38 | 2019-09-13 14:18:55 -0500 | [diff] [blame] | 535 | """ | 
|  | 536 |  | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 537 | # The convention is to set up exit_function with atexit.register() so there is no need to explicitly | 
|  | 538 | # call exit_function from here. | 
| Michael Walsh | 5328f38 | 2019-09-13 14:18:55 -0500 | [diff] [blame] | 539 |  | 
| Michael Walsh | c0e0ad4 | 2019-12-05 16:54:59 -0600 | [diff] [blame] | 540 | gp.qprint_executing() | 
| Michael Walsh | 5328f38 | 2019-09-13 14:18:55 -0500 | [diff] [blame] | 541 |  | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 542 | # Calling exit prevents control from returning to the code that was running when the signal was received. | 
| Michael Walsh | 5328f38 | 2019-09-13 14:18:55 -0500 | [diff] [blame] | 543 | exit(0) | 
|  | 544 |  | 
|  | 545 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 546 | def gen_post_validation(exit_function=None, signal_handler=None): | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 547 | r""" | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 548 | Do generic post-validation processing.  By "post", we mean that this is to be called from a validation | 
|  | 549 | function after the caller has done any validation desired.  If the calling program passes exit_function | 
|  | 550 | and signal_handler parms, this function will register them.  In other words, it will make the | 
|  | 551 | signal_handler functions get called for SIGINT and SIGTERM and will make the exit_function function run | 
|  | 552 | prior to the termination of the program. | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 553 |  | 
|  | 554 | Description of arguments: | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 555 | exit_function                   A function object pointing to the caller's exit function.  This defaults | 
|  | 556 | to this module's gen_exit_function. | 
|  | 557 | signal_handler                  A function object pointing to the caller's signal_handler function.  This | 
|  | 558 | defaults to this module's gen_signal_handler. | 
| Michael Walsh | 7423c01 | 2016-10-04 10:27:21 -0500 | [diff] [blame] | 559 | """ | 
|  | 560 |  | 
| Michael Walsh | 5328f38 | 2019-09-13 14:18:55 -0500 | [diff] [blame] | 561 | # Get defaults. | 
|  | 562 | exit_function = exit_function or gen_exit_function | 
|  | 563 | signal_handler = signal_handler or gen_signal_handler | 
|  | 564 |  | 
|  | 565 | atexit.register(exit_function) | 
|  | 566 | signal.signal(signal.SIGINT, signal_handler) | 
|  | 567 | signal.signal(signal.SIGTERM, signal_handler) | 
|  | 568 |  | 
|  | 569 |  | 
|  | 570 | def gen_setup(): | 
|  | 571 | r""" | 
|  | 572 | Do general setup for a program. | 
|  | 573 | """ | 
|  | 574 |  | 
|  | 575 | # Set exit_on_error for gen_valid functions. | 
|  | 576 | gv.set_exit_on_error(True) | 
|  | 577 |  | 
|  | 578 | # Get main module variable values. | 
|  | 579 | parser = getattr(module, "parser") | 
|  | 580 | stock_list = getattr(module, "stock_list") | 
|  | 581 | validate_parms = getattr(module, "validate_parms", None) | 
|  | 582 |  | 
|  | 583 | gen_get_options(parser, stock_list) | 
|  | 584 |  | 
|  | 585 | if validate_parms: | 
|  | 586 | validate_parms() | 
| Michael Walsh | d339abf | 2020-02-18 11:28:02 -0600 | [diff] [blame] | 587 | sync_args() | 
| Michael Walsh | 5328f38 | 2019-09-13 14:18:55 -0500 | [diff] [blame] | 588 | gen_post_validation() | 
|  | 589 |  | 
|  | 590 | gp.qprint_pgm_header() |