blob: e4855e7cf5e7cdfe77ce82bb58ea2fc00c0cad99 [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)
89 except AttributeError:
90 pass
91
92 # If self.__child_pid is 0, then we are either running as the child
93 # or we've already cleaned up.
94 # If self.__time_out is None, then no child process would have been
95 # spawned.
96 if self.__child_pid == 0 or self.__time_out is None:
97 return
98
99 # Restore the original SIGUSR1 handler.
100 if self.__original_SIGUSR1_handler != 0:
101 signal.signal(signal.SIGUSR1, self.__original_SIGUSR1_handler)
102 try:
103 gp.lprint_timen("Killing child pid " + str(self.__child_pid)
104 + ".")
105 os.kill(self.__child_pid, signal.SIGKILL)
106 except OSError:
107 gp.lprint_timen("Tolerated kill failure.")
108 try:
109 gp.lprint_timen("os.waitpid(" + str(self.__child_pid) + ")")
110 os.waitpid(self.__child_pid, 0)
111 except OSError:
112 gp.lprint_timen("Tolerated waitpid failure.")
113 self.__child_pid = 0
114 # For debug purposes, prove that the child pid was killed.
115 children = gm.get_child_pids()
116 gp.lprint_var(children)
117
118 def timed_out(self,
119 signal_number,
120 frame):
121 r"""
122 Handle a SIGUSR1 generated by the child process after the time_out has
123 expired.
124
125 signal_number The signal_number of the signal causing
126 this method to get invoked. This should
127 always be 10 (SIGUSR1).
128 frame The stack frame associated with the
129 function that times out.
130 """
131
132 gp.lprint_executing()
133
134 self.cleanup()
135
136 # Compose an error message.
137 err_msg = "The " + self.__func.__name__
138 err_msg += " function timed out after " + str(self.__time_out)
139 err_msg += " seconds.\n"
140 if not gp.robot_env:
141 err_msg += gp.sprint_call_stack()
142
143 raise ValueError(err_msg)
144
145 def run(self, func, *args, **kwargs):
146
147 r"""
148 Run the indicated function with the given args and kwargs and return
149 the value that the function returns. If the time_out value expires,
150 raise a ValueError exception with a detailed error message.
151
152 This method passes all of the args and kwargs directly to the child
153 function with the following important exception: If kwargs contains a
154 'time_out' value, it will be used to set the func timer object's
155 time_out value and then the kwargs['time_out'] entry will be removed.
156 If the time-out expires before the function finishes running, this
157 method will raise a ValueError.
158
159 Example:
160 func_timer = func_timer_class()
161 func_timer.run(run_key, "sleep 3", time_out=2)
162
163 Example:
164 try:
165 result = func_timer.run(func1, "parm1", time_out=2)
166 print_var(result)
167 except ValueError:
168 print("The func timed out but we're handling it.")
169
170 Description of argument(s):
171 func The function object which is to be called.
172 args The arguments which are to be passed to
173 the function object.
174 kwargs The keyword arguments which are to be
175 passed to the function object. As noted
176 above, kwargs['time_out'] will get special
177 treatment.
178 """
179
180 gp.lprint_executing()
181
182 # Store method parms as object parms.
183 self.__func = func
184
185 # Get self.__time_out value from kwargs. If kwargs['time_out'] is
186 # not present, self.__time_out will default to None.
187 self.__time_out = None
Michael Walsh4799e2a2018-11-16 15:29:43 -0600188 if 'time_out' in kwargs:
189 self.__time_out = kwargs['time_out']
190 del kwargs['time_out']
191 # Convert "none" string to None.
192 if type(self.__time_out) in (str, unicode)\
193 and self.__time_out.lower() == "none":
194 self.__time_out = None
195 if self.__time_out is not None:
196 self.__time_out = int(self.__time_out)
197 # Ensure that time_out is non-negative.
198 message = gv.svalid_range(self.__time_out, [0], "time_out")
199 if message != "":
200 raise ValueError("\n"
201 + gp.sprint_error_report(message,
202 format='long'))
Michael Walsh4da179c2018-08-07 14:39:35 -0500203
Michael Walsh4799e2a2018-11-16 15:29:43 -0600204 gp.lprint_varx("time_out", self.__time_out)
Michael Walsh4da179c2018-08-07 14:39:35 -0500205 self.__child_pid = 0
206 if self.__time_out is not None:
207 # Save the original SIGUSR1 handler for later restoration by this
208 # class' methods.
209 self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1)
210 # Designate a SIGUSR1 handling function.
211 signal.signal(signal.SIGUSR1, self.timed_out)
212 parent_pid = os.getpid()
213 self.__child_pid = os.fork()
214 if self.__child_pid == 0:
215 gp.dprint_timen("Child timer pid " + str(os.getpid())
216 + ": Sleeping for " + str(self.__time_out)
217 + " seconds.")
218 time.sleep(self.__time_out)
219 gp.dprint_timen("Child timer pid " + str(os.getpid())
220 + ": Sending SIGUSR1 to parent pid "
221 + str(parent_pid) + ".")
222 os.kill(parent_pid, signal.SIGUSR1)
223 os._exit(0)
224
225 # Call the user's function with the user's arguments.
226 children = gm.get_child_pids()
227 gp.lprint_var(children)
228 gp.lprint_timen("Calling the user's function.")
229 gp.lprint_varx("func_name", func.__name__)
230 gp.lprint_vars(args, kwargs)
231 try:
232 result = func(*args, **kwargs)
233 except Exception as func_exception:
234 # We must handle all exceptions so that we have the chance to
235 # cleanup before re-raising the exception.
236 gp.lprint_timen("Encountered exception in user's function.")
237 self.cleanup()
238 raise(func_exception)
239 gp.lprint_timen("Returned from the user's function.")
240
241 self.cleanup()
242
243 return result