blob: f29b362a354e05beb6d334bbd475f9ac91cd1ce5 [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
13
14
15class func_timer_class:
16 r"""
17 Define the func timer class.
18
19 A func timer object can be used to run any function/arguments but with an
20 additional benefit of being able to specify a time_out value. If the
21 function fails to complete before the timer expires, a ValueError
22 exception will be raised along with a detailed error message.
23
24 Example code:
25
26 func_timer = func_timer_class()
27 func_timer.run(run_key, "sleep 2", time_out=1)
28
29 In this example, the run_key function is being run by the func_timer
30 object with a time_out value of 1 second. "sleep 2" is a positional parm
31 for the run_key function.
32 """
33
34 def __init__(self,
35 obj_name='func_timer_class'):
36
37 # Initialize object variables.
38 self.__obj_name = obj_name
39 self.__func = None
40 self.__time_out = None
41 self.__child_pid = 0
42 # Save the original SIGUSR1 handler for later restoration by this
43 # class' methods.
44 self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1)
45
46 def __del__(self):
47 self.cleanup()
48
49 def sprint_obj(self):
50 r"""
51 sprint the fields of this object. This would normally be for debug
52 purposes.
53 """
54
55 buffer = ""
56 buffer += self.__class__.__name__ + ":\n"
57 indent = 2
58 try:
59 func_name = self.__func.__name__
60 except AttributeError:
61 func_name = ""
62 buffer += gp.sprint_var(func_name, hex=1, loc_col1_indent=indent)
63 buffer += gp.sprint_varx("time_out", self.__time_out,
64 loc_col1_indent=indent)
65 buffer += gp.sprint_varx("child_pid", self.__child_pid,
66 loc_col1_indent=indent)
67 buffer += gp.sprint_varx("original_SIGUSR1_handler",
68 self.__original_SIGUSR1_handler,
69 loc_col1_indent=indent)
70 return buffer
71
72 def print_obj(self):
73 r"""
74 print the fields of this object to stdout. This would normally be for
75 debug purposes.
76 """
77
78 sys.stdout.write(self.sprint_obj())
79
80 def cleanup(self):
81 r"""
82 Cleanup after the run method.
83 """
84
85 try:
86 gp.lprint_executing()
87 gp.lprint_var(self.__child_pid)
88 except AttributeError:
89 pass
90
91 # If self.__child_pid is 0, then we are either running as the child
92 # or we've already cleaned up.
93 # If self.__time_out is None, then no child process would have been
94 # spawned.
95 if self.__child_pid == 0 or self.__time_out is None:
96 return
97
98 # Restore the original SIGUSR1 handler.
99 if self.__original_SIGUSR1_handler != 0:
100 signal.signal(signal.SIGUSR1, self.__original_SIGUSR1_handler)
101 try:
102 gp.lprint_timen("Killing child pid " + str(self.__child_pid)
103 + ".")
104 os.kill(self.__child_pid, signal.SIGKILL)
105 except OSError:
106 gp.lprint_timen("Tolerated kill failure.")
107 try:
108 gp.lprint_timen("os.waitpid(" + str(self.__child_pid) + ")")
109 os.waitpid(self.__child_pid, 0)
110 except OSError:
111 gp.lprint_timen("Tolerated waitpid failure.")
112 self.__child_pid = 0
113 # For debug purposes, prove that the child pid was killed.
114 children = gm.get_child_pids()
115 gp.lprint_var(children)
116
117 def timed_out(self,
118 signal_number,
119 frame):
120 r"""
121 Handle a SIGUSR1 generated by the child process after the time_out has
122 expired.
123
124 signal_number The signal_number of the signal causing
125 this method to get invoked. This should
126 always be 10 (SIGUSR1).
127 frame The stack frame associated with the
128 function that times out.
129 """
130
131 gp.lprint_executing()
132
133 self.cleanup()
134
135 # Compose an error message.
136 err_msg = "The " + self.__func.__name__
137 err_msg += " function timed out after " + str(self.__time_out)
138 err_msg += " seconds.\n"
139 if not gp.robot_env:
140 err_msg += gp.sprint_call_stack()
141
142 raise ValueError(err_msg)
143
144 def run(self, func, *args, **kwargs):
145
146 r"""
147 Run the indicated function with the given args and kwargs and return
148 the value that the function returns. If the time_out value expires,
149 raise a ValueError exception with a detailed error message.
150
151 This method passes all of the args and kwargs directly to the child
152 function with the following important exception: If kwargs contains a
153 'time_out' value, it will be used to set the func timer object's
154 time_out value and then the kwargs['time_out'] entry will be removed.
155 If the time-out expires before the function finishes running, this
156 method will raise a ValueError.
157
158 Example:
159 func_timer = func_timer_class()
160 func_timer.run(run_key, "sleep 3", time_out=2)
161
162 Example:
163 try:
164 result = func_timer.run(func1, "parm1", time_out=2)
165 print_var(result)
166 except ValueError:
167 print("The func timed out but we're handling it.")
168
169 Description of argument(s):
170 func The function object which is to be called.
171 args The arguments which are to be passed to
172 the function object.
173 kwargs The keyword arguments which are to be
174 passed to the function object. As noted
175 above, kwargs['time_out'] will get special
176 treatment.
177 """
178
179 gp.lprint_executing()
180
181 # Store method parms as object parms.
182 self.__func = func
183
184 # Get self.__time_out value from kwargs. If kwargs['time_out'] is
185 # not present, self.__time_out will default to None.
186 self.__time_out = None
187 if len(kwargs) > 0:
188 if 'time_out' in kwargs:
189 self.__time_out = kwargs['time_out']
190 del kwargs['time_out']
191 if self.__time_out is not None:
192 self.__time_out = int(self.__time_out)
193
194 self.__child_pid = 0
195 if self.__time_out is not None:
196 # Save the original SIGUSR1 handler for later restoration by this
197 # class' methods.
198 self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1)
199 # Designate a SIGUSR1 handling function.
200 signal.signal(signal.SIGUSR1, self.timed_out)
201 parent_pid = os.getpid()
202 self.__child_pid = os.fork()
203 if self.__child_pid == 0:
204 gp.dprint_timen("Child timer pid " + str(os.getpid())
205 + ": Sleeping for " + str(self.__time_out)
206 + " seconds.")
207 time.sleep(self.__time_out)
208 gp.dprint_timen("Child timer pid " + str(os.getpid())
209 + ": Sending SIGUSR1 to parent pid "
210 + str(parent_pid) + ".")
211 os.kill(parent_pid, signal.SIGUSR1)
212 os._exit(0)
213
214 # Call the user's function with the user's arguments.
215 children = gm.get_child_pids()
216 gp.lprint_var(children)
217 gp.lprint_timen("Calling the user's function.")
218 gp.lprint_varx("func_name", func.__name__)
219 gp.lprint_vars(args, kwargs)
220 try:
221 result = func(*args, **kwargs)
222 except Exception as func_exception:
223 # We must handle all exceptions so that we have the chance to
224 # cleanup before re-raising the exception.
225 gp.lprint_timen("Encountered exception in user's function.")
226 self.cleanup()
227 raise(func_exception)
228 gp.lprint_timen("Returned from the user's function.")
229
230 self.cleanup()
231
232 return result