blob: fc339c260247bf91c74a114f83b73604de96f9b1 [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
George Keishinge635ddc2022-12-08 07:38:02 -06008import signal
Patrick Williams20f38712022-12-08 06:18:26 -06009import sys
Michael Walsh4da179c2018-08-07 14:39:35 -050010import time
Patrick Williams20f38712022-12-08 06:18:26 -060011
George Keishinge635ddc2022-12-08 07:38:02 -060012import gen_misc as gm
Patrick Williams20f38712022-12-08 06:18:26 -060013import gen_print as gp
Michael Walsh4799e2a2018-11-16 15:29:43 -060014import gen_valid as gv
Michael Walsh4da179c2018-08-07 14:39:35 -050015
16
17class func_timer_class:
18 r"""
19 Define the func timer class.
20
Michael Walsh410b1782019-10-22 15:56:18 -050021 A func timer object can be used to run any function/arguments but with an additional benefit of being
22 able to specify a time_out value. If the function fails to complete before the timer expires, a
23 ValueError exception will be raised along with a detailed error message.
Michael Walsh4da179c2018-08-07 14:39:35 -050024
25 Example code:
26
27 func_timer = func_timer_class()
28 func_timer.run(run_key, "sleep 2", time_out=1)
29
Michael Walsh410b1782019-10-22 15:56:18 -050030 In this example, the run_key function is being run by the func_timer object with a time_out value of 1
31 second. "sleep 2" is a positional parm for the run_key function.
Michael Walsh4da179c2018-08-07 14:39:35 -050032 """
33
Patrick Williams20f38712022-12-08 06:18:26 -060034 def __init__(self, obj_name="func_timer_class"):
Michael Walsh4da179c2018-08-07 14:39:35 -050035 # Initialize object variables.
36 self.__obj_name = obj_name
37 self.__func = None
38 self.__time_out = None
39 self.__child_pid = 0
Michael Walsh410b1782019-10-22 15:56:18 -050040 # Save the original SIGUSR1 handler for later restoration by this class' methods.
Michael Walsh4da179c2018-08-07 14:39:35 -050041 self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1)
42
43 def __del__(self):
44 self.cleanup()
45
46 def sprint_obj(self):
47 r"""
Michael Walsh410b1782019-10-22 15:56:18 -050048 sprint the fields of this object. This would normally be for debug purposes.
Michael Walsh4da179c2018-08-07 14:39:35 -050049 """
50
51 buffer = ""
52 buffer += self.__class__.__name__ + ":\n"
53 indent = 2
54 try:
55 func_name = self.__func.__name__
56 except AttributeError:
57 func_name = ""
Michael Walsh0d5f96a2019-05-20 10:09:57 -050058 buffer += gp.sprint_var(func_name, indent=indent)
59 buffer += gp.sprint_varx("time_out", self.__time_out, indent=indent)
60 buffer += gp.sprint_varx("child_pid", self.__child_pid, indent=indent)
Patrick Williams20f38712022-12-08 06:18:26 -060061 buffer += gp.sprint_varx(
62 "original_SIGUSR1_handler",
63 self.__original_SIGUSR1_handler,
64 indent=indent,
65 )
Michael Walsh4da179c2018-08-07 14:39:35 -050066 return buffer
67
68 def print_obj(self):
69 r"""
Michael Walsh410b1782019-10-22 15:56:18 -050070 print the fields of this object to stdout. This would normally be for debug purposes.
Michael Walsh4da179c2018-08-07 14:39:35 -050071 """
72
73 sys.stdout.write(self.sprint_obj())
74
75 def cleanup(self):
76 r"""
77 Cleanup after the run method.
78 """
79
80 try:
81 gp.lprint_executing()
82 gp.lprint_var(self.__child_pid)
Michael Walsh89c0aaa2019-04-18 11:00:54 -050083 except (AttributeError, KeyError, TypeError):
Michael Walsh410b1782019-10-22 15:56:18 -050084 # NOTE: In python 3, this code fails with "KeyError: ('__main__',)" when calling functions like
85 # lprint_executing that use inspect.stack() during object destruction. No fixes found so
86 # tolerating the error. In python 2.x, it may fail with TypeError. This seems to happen when
87 # cleaning up after an exception was raised.
Michael Walsh4da179c2018-08-07 14:39:35 -050088 pass
89
Michael Walsh410b1782019-10-22 15:56:18 -050090 # If self.__child_pid is 0, then we are either running as the child or we've already cleaned up.
91 # If self.__time_out is None, then no child process would have been spawned.
Michael Walsh4da179c2018-08-07 14:39:35 -050092 if self.__child_pid == 0 or self.__time_out is None:
93 return
94
95 # Restore the original SIGUSR1 handler.
96 if self.__original_SIGUSR1_handler != 0:
97 signal.signal(signal.SIGUSR1, self.__original_SIGUSR1_handler)
98 try:
Patrick Williams20f38712022-12-08 06:18:26 -060099 gp.lprint_timen("Killing child pid " + str(self.__child_pid) + ".")
Michael Walsh4da179c2018-08-07 14:39:35 -0500100 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
Patrick Williams20f38712022-12-08 06:18:26 -0600113 def timed_out(self, signal_number, frame):
Michael Walsh4da179c2018-08-07 14:39:35 -0500114 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500115 Handle a SIGUSR1 generated by the child process after the time_out has expired.
Michael Walsh4da179c2018-08-07 14:39:35 -0500116
Michael Walsh410b1782019-10-22 15:56:18 -0500117 signal_number The signal_number of the signal causing this method to get invoked. This
118 should always be 10 (SIGUSR1).
119 frame The stack frame associated with the function that times out.
Michael Walsh4da179c2018-08-07 14:39:35 -0500120 """
121
122 gp.lprint_executing()
123
124 self.cleanup()
125
126 # Compose an error message.
127 err_msg = "The " + self.__func.__name__
128 err_msg += " function timed out after " + str(self.__time_out)
129 err_msg += " seconds.\n"
130 if not gp.robot_env:
131 err_msg += gp.sprint_call_stack()
132
133 raise ValueError(err_msg)
134
135 def run(self, func, *args, **kwargs):
Michael Walsh4da179c2018-08-07 14:39:35 -0500136 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500137 Run the indicated function with the given args and kwargs and return the value that the function
138 returns. If the time_out value expires, raise a ValueError exception with a detailed error message.
Michael Walsh4da179c2018-08-07 14:39:35 -0500139
Michael Walsh410b1782019-10-22 15:56:18 -0500140 This method passes all of the args and kwargs directly to the child function with the following
141 important exception: If kwargs contains a 'time_out' value, it will be used to set the func timer
142 object's time_out value and then the kwargs['time_out'] entry will be removed. If the time-out
143 expires before the function finishes running, this method will raise a ValueError.
Michael Walsh4da179c2018-08-07 14:39:35 -0500144
145 Example:
146 func_timer = func_timer_class()
147 func_timer.run(run_key, "sleep 3", time_out=2)
148
149 Example:
150 try:
151 result = func_timer.run(func1, "parm1", time_out=2)
152 print_var(result)
153 except ValueError:
154 print("The func timed out but we're handling it.")
155
156 Description of argument(s):
157 func The function object which is to be called.
Michael Walsh410b1782019-10-22 15:56:18 -0500158 args The arguments which are to be passed to the function object.
159 kwargs The keyword arguments which are to be passed to the function object. As
160 noted above, kwargs['time_out'] will get special treatment.
Michael Walsh4da179c2018-08-07 14:39:35 -0500161 """
162
163 gp.lprint_executing()
164
165 # Store method parms as object parms.
166 self.__func = func
167
Michael Walsh410b1782019-10-22 15:56:18 -0500168 # Get self.__time_out value from kwargs. If kwargs['time_out'] is not present, self.__time_out will
169 # default to None.
Michael Walsh4da179c2018-08-07 14:39:35 -0500170 self.__time_out = None
Patrick Williams20f38712022-12-08 06:18:26 -0600171 if "time_out" in kwargs:
172 self.__time_out = kwargs["time_out"]
173 del kwargs["time_out"]
Michael Walsh4799e2a2018-11-16 15:29:43 -0600174 # Convert "none" string to None.
George Keishing36efbc02018-12-12 10:18:23 -0600175 try:
176 if self.__time_out.lower() == "none":
177 self.__time_out = None
178 except AttributeError:
179 pass
Michael Walsh4799e2a2018-11-16 15:29:43 -0600180 if self.__time_out is not None:
181 self.__time_out = int(self.__time_out)
182 # Ensure that time_out is non-negative.
Patrick Williams20f38712022-12-08 06:18:26 -0600183 message = gv.valid_range(
184 self.__time_out, 0, var_name="time_out"
185 )
Michael Walsh4799e2a2018-11-16 15:29:43 -0600186 if message != "":
Patrick Williams20f38712022-12-08 06:18:26 -0600187 raise ValueError(
188 "\n" + gp.sprint_error_report(message, format="long")
189 )
Michael Walsh4da179c2018-08-07 14:39:35 -0500190
Michael Walsh4799e2a2018-11-16 15:29:43 -0600191 gp.lprint_varx("time_out", self.__time_out)
Michael Walsh4da179c2018-08-07 14:39:35 -0500192 self.__child_pid = 0
193 if self.__time_out is not None:
Michael Walsh410b1782019-10-22 15:56:18 -0500194 # Save the original SIGUSR1 handler for later restoration by this class' methods.
Michael Walsh4da179c2018-08-07 14:39:35 -0500195 self.__original_SIGUSR1_handler = signal.getsignal(signal.SIGUSR1)
196 # Designate a SIGUSR1 handling function.
197 signal.signal(signal.SIGUSR1, self.timed_out)
198 parent_pid = os.getpid()
199 self.__child_pid = os.fork()
200 if self.__child_pid == 0:
Patrick Williams20f38712022-12-08 06:18:26 -0600201 gp.dprint_timen(
202 "Child timer pid "
203 + str(os.getpid())
204 + ": Sleeping for "
205 + str(self.__time_out)
206 + " seconds."
207 )
Michael Walsh4da179c2018-08-07 14:39:35 -0500208 time.sleep(self.__time_out)
Patrick Williams20f38712022-12-08 06:18:26 -0600209 gp.dprint_timen(
210 "Child timer pid "
211 + str(os.getpid())
212 + ": Sending SIGUSR1 to parent pid "
213 + str(parent_pid)
214 + "."
215 )
Michael Walsh4da179c2018-08-07 14:39:35 -0500216 os.kill(parent_pid, signal.SIGUSR1)
217 os._exit(0)
218
219 # Call the user's function with the user's arguments.
220 children = gm.get_child_pids()
221 gp.lprint_var(children)
222 gp.lprint_timen("Calling the user's function.")
223 gp.lprint_varx("func_name", func.__name__)
224 gp.lprint_vars(args, kwargs)
225 try:
226 result = func(*args, **kwargs)
227 except Exception as func_exception:
Michael Walsh410b1782019-10-22 15:56:18 -0500228 # We must handle all exceptions so that we have the chance to cleanup before re-raising the
229 # exception.
Michael Walsh4da179c2018-08-07 14:39:35 -0500230 gp.lprint_timen("Encountered exception in user's function.")
231 self.cleanup()
George Keishing62246352022-08-01 01:20:06 -0500232 raise (func_exception)
Michael Walsh4da179c2018-08-07 14:39:35 -0500233 gp.lprint_timen("Returned from the user's function.")
234
235 self.cleanup()
236
237 return result