blob: 1919c3e7cb5451da57d37ea061a65d027e1c2c2a [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 = ""
Michael Walsh0d5f96a2019-05-20 10:09:57 -050063 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)
Michael Walsh4da179c2018-08-07 14:39:35 -050066 buffer += gp.sprint_varx("original_SIGUSR1_handler",
67 self.__original_SIGUSR1_handler,
Michael Walsh0d5f96a2019-05-20 10:09:57 -050068 indent=indent)
Michael Walsh4da179c2018-08-07 14:39:35 -050069 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)
Michael Walsh89c0aaa2019-04-18 11:00:54 -050087 except (AttributeError, KeyError, TypeError):
George Keishing36efbc02018-12-12 10:18:23 -060088 # 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
Michael Walsh89c0aaa2019-04-18 11:00:54 -050091 # 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.
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.
Michael Walshec01a6f2019-08-01 12:43:20 -0500204 message = gv.valid_range(self.__time_out, 0,
205 var_name="time_out")
Michael Walsh4799e2a2018-11-16 15:29:43 -0600206 if message != "":
207 raise ValueError("\n"
208 + gp.sprint_error_report(message,
209 format='long'))
Michael Walsh4da179c2018-08-07 14:39:35 -0500210
Michael Walsh4799e2a2018-11-16 15:29:43 -0600211 gp.lprint_varx("time_out", self.__time_out)
Michael Walsh4da179c2018-08-07 14:39:35 -0500212 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