blob: 5f832412ddf02a8ad72d87c50811531139dbf1ad [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)
George Keishing36efbc02018-12-12 10:18:23 -060089 except (AttributeError, KeyError):
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.
Michael Walsh4da179c2018-08-07 14:39:35 -050094 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
Michael Walsh4799e2a2018-11-16 15:29:43 -0600192 if 'time_out' in kwargs:
193 self.__time_out = kwargs['time_out']
194 del kwargs['time_out']
195 # Convert "none" string to None.
George Keishing36efbc02018-12-12 10:18:23 -0600196 try:
197 if self.__time_out.lower() == "none":
198 self.__time_out = None
199 except AttributeError:
200 pass
Michael Walsh4799e2a2018-11-16 15:29:43 -0600201 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.svalid_range(self.__time_out, [0], "time_out")
205 if message != "":
206 raise ValueError("\n"
207 + gp.sprint_error_report(message,
208 format='long'))
Michael Walsh4da179c2018-08-07 14:39:35 -0500209
Michael Walsh4799e2a2018-11-16 15:29:43 -0600210 gp.lprint_varx("time_out", self.__time_out)
Michael Walsh4da179c2018-08-07 14:39:35 -0500211 self.__child_pid = 0
212 if self.__time_out is not None:
213 # Save the original SIGUSR1 handler for later restoration by this
214 # class' methods.
215 self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1)
216 # Designate a SIGUSR1 handling function.
217 signal.signal(signal.SIGUSR1, self.timed_out)
218 parent_pid = os.getpid()
219 self.__child_pid = os.fork()
220 if self.__child_pid == 0:
221 gp.dprint_timen("Child timer pid " + str(os.getpid())
222 + ": Sleeping for " + str(self.__time_out)
223 + " seconds.")
224 time.sleep(self.__time_out)
225 gp.dprint_timen("Child timer pid " + str(os.getpid())
226 + ": Sending SIGUSR1 to parent pid "
227 + str(parent_pid) + ".")
228 os.kill(parent_pid, signal.SIGUSR1)
229 os._exit(0)
230
231 # Call the user's function with the user's arguments.
232 children = gm.get_child_pids()
233 gp.lprint_var(children)
234 gp.lprint_timen("Calling the user's function.")
235 gp.lprint_varx("func_name", func.__name__)
236 gp.lprint_vars(args, kwargs)
237 try:
238 result = func(*args, **kwargs)
239 except Exception as func_exception:
240 # We must handle all exceptions so that we have the chance to
241 # cleanup before re-raising the exception.
242 gp.lprint_timen("Encountered exception in user's function.")
243 self.cleanup()
244 raise(func_exception)
245 gp.lprint_timen("Returned from the user's function.")
246
247 self.cleanup()
248
249 return result