blob: 852bbf2f23bb04f086d267cedbae0f17dcbca14a [file] [log] [blame]
George Keishinge7e91712021-09-03 11:28:44 -05001#!/usr/bin/env python3
Michael Walsh4da179c2018-08-07 14:39:35 -05002
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
Michael Walsh410b1782019-10-22 15:56:18 -050020 A func timer object can be used to run any function/arguments but with an additional benefit of being
21 able to specify a time_out value. If the function fails to complete before the timer expires, a
22 ValueError exception will be raised along with a detailed error message.
Michael Walsh4da179c2018-08-07 14:39:35 -050023
24 Example code:
25
26 func_timer = func_timer_class()
27 func_timer.run(run_key, "sleep 2", time_out=1)
28
Michael Walsh410b1782019-10-22 15:56:18 -050029 In this example, the run_key function is being run by the func_timer object with a time_out value of 1
30 second. "sleep 2" is a positional parm for the run_key function.
Michael Walsh4da179c2018-08-07 14:39:35 -050031 """
32
33 def __init__(self,
34 obj_name='func_timer_class'):
35
36 # Initialize object variables.
37 self.__obj_name = obj_name
38 self.__func = None
39 self.__time_out = None
40 self.__child_pid = 0
Michael Walsh410b1782019-10-22 15:56:18 -050041 # Save the original SIGUSR1 handler for later restoration by this class' methods.
Michael Walsh4da179c2018-08-07 14:39:35 -050042 self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1)
43
44 def __del__(self):
45 self.cleanup()
46
47 def sprint_obj(self):
48 r"""
Michael Walsh410b1782019-10-22 15:56:18 -050049 sprint the fields of this object. This would normally be for debug purposes.
Michael Walsh4da179c2018-08-07 14:39:35 -050050 """
51
52 buffer = ""
53 buffer += self.__class__.__name__ + ":\n"
54 indent = 2
55 try:
56 func_name = self.__func.__name__
57 except AttributeError:
58 func_name = ""
Michael Walsh0d5f96a2019-05-20 10:09:57 -050059 buffer += gp.sprint_var(func_name, indent=indent)
60 buffer += gp.sprint_varx("time_out", self.__time_out, indent=indent)
61 buffer += gp.sprint_varx("child_pid", self.__child_pid, indent=indent)
Michael Walsh4da179c2018-08-07 14:39:35 -050062 buffer += gp.sprint_varx("original_SIGUSR1_handler",
63 self.__original_SIGUSR1_handler,
Michael Walsh0d5f96a2019-05-20 10:09:57 -050064 indent=indent)
Michael Walsh4da179c2018-08-07 14:39:35 -050065 return buffer
66
67 def print_obj(self):
68 r"""
Michael Walsh410b1782019-10-22 15:56:18 -050069 print the fields of this object to stdout. This would normally be for debug purposes.
Michael Walsh4da179c2018-08-07 14:39:35 -050070 """
71
72 sys.stdout.write(self.sprint_obj())
73
74 def cleanup(self):
75 r"""
76 Cleanup after the run method.
77 """
78
79 try:
80 gp.lprint_executing()
81 gp.lprint_var(self.__child_pid)
Michael Walsh89c0aaa2019-04-18 11:00:54 -050082 except (AttributeError, KeyError, TypeError):
Michael Walsh410b1782019-10-22 15:56:18 -050083 # NOTE: In python 3, this code fails with "KeyError: ('__main__',)" when calling functions like
84 # lprint_executing that use inspect.stack() during object destruction. No fixes found so
85 # tolerating the error. In python 2.x, it may fail with TypeError. This seems to happen when
86 # cleaning up after an exception was raised.
Michael Walsh4da179c2018-08-07 14:39:35 -050087 pass
88
Michael Walsh410b1782019-10-22 15:56:18 -050089 # If self.__child_pid is 0, then we are either running as the child or we've already cleaned up.
90 # If self.__time_out is None, then no child process would have been spawned.
Michael Walsh4da179c2018-08-07 14:39:35 -050091 if self.__child_pid == 0 or self.__time_out is None:
92 return
93
94 # Restore the original SIGUSR1 handler.
95 if self.__original_SIGUSR1_handler != 0:
96 signal.signal(signal.SIGUSR1, self.__original_SIGUSR1_handler)
97 try:
98 gp.lprint_timen("Killing child pid " + str(self.__child_pid)
99 + ".")
100 os.kill(self.__child_pid, signal.SIGKILL)
101 except OSError:
102 gp.lprint_timen("Tolerated kill failure.")
103 try:
104 gp.lprint_timen("os.waitpid(" + str(self.__child_pid) + ")")
105 os.waitpid(self.__child_pid, 0)
106 except OSError:
107 gp.lprint_timen("Tolerated waitpid failure.")
108 self.__child_pid = 0
109 # For debug purposes, prove that the child pid was killed.
110 children = gm.get_child_pids()
111 gp.lprint_var(children)
112
113 def timed_out(self,
114 signal_number,
115 frame):
116 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500117 Handle a SIGUSR1 generated by the child process after the time_out has expired.
Michael Walsh4da179c2018-08-07 14:39:35 -0500118
Michael Walsh410b1782019-10-22 15:56:18 -0500119 signal_number The signal_number of the signal causing this method to get invoked. This
120 should always be 10 (SIGUSR1).
121 frame The stack frame associated with the function that times out.
Michael Walsh4da179c2018-08-07 14:39:35 -0500122 """
123
124 gp.lprint_executing()
125
126 self.cleanup()
127
128 # Compose an error message.
129 err_msg = "The " + self.__func.__name__
130 err_msg += " function timed out after " + str(self.__time_out)
131 err_msg += " seconds.\n"
132 if not gp.robot_env:
133 err_msg += gp.sprint_call_stack()
134
135 raise ValueError(err_msg)
136
137 def run(self, func, *args, **kwargs):
138
139 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500140 Run the indicated function with the given args and kwargs and return the value that the function
141 returns. If the time_out value expires, raise a ValueError exception with a detailed error message.
Michael Walsh4da179c2018-08-07 14:39:35 -0500142
Michael Walsh410b1782019-10-22 15:56:18 -0500143 This method passes all of the args and kwargs directly to the child function with the following
144 important exception: If kwargs contains a 'time_out' value, it will be used to set the func timer
145 object's time_out value and then the kwargs['time_out'] entry will be removed. If the time-out
146 expires before the function finishes running, this method will raise a ValueError.
Michael Walsh4da179c2018-08-07 14:39:35 -0500147
148 Example:
149 func_timer = func_timer_class()
150 func_timer.run(run_key, "sleep 3", time_out=2)
151
152 Example:
153 try:
154 result = func_timer.run(func1, "parm1", time_out=2)
155 print_var(result)
156 except ValueError:
157 print("The func timed out but we're handling it.")
158
159 Description of argument(s):
160 func The function object which is to be called.
Michael Walsh410b1782019-10-22 15:56:18 -0500161 args The arguments which are to be passed to the function object.
162 kwargs The keyword arguments which are to be passed to the function object. As
163 noted above, kwargs['time_out'] will get special treatment.
Michael Walsh4da179c2018-08-07 14:39:35 -0500164 """
165
166 gp.lprint_executing()
167
168 # Store method parms as object parms.
169 self.__func = func
170
Michael Walsh410b1782019-10-22 15:56:18 -0500171 # Get self.__time_out value from kwargs. If kwargs['time_out'] is not present, self.__time_out will
172 # default to None.
Michael Walsh4da179c2018-08-07 14:39:35 -0500173 self.__time_out = None
Michael Walsh4799e2a2018-11-16 15:29:43 -0600174 if 'time_out' in kwargs:
175 self.__time_out = kwargs['time_out']
176 del kwargs['time_out']
177 # Convert "none" string to None.
George Keishing36efbc02018-12-12 10:18:23 -0600178 try:
179 if self.__time_out.lower() == "none":
180 self.__time_out = None
181 except AttributeError:
182 pass
Michael Walsh4799e2a2018-11-16 15:29:43 -0600183 if self.__time_out is not None:
184 self.__time_out = int(self.__time_out)
185 # Ensure that time_out is non-negative.
Michael Walshec01a6f2019-08-01 12:43:20 -0500186 message = gv.valid_range(self.__time_out, 0,
187 var_name="time_out")
Michael Walsh4799e2a2018-11-16 15:29:43 -0600188 if message != "":
189 raise ValueError("\n"
190 + gp.sprint_error_report(message,
191 format='long'))
Michael Walsh4da179c2018-08-07 14:39:35 -0500192
Michael Walsh4799e2a2018-11-16 15:29:43 -0600193 gp.lprint_varx("time_out", self.__time_out)
Michael Walsh4da179c2018-08-07 14:39:35 -0500194 self.__child_pid = 0
195 if self.__time_out is not None:
Michael Walsh410b1782019-10-22 15:56:18 -0500196 # Save the original SIGUSR1 handler for later restoration by this class' methods.
Michael Walsh4da179c2018-08-07 14:39:35 -0500197 self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1)
198 # Designate a SIGUSR1 handling function.
199 signal.signal(signal.SIGUSR1, self.timed_out)
200 parent_pid = os.getpid()
201 self.__child_pid = os.fork()
202 if self.__child_pid == 0:
203 gp.dprint_timen("Child timer pid " + str(os.getpid())
204 + ": Sleeping for " + str(self.__time_out)
205 + " seconds.")
206 time.sleep(self.__time_out)
207 gp.dprint_timen("Child timer pid " + str(os.getpid())
208 + ": Sending SIGUSR1 to parent pid "
209 + str(parent_pid) + ".")
210 os.kill(parent_pid, signal.SIGUSR1)
211 os._exit(0)
212
213 # Call the user's function with the user's arguments.
214 children = gm.get_child_pids()
215 gp.lprint_var(children)
216 gp.lprint_timen("Calling the user's function.")
217 gp.lprint_varx("func_name", func.__name__)
218 gp.lprint_vars(args, kwargs)
219 try:
220 result = func(*args, **kwargs)
221 except Exception as func_exception:
Michael Walsh410b1782019-10-22 15:56:18 -0500222 # We must handle all exceptions so that we have the chance to cleanup before re-raising the
223 # exception.
Michael Walsh4da179c2018-08-07 14:39:35 -0500224 gp.lprint_timen("Encountered exception in user's function.")
225 self.cleanup()
George Keishing62246352022-08-01 01:20:06 -0500226 raise (func_exception)
Michael Walsh4da179c2018-08-07 14:39:35 -0500227 gp.lprint_timen("Returned from the user's function.")
228
229 self.cleanup()
230
231 return result