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