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