1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2014 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing an import hook patching thread modules to get debugged too. 8""" 9 10import os 11import sys 12import contextlib 13 14import _thread 15import threading 16 17from DebugBase import DebugBase 18 19_qtThreadNumber = 1 20 21 22class ThreadExtension: 23 """ 24 Class implementing the thread support for the debugger. 25 26 Provides methods for intercepting thread creation, retrieving the running 27 threads and their name and state. 28 """ 29 def __init__(self): 30 """ 31 Constructor 32 """ 33 self.threadNumber = 1 34 self._original_start_new_thread = None 35 36 self.clientLock = threading.RLock() 37 38 # dictionary of all threads running {id: DebugBase} 39 self.threads = {_thread.get_ident(): self} 40 41 # the "current" thread, basically for variables view 42 self.currentThread = self 43 # the thread we are at a breakpoint continuing at next command 44 self.currentThreadExec = self 45 46 # special objects representing the main scripts thread and frame 47 self.mainThread = self 48 49 def attachThread(self, target=None, args=None, kwargs=None, 50 mainThread=False): 51 """ 52 Public method to setup a standard thread for DebugClient to debug. 53 54 If mainThread is True, then we are attaching to the already 55 started mainthread of the app and the rest of the args are ignored. 56 57 @param target the start function of the target thread (i.e. the user 58 code) 59 @param args arguments to pass to target 60 @param kwargs keyword arguments to pass to target 61 @param mainThread True, if we are attaching to the already 62 started mainthread of the app 63 @return identifier of the created thread 64 """ 65 if kwargs is None: 66 kwargs = {} 67 68 if mainThread: 69 ident = _thread.get_ident() 70 name = 'MainThread' 71 newThread = self.mainThread 72 newThread.isMainThread = True 73 if self.debugging: 74 sys.setprofile(newThread.profile) 75 76 else: 77 newThread = DebugBase(self) 78 ident = self._original_start_new_thread( 79 newThread.bootstrap, (target, args, kwargs)) 80 name = 'Thread-{0}'.format(self.threadNumber) 81 self.threadNumber += 1 82 83 newThread.id = ident 84 newThread.name = name 85 86 self.threads[ident] = newThread 87 88 return ident 89 90 def threadTerminated(self, threadId): 91 """ 92 Public method called when a DebugThread has exited. 93 94 @param threadId id of the DebugThread that has exited 95 @type int 96 """ 97 self.lockClient() 98 try: 99 with contextlib.suppress(KeyError): 100 del self.threads[threadId] 101 finally: 102 self.unlockClient() 103 104 def lockClient(self, blocking=True): 105 """ 106 Public method to acquire the lock for this client. 107 108 @param blocking flag to indicating a blocking lock 109 @type bool 110 @return flag indicating successful locking 111 @rtype bool 112 """ 113 return self.clientLock.acquire(blocking) 114 115 def unlockClient(self): 116 """ 117 Public method to release the lock for this client. 118 """ 119 with contextlib.suppress(RuntimeError): 120 self.clientLock.release() 121 122 def setCurrentThread(self, threadId): 123 """ 124 Public method to set the current thread. 125 126 @param threadId the id the current thread should be set to. 127 @type int 128 """ 129 try: 130 self.lockClient() 131 if threadId is None: 132 self.currentThread = None 133 else: 134 self.currentThread = self.threads.get(threadId) 135 finally: 136 self.unlockClient() 137 138 def dumpThreadList(self): 139 """ 140 Public method to send the list of threads. 141 """ 142 self.updateThreadList() 143 144 threadList = [] 145 currentId = _thread.get_ident() 146 # update thread names set by user (threading.setName) 147 threadNames = {t.ident: t.getName() for t in threading.enumerate()} 148 149 for threadId, thd in self.threads.items(): 150 d = {"id": threadId} 151 try: 152 d["name"] = threadNames.get(threadId, thd.name) 153 d["broken"] = thd.isBroken 154 d["except"] = thd.isException 155 except Exception: 156 d["name"] = 'UnknownThread' 157 d["broken"] = False 158 d["except"] = False 159 160 threadList.append(d) 161 162 self.sendJsonCommand("ResponseThreadList", { 163 "currentID": currentId, 164 "threadList": threadList, 165 }) 166 167 def getExecutedFrame(self, frame): 168 """ 169 Public method to return the currently executed frame. 170 171 @param frame the current frame 172 @type frame object 173 @return the frame which is excecuted (without debugger frames) 174 @rtype frame object 175 """ 176 # to get the currently executed frame, skip all frames belonging to the 177 # debugger 178 while frame is not None: 179 baseName = os.path.basename(frame.f_code.co_filename) 180 if not baseName.startswith( 181 ('DebugClientBase.py', 'DebugBase.py', 'AsyncFile.py', 182 'ThreadExtension.py')): 183 break 184 frame = frame.f_back 185 186 return frame 187 188 def updateThreadList(self): 189 """ 190 Public method to update the list of running threads. 191 """ 192 frames = sys._current_frames() 193 for threadId, frame in frames.items(): 194 # skip our own timer thread 195 if frame.f_code.co_name == '__eventPollTimer': 196 continue 197 198 # Unknown thread 199 if threadId not in self.threads: 200 newThread = DebugBase(self) 201 name = 'Thread-{0}'.format(self.threadNumber) 202 self.threadNumber += 1 203 204 newThread.id = threadId 205 newThread.name = name 206 self.threads[threadId] = newThread 207 208 # adjust current frame 209 if "__pypy__" not in sys.builtin_module_names: 210 # Don't update with None 211 currentFrame = self.getExecutedFrame(frame) 212 if (currentFrame is not None and 213 self.threads[threadId].isBroken is False): 214 self.threads[threadId].currentFrame = currentFrame 215 216 # Clean up obsolet because terminated threads 217 self.threads = {id_: thrd for id_, thrd in self.threads.items() 218 if id_ in frames} 219 220 ####################################################################### 221 ## Methods below deal with patching various modules to support 222 ## debugging of threads. 223 ####################################################################### 224 225 def patchPyThread(self, module): 226 """ 227 Public method to patch Python _thread (Python3) and thread (Python2) 228 modules. 229 230 @param module reference to the imported module to be patched 231 @type module 232 """ 233 # make thread hooks available to system 234 self._original_start_new_thread = module.start_new_thread 235 module.start_new_thread = self.attachThread 236 237 def patchGreenlet(self, module): 238 """ 239 Public method to patch the 'greenlet' module. 240 241 @param module reference to the imported module to be patched 242 @type module 243 @return flag indicating that the module was processed 244 @rtype bool 245 """ 246 # Check for greenlet.settrace 247 if hasattr(module, 'settrace'): 248 DebugBase.pollTimerEnabled = False 249 return True 250 return False 251 252 def patchPyThreading(self, module): 253 """ 254 Public method to patch the Python threading module. 255 256 @param module reference to the imported module to be patched 257 @type module 258 """ 259 # _debugClient as a class attribute can't be accessed in following 260 # class. Therefore we need a global variable. 261 _debugClient = self 262 263 def _bootstrap(self, run): 264 """ 265 Bootstrap for threading, which reports exceptions correctly. 266 267 @param run the run method of threading.Thread 268 @type method pointer 269 """ 270 newThread = DebugBase(_debugClient) 271 newThread.name = self.name 272 273 _debugClient.threads[self.ident] = newThread 274 _debugClient.dumpThreadList() 275 276 # see DebugBase.bootstrap 277 sys.settrace(newThread.trace_dispatch) 278 try: 279 run() 280 except Exception: 281 excinfo = sys.exc_info() 282 newThread.user_exception(excinfo, True) 283 finally: 284 sys.settrace(None) 285 _debugClient.dumpThreadList() 286 287 class ThreadWrapper(module.Thread): 288 """ 289 Wrapper class for threading.Thread. 290 """ 291 def __init__(self, *args, **kwargs): 292 """ 293 Constructor 294 """ 295 # Overwrite the provided run method with our own, to 296 # intercept the thread creation by threading.Thread 297 self.run = lambda s=self, run=self.run: _bootstrap(s, run) 298 299 super().__init__(*args, **kwargs) 300 301 module.Thread = ThreadWrapper 302 303 # Special handling of threading.(_)Timer 304 timer = module.Timer 305 306 class TimerWrapper(timer, ThreadWrapper): 307 """ 308 Wrapper class for threading.(_)Timer. 309 """ 310 def __init__(self, interval, function, *args, **kwargs): 311 """ 312 Constructor 313 """ 314 super().__init__( 315 interval, function, *args, **kwargs) 316 317 module.Timer = TimerWrapper 318 319 # Special handling of threading._DummyThread 320 class DummyThreadWrapper(module._DummyThread, ThreadWrapper): 321 """ 322 Wrapper class for threading._DummyThread. 323 """ 324 def __init__(self, *args, **kwargs): 325 """ 326 Constructor 327 """ 328 super().__init__(*args, **kwargs) 329 330 module._DummyThread = DummyThreadWrapper 331 332 def patchQThread(self, module): 333 """ 334 Public method to patch the QtCore module's QThread. 335 336 @param module reference to the imported module to be patched 337 @type module 338 """ 339 # _debugClient as a class attribute can't be accessed in following 340 # class. Therefore we need a global variable. 341 _debugClient = self 342 343 def _bootstrapQThread(self, run): 344 """ 345 Bootstrap for QThread, which reports exceptions correctly. 346 347 @param run the run method of *.QThread 348 @type method pointer 349 """ 350 global _qtThreadNumber 351 352 newThread = DebugBase(_debugClient) 353 ident = _thread.get_ident() 354 name = 'QtThread-{0}'.format(_qtThreadNumber) 355 356 _qtThreadNumber += 1 357 358 newThread.id = ident 359 newThread.name = name 360 361 _debugClient.threads[ident] = newThread 362 _debugClient.dumpThreadList() 363 364 # see DebugBase.bootstrap 365 sys.settrace(newThread.trace_dispatch) 366 try: 367 run() 368 except SystemExit: 369 # *.QThreads doesn't like SystemExit 370 pass 371 except Exception: 372 excinfo = sys.exc_info() 373 newThread.user_exception(excinfo, True) 374 finally: 375 sys.settrace(None) 376 _debugClient.dumpThreadList() 377 378 class QThreadWrapper(module.QThread): 379 """ 380 Wrapper class for *.QThread. 381 """ 382 def __init__(self, *args, **kwargs): 383 """ 384 Constructor 385 """ 386 # Overwrite the provided run method with our own, to 387 # intercept the thread creation by Qt 388 self.run = lambda s=self, run=self.run: ( 389 _bootstrapQThread(s, run)) 390 391 super().__init__(*args, **kwargs) 392 393 class QRunnableWrapper(module.QRunnable): 394 """ 395 Wrapper class for *.QRunnable. 396 """ 397 def __init__(self, *args, **kwargs): 398 """ 399 Constructor 400 """ 401 # Overwrite the provided run method with our own, to 402 # intercept the thread creation by Qt 403 self.run = lambda s=self, run=self.run: ( 404 _bootstrapQThread(s, run)) 405 406 super().__init__(*args, **kwargs) 407 408 module.QThread = QThreadWrapper 409 module.QRunnable = QRunnableWrapper 410