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