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