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