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