A thread that can be interrupted with t.raiseException() Based on https://stackoverflow.com/a/325528
| 124 | |
| 125 | |
| 126 | class InterruptibleThread(threading.Thread): |
| 127 | """ |
| 128 | A thread that can be interrupted with t.raiseException() |
| 129 | Based on https://stackoverflow.com/a/325528 |
| 130 | """ |
| 131 | |
| 132 | def run(self): |
| 133 | """ |
| 134 | Catch uncaught exceptions and save them to t.exc. |
| 135 | Necessary to remove unwanted "Exception ignored in thread started by..." and "Exception ignored in sys.unraisablehook..." |
| 136 | https://stackoverflow.com/a/31614591 |
| 137 | """ |
| 138 | self.exc = None |
| 139 | try: |
| 140 | self.ret = self._target(*self._args, **self._kwargs) # type: ignore |
| 141 | except Exception as e: |
| 142 | self.exc = e |
| 143 | |
| 144 | def raiseException(self, ExceptionClass): |
| 145 | """ |
| 146 | Interrupt thread with an exception. |
| 147 | Exception happens after the current system call finishes executing. |
| 148 | (So eg time.sleep() is not interrupted.) |
| 149 | If exception isn't firing then you can try calling this in a loop. |
| 150 | """ |
| 151 | if not self.is_alive(): |
| 152 | return # do nothing |
| 153 | thread_id = self.ident |
| 154 | if thread_id is None: |
| 155 | raise Exception("couldn't get thread identifier") |
| 156 | res = ctypes.pythonapi.PyThreadState_SetAsyncExc( |
| 157 | ctypes.c_long(thread_id), ctypes.py_object(ExceptionClass) |
| 158 | ) |
| 159 | if res == 0: |
| 160 | raise ValueError("invalid thread id") |
| 161 | elif res != 1: |
| 162 | # "if it returns a number greater than one, you're in trouble, |
| 163 | # and you should call it again with exc=NULL to revert the effect" |
| 164 | ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), None) |
| 165 | raise SystemError("PyThreadState_SetAsyncExc failed") |
| 166 | |
| 167 | |
| 168 | worker_counter_box = [0] # just for logging |