New func_timer class
This class will allow you to run any function with a time_out.
Change-Id: I3f42153d7d52ade7c1a488521588ee873e0758e5
Signed-off-by: Michael Walsh <micwalsh@us.ibm.com>
diff --git a/lib/func_timer.py b/lib/func_timer.py
new file mode 100644
index 0000000..f29b362
--- /dev/null
+++ b/lib/func_timer.py
@@ -0,0 +1,232 @@
+#!/usr/bin/env python
+
+r"""
+Define the func_timer class.
+"""
+
+import os
+import sys
+import signal
+import time
+import gen_print as gp
+import gen_misc as gm
+
+
+class func_timer_class:
+ r"""
+ Define the func timer class.
+
+ A func timer object can be used to run any function/arguments but with an
+ additional benefit of being able to specify a time_out value. If the
+ function fails to complete before the timer expires, a ValueError
+ exception will be raised along with a detailed error message.
+
+ Example code:
+
+ func_timer = func_timer_class()
+ func_timer.run(run_key, "sleep 2", time_out=1)
+
+ In this example, the run_key function is being run by the func_timer
+ object with a time_out value of 1 second. "sleep 2" is a positional parm
+ for the run_key function.
+ """
+
+ def __init__(self,
+ obj_name='func_timer_class'):
+
+ # Initialize object variables.
+ self.__obj_name = obj_name
+ self.__func = None
+ self.__time_out = None
+ self.__child_pid = 0
+ # Save the original SIGUSR1 handler for later restoration by this
+ # class' methods.
+ self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1)
+
+ def __del__(self):
+ self.cleanup()
+
+ def sprint_obj(self):
+ r"""
+ sprint the fields of this object. This would normally be for debug
+ purposes.
+ """
+
+ buffer = ""
+ buffer += self.__class__.__name__ + ":\n"
+ indent = 2
+ try:
+ func_name = self.__func.__name__
+ except AttributeError:
+ func_name = ""
+ buffer += gp.sprint_var(func_name, hex=1, loc_col1_indent=indent)
+ buffer += gp.sprint_varx("time_out", self.__time_out,
+ loc_col1_indent=indent)
+ buffer += gp.sprint_varx("child_pid", self.__child_pid,
+ loc_col1_indent=indent)
+ buffer += gp.sprint_varx("original_SIGUSR1_handler",
+ self.__original_SIGUSR1_handler,
+ loc_col1_indent=indent)
+ return buffer
+
+ def print_obj(self):
+ r"""
+ print the fields of this object to stdout. This would normally be for
+ debug purposes.
+ """
+
+ sys.stdout.write(self.sprint_obj())
+
+ def cleanup(self):
+ r"""
+ Cleanup after the run method.
+ """
+
+ try:
+ gp.lprint_executing()
+ gp.lprint_var(self.__child_pid)
+ except AttributeError:
+ pass
+
+ # If self.__child_pid is 0, then we are either running as the child
+ # or we've already cleaned up.
+ # If self.__time_out is None, then no child process would have been
+ # spawned.
+ if self.__child_pid == 0 or self.__time_out is None:
+ return
+
+ # Restore the original SIGUSR1 handler.
+ if self.__original_SIGUSR1_handler != 0:
+ signal.signal(signal.SIGUSR1, self.__original_SIGUSR1_handler)
+ try:
+ gp.lprint_timen("Killing child pid " + str(self.__child_pid)
+ + ".")
+ os.kill(self.__child_pid, signal.SIGKILL)
+ except OSError:
+ gp.lprint_timen("Tolerated kill failure.")
+ try:
+ gp.lprint_timen("os.waitpid(" + str(self.__child_pid) + ")")
+ os.waitpid(self.__child_pid, 0)
+ except OSError:
+ gp.lprint_timen("Tolerated waitpid failure.")
+ self.__child_pid = 0
+ # For debug purposes, prove that the child pid was killed.
+ children = gm.get_child_pids()
+ gp.lprint_var(children)
+
+ def timed_out(self,
+ signal_number,
+ frame):
+ r"""
+ Handle a SIGUSR1 generated by the child process after the time_out has
+ expired.
+
+ signal_number The signal_number of the signal causing
+ this method to get invoked. This should
+ always be 10 (SIGUSR1).
+ frame The stack frame associated with the
+ function that times out.
+ """
+
+ gp.lprint_executing()
+
+ self.cleanup()
+
+ # Compose an error message.
+ err_msg = "The " + self.__func.__name__
+ err_msg += " function timed out after " + str(self.__time_out)
+ err_msg += " seconds.\n"
+ if not gp.robot_env:
+ err_msg += gp.sprint_call_stack()
+
+ raise ValueError(err_msg)
+
+ def run(self, func, *args, **kwargs):
+
+ r"""
+ Run the indicated function with the given args and kwargs and return
+ the value that the function returns. If the time_out value expires,
+ raise a ValueError exception with a detailed error message.
+
+ This method passes all of the args and kwargs directly to the child
+ function with the following important exception: If kwargs contains a
+ 'time_out' value, it will be used to set the func timer object's
+ time_out value and then the kwargs['time_out'] entry will be removed.
+ If the time-out expires before the function finishes running, this
+ method will raise a ValueError.
+
+ Example:
+ func_timer = func_timer_class()
+ func_timer.run(run_key, "sleep 3", time_out=2)
+
+ Example:
+ try:
+ result = func_timer.run(func1, "parm1", time_out=2)
+ print_var(result)
+ except ValueError:
+ print("The func timed out but we're handling it.")
+
+ Description of argument(s):
+ func The function object which is to be called.
+ args The arguments which are to be passed to
+ the function object.
+ kwargs The keyword arguments which are to be
+ passed to the function object. As noted
+ above, kwargs['time_out'] will get special
+ treatment.
+ """
+
+ gp.lprint_executing()
+
+ # Store method parms as object parms.
+ self.__func = func
+
+ # Get self.__time_out value from kwargs. If kwargs['time_out'] is
+ # not present, self.__time_out will default to None.
+ self.__time_out = None
+ if len(kwargs) > 0:
+ if 'time_out' in kwargs:
+ self.__time_out = kwargs['time_out']
+ del kwargs['time_out']
+ if self.__time_out is not None:
+ self.__time_out = int(self.__time_out)
+
+ self.__child_pid = 0
+ if self.__time_out is not None:
+ # Save the original SIGUSR1 handler for later restoration by this
+ # class' methods.
+ self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1)
+ # Designate a SIGUSR1 handling function.
+ signal.signal(signal.SIGUSR1, self.timed_out)
+ parent_pid = os.getpid()
+ self.__child_pid = os.fork()
+ if self.__child_pid == 0:
+ gp.dprint_timen("Child timer pid " + str(os.getpid())
+ + ": Sleeping for " + str(self.__time_out)
+ + " seconds.")
+ time.sleep(self.__time_out)
+ gp.dprint_timen("Child timer pid " + str(os.getpid())
+ + ": Sending SIGUSR1 to parent pid "
+ + str(parent_pid) + ".")
+ os.kill(parent_pid, signal.SIGUSR1)
+ os._exit(0)
+
+ # Call the user's function with the user's arguments.
+ children = gm.get_child_pids()
+ gp.lprint_var(children)
+ gp.lprint_timen("Calling the user's function.")
+ gp.lprint_varx("func_name", func.__name__)
+ gp.lprint_vars(args, kwargs)
+ try:
+ result = func(*args, **kwargs)
+ except Exception as func_exception:
+ # We must handle all exceptions so that we have the chance to
+ # cleanup before re-raising the exception.
+ gp.lprint_timen("Encountered exception in user's function.")
+ self.cleanup()
+ raise(func_exception)
+ gp.lprint_timen("Returned from the user's function.")
+
+ self.cleanup()
+
+ return result