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