1e7e9171eSGeorge Keishing#!/usr/bin/env python3 24da179cfSMichael Walsh 34da179cfSMichael Walshr""" 44da179cfSMichael WalshDefine the func_timer class. 54da179cfSMichael Walsh""" 64da179cfSMichael Walsh 74da179cfSMichael Walshimport os 8e635ddc0SGeorge Keishingimport signal 9*20f38712SPatrick Williamsimport sys 104da179cfSMichael Walshimport time 11*20f38712SPatrick Williams 12e635ddc0SGeorge Keishingimport gen_misc as gm 13*20f38712SPatrick Williamsimport gen_print as gp 144799e2a6SMichael Walshimport gen_valid as gv 154da179cfSMichael Walsh 164da179cfSMichael Walsh 174da179cfSMichael Walshclass func_timer_class: 184da179cfSMichael Walsh r""" 194da179cfSMichael Walsh Define the func timer class. 204da179cfSMichael Walsh 21410b1787SMichael Walsh A func timer object can be used to run any function/arguments but with an additional benefit of being 22410b1787SMichael Walsh able to specify a time_out value. If the function fails to complete before the timer expires, a 23410b1787SMichael Walsh ValueError exception will be raised along with a detailed error message. 244da179cfSMichael Walsh 254da179cfSMichael Walsh Example code: 264da179cfSMichael Walsh 274da179cfSMichael Walsh func_timer = func_timer_class() 284da179cfSMichael Walsh func_timer.run(run_key, "sleep 2", time_out=1) 294da179cfSMichael Walsh 30410b1787SMichael Walsh In this example, the run_key function is being run by the func_timer object with a time_out value of 1 31410b1787SMichael Walsh second. "sleep 2" is a positional parm for the run_key function. 324da179cfSMichael Walsh """ 334da179cfSMichael Walsh 34*20f38712SPatrick Williams def __init__(self, obj_name="func_timer_class"): 354da179cfSMichael Walsh # Initialize object variables. 364da179cfSMichael Walsh self.__obj_name = obj_name 374da179cfSMichael Walsh self.__func = None 384da179cfSMichael Walsh self.__time_out = None 394da179cfSMichael Walsh self.__child_pid = 0 40410b1787SMichael Walsh # Save the original SIGUSR1 handler for later restoration by this class' methods. 414da179cfSMichael Walsh self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1) 424da179cfSMichael Walsh 434da179cfSMichael Walsh def __del__(self): 444da179cfSMichael Walsh self.cleanup() 454da179cfSMichael Walsh 464da179cfSMichael Walsh def sprint_obj(self): 474da179cfSMichael Walsh r""" 48410b1787SMichael Walsh sprint the fields of this object. This would normally be for debug purposes. 494da179cfSMichael Walsh """ 504da179cfSMichael Walsh 514da179cfSMichael Walsh buffer = "" 524da179cfSMichael Walsh buffer += self.__class__.__name__ + ":\n" 534da179cfSMichael Walsh indent = 2 544da179cfSMichael Walsh try: 554da179cfSMichael Walsh func_name = self.__func.__name__ 564da179cfSMichael Walsh except AttributeError: 574da179cfSMichael Walsh func_name = "" 580d5f96a4SMichael Walsh buffer += gp.sprint_var(func_name, indent=indent) 590d5f96a4SMichael Walsh buffer += gp.sprint_varx("time_out", self.__time_out, indent=indent) 600d5f96a4SMichael Walsh buffer += gp.sprint_varx("child_pid", self.__child_pid, indent=indent) 61*20f38712SPatrick Williams buffer += gp.sprint_varx( 62*20f38712SPatrick Williams "original_SIGUSR1_handler", 634da179cfSMichael Walsh self.__original_SIGUSR1_handler, 64*20f38712SPatrick Williams indent=indent, 65*20f38712SPatrick Williams ) 664da179cfSMichael Walsh return buffer 674da179cfSMichael Walsh 684da179cfSMichael Walsh def print_obj(self): 694da179cfSMichael Walsh r""" 70410b1787SMichael Walsh print the fields of this object to stdout. This would normally be for debug purposes. 714da179cfSMichael Walsh """ 724da179cfSMichael Walsh 734da179cfSMichael Walsh sys.stdout.write(self.sprint_obj()) 744da179cfSMichael Walsh 754da179cfSMichael Walsh def cleanup(self): 764da179cfSMichael Walsh r""" 774da179cfSMichael Walsh Cleanup after the run method. 784da179cfSMichael Walsh """ 794da179cfSMichael Walsh 804da179cfSMichael Walsh try: 814da179cfSMichael Walsh gp.lprint_executing() 824da179cfSMichael Walsh gp.lprint_var(self.__child_pid) 8389c0aaaaSMichael Walsh except (AttributeError, KeyError, TypeError): 84410b1787SMichael Walsh # NOTE: In python 3, this code fails with "KeyError: ('__main__',)" when calling functions like 85410b1787SMichael Walsh # lprint_executing that use inspect.stack() during object destruction. No fixes found so 86410b1787SMichael Walsh # tolerating the error. In python 2.x, it may fail with TypeError. This seems to happen when 87410b1787SMichael Walsh # cleaning up after an exception was raised. 884da179cfSMichael Walsh pass 894da179cfSMichael Walsh 90410b1787SMichael Walsh # If self.__child_pid is 0, then we are either running as the child or we've already cleaned up. 91410b1787SMichael Walsh # If self.__time_out is None, then no child process would have been spawned. 924da179cfSMichael Walsh if self.__child_pid == 0 or self.__time_out is None: 934da179cfSMichael Walsh return 944da179cfSMichael Walsh 954da179cfSMichael Walsh # Restore the original SIGUSR1 handler. 964da179cfSMichael Walsh if self.__original_SIGUSR1_handler != 0: 974da179cfSMichael Walsh signal.signal(signal.SIGUSR1, self.__original_SIGUSR1_handler) 984da179cfSMichael Walsh try: 99*20f38712SPatrick Williams gp.lprint_timen("Killing child pid " + str(self.__child_pid) + ".") 1004da179cfSMichael Walsh os.kill(self.__child_pid, signal.SIGKILL) 1014da179cfSMichael Walsh except OSError: 1024da179cfSMichael Walsh gp.lprint_timen("Tolerated kill failure.") 1034da179cfSMichael Walsh try: 1044da179cfSMichael Walsh gp.lprint_timen("os.waitpid(" + str(self.__child_pid) + ")") 1054da179cfSMichael Walsh os.waitpid(self.__child_pid, 0) 1064da179cfSMichael Walsh except OSError: 1074da179cfSMichael Walsh gp.lprint_timen("Tolerated waitpid failure.") 1084da179cfSMichael Walsh self.__child_pid = 0 1094da179cfSMichael Walsh # For debug purposes, prove that the child pid was killed. 1104da179cfSMichael Walsh children = gm.get_child_pids() 1114da179cfSMichael Walsh gp.lprint_var(children) 1124da179cfSMichael Walsh 113*20f38712SPatrick Williams def timed_out(self, signal_number, frame): 1144da179cfSMichael Walsh r""" 115410b1787SMichael Walsh Handle a SIGUSR1 generated by the child process after the time_out has expired. 1164da179cfSMichael Walsh 117410b1787SMichael Walsh signal_number The signal_number of the signal causing this method to get invoked. This 118410b1787SMichael Walsh should always be 10 (SIGUSR1). 119410b1787SMichael Walsh frame The stack frame associated with the function that times out. 1204da179cfSMichael Walsh """ 1214da179cfSMichael Walsh 1224da179cfSMichael Walsh gp.lprint_executing() 1234da179cfSMichael Walsh 1244da179cfSMichael Walsh self.cleanup() 1254da179cfSMichael Walsh 1264da179cfSMichael Walsh # Compose an error message. 1274da179cfSMichael Walsh err_msg = "The " + self.__func.__name__ 1284da179cfSMichael Walsh err_msg += " function timed out after " + str(self.__time_out) 1294da179cfSMichael Walsh err_msg += " seconds.\n" 1304da179cfSMichael Walsh if not gp.robot_env: 1314da179cfSMichael Walsh err_msg += gp.sprint_call_stack() 1324da179cfSMichael Walsh 1334da179cfSMichael Walsh raise ValueError(err_msg) 1344da179cfSMichael Walsh 1354da179cfSMichael Walsh def run(self, func, *args, **kwargs): 1364da179cfSMichael Walsh r""" 137410b1787SMichael Walsh Run the indicated function with the given args and kwargs and return the value that the function 138410b1787SMichael Walsh returns. If the time_out value expires, raise a ValueError exception with a detailed error message. 1394da179cfSMichael Walsh 140410b1787SMichael Walsh This method passes all of the args and kwargs directly to the child function with the following 141410b1787SMichael Walsh important exception: If kwargs contains a 'time_out' value, it will be used to set the func timer 142410b1787SMichael Walsh object's time_out value and then the kwargs['time_out'] entry will be removed. If the time-out 143410b1787SMichael Walsh expires before the function finishes running, this method will raise a ValueError. 1444da179cfSMichael Walsh 1454da179cfSMichael Walsh Example: 1464da179cfSMichael Walsh func_timer = func_timer_class() 1474da179cfSMichael Walsh func_timer.run(run_key, "sleep 3", time_out=2) 1484da179cfSMichael Walsh 1494da179cfSMichael Walsh Example: 1504da179cfSMichael Walsh try: 1514da179cfSMichael Walsh result = func_timer.run(func1, "parm1", time_out=2) 1524da179cfSMichael Walsh print_var(result) 1534da179cfSMichael Walsh except ValueError: 1544da179cfSMichael Walsh print("The func timed out but we're handling it.") 1554da179cfSMichael Walsh 1564da179cfSMichael Walsh Description of argument(s): 1574da179cfSMichael Walsh func The function object which is to be called. 158410b1787SMichael Walsh args The arguments which are to be passed to the function object. 159410b1787SMichael Walsh kwargs The keyword arguments which are to be passed to the function object. As 160410b1787SMichael Walsh noted above, kwargs['time_out'] will get special treatment. 1614da179cfSMichael Walsh """ 1624da179cfSMichael Walsh 1634da179cfSMichael Walsh gp.lprint_executing() 1644da179cfSMichael Walsh 1654da179cfSMichael Walsh # Store method parms as object parms. 1664da179cfSMichael Walsh self.__func = func 1674da179cfSMichael Walsh 168410b1787SMichael Walsh # Get self.__time_out value from kwargs. If kwargs['time_out'] is not present, self.__time_out will 169410b1787SMichael Walsh # default to None. 1704da179cfSMichael Walsh self.__time_out = None 171*20f38712SPatrick Williams if "time_out" in kwargs: 172*20f38712SPatrick Williams self.__time_out = kwargs["time_out"] 173*20f38712SPatrick Williams del kwargs["time_out"] 1744799e2a6SMichael Walsh # Convert "none" string to None. 17536efbc04SGeorge Keishing try: 17636efbc04SGeorge Keishing if self.__time_out.lower() == "none": 1774799e2a6SMichael Walsh self.__time_out = None 17836efbc04SGeorge Keishing except AttributeError: 17936efbc04SGeorge Keishing pass 1804da179cfSMichael Walsh if self.__time_out is not None: 1814da179cfSMichael Walsh self.__time_out = int(self.__time_out) 1824799e2a6SMichael Walsh # Ensure that time_out is non-negative. 183*20f38712SPatrick Williams message = gv.valid_range( 184*20f38712SPatrick Williams self.__time_out, 0, var_name="time_out" 185*20f38712SPatrick Williams ) 1864799e2a6SMichael Walsh if message != "": 187*20f38712SPatrick Williams raise ValueError( 188*20f38712SPatrick Williams "\n" + gp.sprint_error_report(message, format="long") 189*20f38712SPatrick Williams ) 1904da179cfSMichael Walsh 1914799e2a6SMichael Walsh gp.lprint_varx("time_out", self.__time_out) 1924da179cfSMichael Walsh self.__child_pid = 0 1934da179cfSMichael Walsh if self.__time_out is not None: 194410b1787SMichael Walsh # Save the original SIGUSR1 handler for later restoration by this class' methods. 1954da179cfSMichael Walsh self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1) 1964da179cfSMichael Walsh # Designate a SIGUSR1 handling function. 1974da179cfSMichael Walsh signal.signal(signal.SIGUSR1, self.timed_out) 1984da179cfSMichael Walsh parent_pid = os.getpid() 1994da179cfSMichael Walsh self.__child_pid = os.fork() 2004da179cfSMichael Walsh if self.__child_pid == 0: 201*20f38712SPatrick Williams gp.dprint_timen( 202*20f38712SPatrick Williams "Child timer pid " 203*20f38712SPatrick Williams + str(os.getpid()) 204*20f38712SPatrick Williams + ": Sleeping for " 205*20f38712SPatrick Williams + str(self.__time_out) 206*20f38712SPatrick Williams + " seconds." 207*20f38712SPatrick Williams ) 2084da179cfSMichael Walsh time.sleep(self.__time_out) 209*20f38712SPatrick Williams gp.dprint_timen( 210*20f38712SPatrick Williams "Child timer pid " 211*20f38712SPatrick Williams + str(os.getpid()) 2124da179cfSMichael Walsh + ": Sending SIGUSR1 to parent pid " 213*20f38712SPatrick Williams + str(parent_pid) 214*20f38712SPatrick Williams + "." 215*20f38712SPatrick Williams ) 2164da179cfSMichael Walsh os.kill(parent_pid, signal.SIGUSR1) 2174da179cfSMichael Walsh os._exit(0) 2184da179cfSMichael Walsh 2194da179cfSMichael Walsh # Call the user's function with the user's arguments. 2204da179cfSMichael Walsh children = gm.get_child_pids() 2214da179cfSMichael Walsh gp.lprint_var(children) 2224da179cfSMichael Walsh gp.lprint_timen("Calling the user's function.") 2234da179cfSMichael Walsh gp.lprint_varx("func_name", func.__name__) 2244da179cfSMichael Walsh gp.lprint_vars(args, kwargs) 2254da179cfSMichael Walsh try: 2264da179cfSMichael Walsh result = func(*args, **kwargs) 2274da179cfSMichael Walsh except Exception as func_exception: 228410b1787SMichael Walsh # We must handle all exceptions so that we have the chance to cleanup before re-raising the 229410b1787SMichael Walsh # exception. 2304da179cfSMichael Walsh gp.lprint_timen("Encountered exception in user's function.") 2314da179cfSMichael Walsh self.cleanup() 2324da179cfSMichael Walsh raise (func_exception) 2334da179cfSMichael Walsh gp.lprint_timen("Returned from the user's function.") 2344da179cfSMichael Walsh 2354da179cfSMichael Walsh self.cleanup() 2364da179cfSMichael Walsh 2374da179cfSMichael Walsh return result 238