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