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
13
14
15class func_timer_class:
16    r"""
17    Define the func timer class.
18
19    A func timer object can be used to run any function/arguments but with an
20    additional benefit of being able to specify a time_out value.  If the
21    function fails to complete before the timer expires, a ValueError
22    exception will be raised along with a detailed error message.
23
24    Example code:
25
26    func_timer = func_timer_class()
27    func_timer.run(run_key, "sleep 2", time_out=1)
28
29    In this example, the run_key function is being run by the func_timer
30    object with a time_out value of 1 second.  "sleep 2" is a positional parm
31    for the run_key function.
32    """
33
34    def __init__(self,
35                 obj_name='func_timer_class'):
36
37        # Initialize object variables.
38        self.__obj_name = obj_name
39        self.__func = None
40        self.__time_out = None
41        self.__child_pid = 0
42        # Save the original SIGUSR1 handler for later restoration by this
43        # class' methods.
44        self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1)
45
46    def __del__(self):
47        self.cleanup()
48
49    def sprint_obj(self):
50        r"""
51        sprint the fields of this object.  This would normally be for debug
52        purposes.
53        """
54
55        buffer = ""
56        buffer += self.__class__.__name__ + ":\n"
57        indent = 2
58        try:
59            func_name = self.__func.__name__
60        except AttributeError:
61            func_name = ""
62        buffer += gp.sprint_var(func_name, hex=1, loc_col1_indent=indent)
63        buffer += gp.sprint_varx("time_out", self.__time_out,
64                                 loc_col1_indent=indent)
65        buffer += gp.sprint_varx("child_pid", self.__child_pid,
66                                 loc_col1_indent=indent)
67        buffer += gp.sprint_varx("original_SIGUSR1_handler",
68                                 self.__original_SIGUSR1_handler,
69                                 loc_col1_indent=indent)
70        return buffer
71
72    def print_obj(self):
73        r"""
74        print the fields of this object to stdout.  This would normally be for
75        debug purposes.
76        """
77
78        sys.stdout.write(self.sprint_obj())
79
80    def cleanup(self):
81        r"""
82        Cleanup after the run method.
83        """
84
85        try:
86            gp.lprint_executing()
87            gp.lprint_var(self.__child_pid)
88        except AttributeError:
89            pass
90
91        # If self.__child_pid is 0, then we are either running as the child
92        # or we've already cleaned up.
93        # If self.__time_out is None, then no child process would have been
94        # spawned.
95        if self.__child_pid == 0 or self.__time_out is None:
96            return
97
98        # Restore the original SIGUSR1 handler.
99        if self.__original_SIGUSR1_handler != 0:
100            signal.signal(signal.SIGUSR1, self.__original_SIGUSR1_handler)
101        try:
102            gp.lprint_timen("Killing child pid " + str(self.__child_pid)
103                            + ".")
104            os.kill(self.__child_pid, signal.SIGKILL)
105        except OSError:
106            gp.lprint_timen("Tolerated kill failure.")
107        try:
108            gp.lprint_timen("os.waitpid(" + str(self.__child_pid) + ")")
109            os.waitpid(self.__child_pid, 0)
110        except OSError:
111            gp.lprint_timen("Tolerated waitpid failure.")
112        self.__child_pid = 0
113        # For debug purposes, prove that the child pid was killed.
114        children = gm.get_child_pids()
115        gp.lprint_var(children)
116
117    def timed_out(self,
118                  signal_number,
119                  frame):
120        r"""
121        Handle a SIGUSR1 generated by the child process after the time_out has
122        expired.
123
124        signal_number               The signal_number of the signal causing
125                                    this method to get invoked.  This should
126                                    always be 10 (SIGUSR1).
127        frame                       The stack frame associated with the
128                                    function that times out.
129        """
130
131        gp.lprint_executing()
132
133        self.cleanup()
134
135        # Compose an error message.
136        err_msg = "The " + self.__func.__name__
137        err_msg += " function timed out after " + str(self.__time_out)
138        err_msg += " seconds.\n"
139        if not gp.robot_env:
140            err_msg += gp.sprint_call_stack()
141
142        raise ValueError(err_msg)
143
144    def run(self, func, *args, **kwargs):
145
146        r"""
147        Run the indicated function with the given args and kwargs and return
148        the value that the function returns.  If the time_out value expires,
149        raise a ValueError exception with a detailed error message.
150
151        This method passes all of the args and kwargs directly to the child
152        function with the following important exception: If kwargs contains a
153        'time_out' value, it will be used to set the func timer object's
154        time_out value and then the kwargs['time_out'] entry will be removed.
155        If the time-out expires before the function finishes running, this
156        method will raise a ValueError.
157
158        Example:
159        func_timer = func_timer_class()
160        func_timer.run(run_key, "sleep 3", time_out=2)
161
162        Example:
163        try:
164            result = func_timer.run(func1, "parm1", time_out=2)
165            print_var(result)
166        except ValueError:
167            print("The func timed out but we're handling it.")
168
169        Description of argument(s):
170        func                        The function object which is to be called.
171        args                        The arguments which are to be passed to
172                                    the function object.
173        kwargs                      The keyword arguments which are to be
174                                    passed to the function object.  As noted
175                                    above, kwargs['time_out'] will get special
176                                    treatment.
177        """
178
179        gp.lprint_executing()
180
181        # Store method parms as object parms.
182        self.__func = func
183
184        # Get self.__time_out value from kwargs.  If kwargs['time_out'] is
185        # not present, self.__time_out will default to None.
186        self.__time_out = None
187        if len(kwargs) > 0:
188            if 'time_out' in kwargs:
189                self.__time_out = kwargs['time_out']
190                del kwargs['time_out']
191                if self.__time_out is not None:
192                    self.__time_out = int(self.__time_out)
193
194        self.__child_pid = 0
195        if self.__time_out is not None:
196            # Save the original SIGUSR1 handler for later restoration by this
197            # class' methods.
198            self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1)
199            # Designate a SIGUSR1 handling function.
200            signal.signal(signal.SIGUSR1, self.timed_out)
201            parent_pid = os.getpid()
202            self.__child_pid = os.fork()
203            if self.__child_pid == 0:
204                gp.dprint_timen("Child timer pid " + str(os.getpid())
205                                + ": Sleeping for " + str(self.__time_out)
206                                + " seconds.")
207                time.sleep(self.__time_out)
208                gp.dprint_timen("Child timer pid " + str(os.getpid())
209                                + ": Sending SIGUSR1 to parent pid "
210                                + str(parent_pid) + ".")
211                os.kill(parent_pid, signal.SIGUSR1)
212                os._exit(0)
213
214        # Call the user's function with the user's arguments.
215        children = gm.get_child_pids()
216        gp.lprint_var(children)
217        gp.lprint_timen("Calling the user's function.")
218        gp.lprint_varx("func_name", func.__name__)
219        gp.lprint_vars(args, kwargs)
220        try:
221            result = func(*args, **kwargs)
222        except Exception as func_exception:
223            # We must handle all exceptions so that we have the chance to
224            # cleanup before re-raising the exception.
225            gp.lprint_timen("Encountered exception in user's function.")
226            self.cleanup()
227            raise(func_exception)
228        gp.lprint_timen("Returned from the user's function.")
229
230        self.cleanup()
231
232        return result
233