xref: /openbmc/openbmc-test-automation/lib/func_timer.py (revision 20f38712b324e61a94e174017c487a0af4b373e1)
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