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