1#!/usr/bin/env python3 2 3r""" 4Define the func_timer class. 5""" 6 7import os 8import signal 9import sys 10import time 11 12import gen_misc as gm 13import gen_print as gp 14import gen_valid as gv 15 16 17class func_timer_class: 18 r""" 19 Define the func timer class. 20 21 A func timer object can be used to run any function/arguments but with an additional benefit of being 22 able to specify a time_out value. If the function fails to complete before the timer expires, a 23 ValueError exception will be raised along with a detailed error message. 24 25 Example code: 26 27 func_timer = func_timer_class() 28 func_timer.run(run_key, "sleep 2", time_out=1) 29 30 In this example, the run_key function is being run by the func_timer object with a time_out value of 1 31 second. "sleep 2" is a positional parm for the run_key function. 32 """ 33 34 def __init__(self, obj_name="func_timer_class"): 35 # Initialize object variables. 36 self.__obj_name = obj_name 37 self.__func = None 38 self.__time_out = None 39 self.__child_pid = 0 40 # Save the original SIGUSR1 handler for later restoration by this class' methods. 41 self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1) 42 43 def __del__(self): 44 self.cleanup() 45 46 def sprint_obj(self): 47 r""" 48 sprint the fields of this object. This would normally be for debug purposes. 49 """ 50 51 buffer = "" 52 buffer += self.__class__.__name__ + ":\n" 53 indent = 2 54 try: 55 func_name = self.__func.__name__ 56 except AttributeError: 57 func_name = "" 58 buffer += gp.sprint_var(func_name, indent=indent) 59 buffer += gp.sprint_varx("time_out", self.__time_out, indent=indent) 60 buffer += gp.sprint_varx("child_pid", self.__child_pid, indent=indent) 61 buffer += gp.sprint_varx( 62 "original_SIGUSR1_handler", 63 self.__original_SIGUSR1_handler, 64 indent=indent, 65 ) 66 return buffer 67 68 def print_obj(self): 69 r""" 70 print the fields of this object to stdout. This would normally be for debug purposes. 71 """ 72 73 sys.stdout.write(self.sprint_obj()) 74 75 def cleanup(self): 76 r""" 77 Cleanup after the run method. 78 """ 79 80 try: 81 gp.lprint_executing() 82 gp.lprint_var(self.__child_pid) 83 except (AttributeError, KeyError, TypeError): 84 # NOTE: In python 3, this code fails with "KeyError: ('__main__',)" when calling functions like 85 # lprint_executing that use inspect.stack() during object destruction. No fixes found so 86 # tolerating the error. In python 2.x, it may fail with TypeError. This seems to happen when 87 # cleaning up after an exception was raised. 88 pass 89 90 # If self.__child_pid is 0, then we are either running as the child or we've already cleaned up. 91 # If self.__time_out is None, then no child process would have been spawned. 92 if self.__child_pid == 0 or self.__time_out is None: 93 return 94 95 # Restore the original SIGUSR1 handler. 96 if self.__original_SIGUSR1_handler != 0: 97 signal.signal(signal.SIGUSR1, self.__original_SIGUSR1_handler) 98 try: 99 gp.lprint_timen("Killing child pid " + str(self.__child_pid) + ".") 100 os.kill(self.__child_pid, signal.SIGKILL) 101 except OSError: 102 gp.lprint_timen("Tolerated kill failure.") 103 try: 104 gp.lprint_timen("os.waitpid(" + str(self.__child_pid) + ")") 105 os.waitpid(self.__child_pid, 0) 106 except OSError: 107 gp.lprint_timen("Tolerated waitpid failure.") 108 self.__child_pid = 0 109 # For debug purposes, prove that the child pid was killed. 110 children = gm.get_child_pids() 111 gp.lprint_var(children) 112 113 def timed_out(self, signal_number, frame): 114 r""" 115 Handle a SIGUSR1 generated by the child process after the time_out has expired. 116 117 signal_number The signal_number of the signal causing this method to get invoked. This 118 should always be 10 (SIGUSR1). 119 frame The stack frame associated with the function that times out. 120 """ 121 122 gp.lprint_executing() 123 124 self.cleanup() 125 126 # Compose an error message. 127 err_msg = "The " + self.__func.__name__ 128 err_msg += " function timed out after " + str(self.__time_out) 129 err_msg += " seconds.\n" 130 if not gp.robot_env: 131 err_msg += gp.sprint_call_stack() 132 133 raise ValueError(err_msg) 134 135 def run(self, func, *args, **kwargs): 136 r""" 137 Run the indicated function with the given args and kwargs and return the value that the function 138 returns. If the time_out value expires, raise a ValueError exception with a detailed error message. 139 140 This method passes all of the args and kwargs directly to the child function with the following 141 important exception: If kwargs contains a 'time_out' value, it will be used to set the func timer 142 object's time_out value and then the kwargs['time_out'] entry will be removed. If the time-out 143 expires before the function finishes running, this method will raise a ValueError. 144 145 Example: 146 func_timer = func_timer_class() 147 func_timer.run(run_key, "sleep 3", time_out=2) 148 149 Example: 150 try: 151 result = func_timer.run(func1, "parm1", time_out=2) 152 print_var(result) 153 except ValueError: 154 print("The func timed out but we're handling it.") 155 156 Description of argument(s): 157 func The function object which is to be called. 158 args The arguments which are to be passed to the function object. 159 kwargs The keyword arguments which are to be passed to the function object. As 160 noted above, kwargs['time_out'] will get special treatment. 161 """ 162 163 gp.lprint_executing() 164 165 # Store method parms as object parms. 166 self.__func = func 167 168 # Get self.__time_out value from kwargs. If kwargs['time_out'] is not present, self.__time_out will 169 # default to None. 170 self.__time_out = None 171 if "time_out" in kwargs: 172 self.__time_out = kwargs["time_out"] 173 del kwargs["time_out"] 174 # Convert "none" string to None. 175 try: 176 if self.__time_out.lower() == "none": 177 self.__time_out = None 178 except AttributeError: 179 pass 180 if self.__time_out is not None: 181 self.__time_out = int(self.__time_out) 182 # Ensure that time_out is non-negative. 183 message = gv.valid_range( 184 self.__time_out, 0, var_name="time_out" 185 ) 186 if message != "": 187 raise ValueError( 188 "\n" + gp.sprint_error_report(message, format="long") 189 ) 190 191 gp.lprint_varx("time_out", self.__time_out) 192 self.__child_pid = 0 193 if self.__time_out is not None: 194 # Save the original SIGUSR1 handler for later restoration by this class' methods. 195 self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1) 196 # Designate a SIGUSR1 handling function. 197 signal.signal(signal.SIGUSR1, self.timed_out) 198 parent_pid = os.getpid() 199 self.__child_pid = os.fork() 200 if self.__child_pid == 0: 201 gp.dprint_timen( 202 "Child timer pid " 203 + str(os.getpid()) 204 + ": Sleeping for " 205 + str(self.__time_out) 206 + " seconds." 207 ) 208 time.sleep(self.__time_out) 209 gp.dprint_timen( 210 "Child timer pid " 211 + str(os.getpid()) 212 + ": Sending SIGUSR1 to parent pid " 213 + str(parent_pid) 214 + "." 215 ) 216 os.kill(parent_pid, signal.SIGUSR1) 217 os._exit(0) 218 219 # Call the user's function with the user's arguments. 220 children = gm.get_child_pids() 221 gp.lprint_var(children) 222 gp.lprint_timen("Calling the user's function.") 223 gp.lprint_varx("func_name", func.__name__) 224 gp.lprint_vars(args, kwargs) 225 try: 226 result = func(*args, **kwargs) 227 except Exception as func_exception: 228 # We must handle all exceptions so that we have the chance to cleanup before re-raising the 229 # exception. 230 gp.lprint_timen("Encountered exception in user's function.") 231 self.cleanup() 232 raise (func_exception) 233 gp.lprint_timen("Returned from the user's function.") 234 235 self.cleanup() 236 237 return result 238