blob: d07ba322784da4062aa6d7bd0fde62bb6325710e [file] [log] [blame]
Michael Walsh4da179c2018-08-07 14:39:35 -05001#!/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
Michael Walsh4799e2a2018-11-16 15:29:43 -060013import gen_valid as gv
Michael Walsh4da179c2018-08-07 14:39:35 -050014
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)
Michael Walsh89c0aaa2019-04-18 11:00:54 -050089 except (AttributeError, KeyError, TypeError):
George Keishing36efbc02018-12-12 10:18:23 -060090 # 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
Michael Walsh89c0aaa2019-04-18 11:00:54 -050093 # 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.
Michael Walsh4da179c2018-08-07 14:39:35 -050096 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
Michael Walsh4799e2a2018-11-16 15:29:43 -0600194 if 'time_out' in kwargs:
195 self.__time_out = kwargs['time_out']
196 del kwargs['time_out']
197 # Convert "none" string to None.
George Keishing36efbc02018-12-12 10:18:23 -0600198 try:
199 if self.__time_out.lower() == "none":
200 self.__time_out = None
201 except AttributeError:
202 pass
Michael Walsh4799e2a2018-11-16 15:29:43 -0600203 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'))
Michael Walsh4da179c2018-08-07 14:39:35 -0500211
Michael Walsh4799e2a2018-11-16 15:29:43 -0600212 gp.lprint_varx("time_out", self.__time_out)
Michael Walsh4da179c2018-08-07 14:39:35 -0500213 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