eric6/DebugClients/Python/ThreadExtension.py

branch
maintenance
changeset 8043
0acf98cd089a
parent 7924
8a96736d465e
parent 7986
2971d5d19951
child 8273
698ae46f40a4
equal deleted inserted replaced
7991:866adc8c315b 8043:0acf98cd089a
5 5
6 """ 6 """
7 Module implementing an import hook patching thread modules to get debugged too. 7 Module implementing an import hook patching thread modules to get debugged too.
8 """ 8 """
9 9
10 import os.path 10 import os
11 import sys 11 import sys
12 import importlib
13 12
14 import _thread 13 import _thread
15 import threading 14 import threading
16 15
17 from DebugBase import DebugBase 16 from DebugBase import DebugBase
21 20
22 class ThreadExtension(object): 21 class ThreadExtension(object):
23 """ 22 """
24 Class implementing the thread support for the debugger. 23 Class implementing the thread support for the debugger.
25 24
26 Provides methods for intercepting thread creation, retriving the running 25 Provides methods for intercepting thread creation, retrieving the running
27 threads and their name and state. 26 threads and their name and state.
28 """ 27 """
29 def __init__(self): 28 def __init__(self):
30 """ 29 """
31 Constructor 30 Constructor
32 """ 31 """
33 self.threadNumber = 1 32 self.threadNumber = 1
34 self.enableImportHooks = True
35 self._original_start_new_thread = None 33 self._original_start_new_thread = None
36 self.threadingAttached = False
37 self.qtThreadAttached = False
38 self.greenlet = False
39 34
40 self.clientLock = threading.RLock() 35 self.clientLock = threading.RLock()
41 36
42 # dictionary of all threads running {id: DebugBase} 37 # dictionary of all threads running {id: DebugBase}
43 self.threads = {_thread.get_ident(): self} 38 self.threads = {_thread.get_ident(): self}
47 # the thread we are at a breakpoint continuing at next command 42 # the thread we are at a breakpoint continuing at next command
48 self.currentThreadExec = self 43 self.currentThreadExec = self
49 44
50 # special objects representing the main scripts thread and frame 45 # special objects representing the main scripts thread and frame
51 self.mainThread = self 46 self.mainThread = self
52
53 # reset already imported thread module to apply hooks at next import
54 del sys.modules['_thread']
55 del sys.modules['threading']
56
57 sys.meta_path.insert(0, self)
58 47
59 def attachThread(self, target=None, args=None, kwargs=None, 48 def attachThread(self, target=None, args=None, kwargs=None,
60 mainThread=False): 49 mainThread=False):
61 """ 50 """
62 Public method to setup a standard thread for DebugClient to debug. 51 Public method to setup a standard thread for DebugClient to debug.
119 @param blocking flag to indicating a blocking lock 108 @param blocking flag to indicating a blocking lock
120 @type bool 109 @type bool
121 @return flag indicating successful locking 110 @return flag indicating successful locking
122 @rtype bool 111 @rtype bool
123 """ 112 """
124 if blocking: 113 return self.clientLock.acquire(blocking)
125 return self.clientLock.acquire()
126 else:
127 return self.clientLock.acquire(blocking)
128 114
129 def unlockClient(self): 115 def unlockClient(self):
130 """ 116 """
131 Public method to release the lock for this client. 117 Public method to release the lock for this client.
132 """ 118 """
133 try: 119 try:
134 self.clientLock.release() 120 self.clientLock.release()
135 except AssertionError: 121 except RuntimeError:
136 pass 122 pass
137 123
138 def setCurrentThread(self, threadId): 124 def setCurrentThread(self, threadId):
139 """ 125 """
140 Public method to set the current thread. 126 Public method to set the current thread.
154 def dumpThreadList(self): 140 def dumpThreadList(self):
155 """ 141 """
156 Public method to send the list of threads. 142 Public method to send the list of threads.
157 """ 143 """
158 self.updateThreadList() 144 self.updateThreadList()
145
159 threadList = [] 146 threadList = []
160 if len(self.threads) > 1: 147 currentId = _thread.get_ident()
161 currentId = _thread.get_ident() 148 # update thread names set by user (threading.setName)
162 # update thread names set by user (threading.setName) 149 threadNames = {t.ident: t.getName() for t in threading.enumerate()}
163 threadNames = {t.ident: t.getName() for t in threading.enumerate()} 150
164 151 for threadId, thd in self.threads.items():
165 for threadId, thd in self.threads.items(): 152 d = {"id": threadId}
166 d = {"id": threadId} 153 try:
167 try: 154 d["name"] = threadNames.get(threadId, thd.name)
168 d["name"] = threadNames.get(threadId, thd.name) 155 d["broken"] = thd.isBroken
169 d["broken"] = thd.isBroken 156 d["except"] = thd.isException
170 except Exception: 157 except Exception:
171 d["name"] = 'UnknownThread' 158 d["name"] = 'UnknownThread'
172 d["broken"] = False 159 d["broken"] = False
173 160 d["except"] = False
174 threadList.append(d) 161
175 else:
176 currentId = -1
177 d = {"id": -1}
178 d["name"] = "MainThread"
179 d["broken"] = self.isBroken
180 threadList.append(d) 162 threadList.append(d)
181 163
182 self.sendJsonCommand("ResponseThreadList", { 164 self.sendJsonCommand("ResponseThreadList", {
183 "currentID": currentId, 165 "currentID": currentId,
184 "threadList": threadList, 166 "threadList": threadList,
235 217
236 # Clean up obsolet because terminated threads 218 # Clean up obsolet because terminated threads
237 self.threads = {id_: thrd for id_, thrd in self.threads.items() 219 self.threads = {id_: thrd for id_, thrd in self.threads.items()
238 if id_ in frames} 220 if id_ in frames}
239 221
240 def find_module(self, fullname, path=None): 222 #######################################################################
241 """ 223 ## Methods below deal with patching various modules to support
242 Public method returning the module loader. 224 ## debugging of threads.
243 225 #######################################################################
244 @param fullname name of the module to be loaded 226
245 @type str 227 def patchPyThread(self, module):
246 @param path path to resolve the module name 228 """
247 @type str 229 Public method to patch Python _thread (Python3) and thread (Python2)
248 @return module loader object 230 modules.
249 @rtype object 231
250 """ 232 @param module reference to the imported module to be patched
251 if fullname in sys.modules or not self.debugging: 233 @type module
252 return None 234 """
253 235 # make thread hooks available to system
254 if fullname in ['_thread', 'PyQt5.QtCore', 'PySide2.QtCore', 236 self._original_start_new_thread = module.start_new_thread
255 'greenlet', 'threading' 237 module.start_new_thread = self.attachThread
256 ] and self.enableImportHooks: 238
257 # Disable hook to be able to import original module 239 def patchGreenlet(self, module):
258 self.enableImportHooks = False 240 """
259 return self 241 Public method to patch the 'greenlet' module.
260 242
261 return None 243 @param module reference to the imported module to be patched
262 244 @type module
263 def load_module(self, fullname): 245 @return flag indicating that the module was processed
264 """ 246 @rtype bool
265 Public method to load a module. 247 """
266 248 # Check for greenlet.settrace
267 @param fullname name of the module to be loaded 249 if hasattr(module, 'settrace'):
268 @type str 250 DebugBase.pollTimerEnabled = False
269 @return reference to the loaded module 251 return True
270 @rtype module 252 return False
271 """ 253
272 module = importlib.import_module(fullname) 254 def patchPyThreading(self, module):
273 sys.modules[fullname] = module 255 """
274 if (fullname == '_thread' and 256 Public method to patch the Python threading module.
275 self._original_start_new_thread is None): 257
276 # make thread hooks available to system 258 @param module reference to the imported module to be patched
277 self._original_start_new_thread = module.start_new_thread 259 @type module
278 module.start_new_thread = self.attachThread 260 """
279 261 # _debugClient as a class attribute can't be accessed in following
280 elif (fullname == 'greenlet' and self.greenlet is False): 262 # class. Therefore we need a global variable.
281 # Check for greenlet.settrace 263 _debugClient = self
282 if hasattr(module, 'settrace'): 264
283 self.greenlet = True 265 def _bootstrap(self, run):
284 DebugBase.pollTimerEnabled = False 266 """
285 267 Bootstrap for threading, which reports exceptions correctly.
286 # Add hook for threading.run() 268
287 elif (fullname == "threading" and self.threadingAttached is False): 269 @param run the run method of threading.Thread
288 self.threadingAttached = True 270 @type method pointer
289 271 """
290 # _debugClient as a class attribute can't be accessed in following 272 newThread = DebugBase(_debugClient)
291 # class. Therefore we need a global variable. 273 newThread.name = self.name
292 _debugClient = self 274
293 275 _debugClient.threads[self.ident] = newThread
294 def _bootstrap(self, run): 276 _debugClient.dumpThreadList()
295 """ 277
296 Bootstrap for threading, which reports exceptions correctly. 278 # see DebugBase.bootstrap
279 sys.settrace(newThread.trace_dispatch)
280 try:
281 run()
282 except Exception:
283 excinfo = sys.exc_info()
284 newThread.user_exception(excinfo, True)
285 finally:
286 sys.settrace(None)
287 _debugClient.dumpThreadList()
288
289 class ThreadWrapper(module.Thread):
290 """
291 Wrapper class for threading.Thread.
292 """
293 def __init__(self, *args, **kwargs):
294 """
295 Constructor
296 """
297 # Overwrite the provided run method with our own, to
298 # intercept the thread creation by threading.Thread
299 self.run = lambda s=self, run=self.run: _bootstrap(s, run)
297 300
298 @param run the run method of threading.Thread 301 super(ThreadWrapper, self).__init__(*args, **kwargs)
299 @type method pointer 302
300 """ 303 module.Thread = ThreadWrapper
301 newThread = DebugBase(_debugClient) 304
302 _debugClient.threads[self.ident] = newThread 305 # Special handling of threading.(_)Timer
303 newThread.name = self.name 306 timer = module.Timer
304 # see DebugBase.bootstrap 307
305 sys.settrace(newThread.trace_dispatch) 308 class TimerWrapper(timer, ThreadWrapper):
306 try: 309 """
307 run() 310 Wrapper class for threading.(_)Timer.
308 except Exception: 311 """
309 excinfo = sys.exc_info() 312 def __init__(self, interval, function, *args, **kwargs):
310 newThread.user_exception(excinfo, True) 313 """
311 finally: 314 Constructor
312 sys.settrace(None) 315 """
313 316 super(TimerWrapper, self).__init__(
314 class ThreadWrapper(module.Thread): 317 interval, function, *args, **kwargs)
315 """ 318
316 Wrapper class for threading.Thread. 319 module.Timer = TimerWrapper
317 """ 320
318 def __init__(self, *args, **kwargs): 321 # Special handling of threading._DummyThread
319 """ 322 class DummyThreadWrapper(module._DummyThread, ThreadWrapper):
320 Constructor 323 """
321 """ 324 Wrapper class for threading._DummyThread.
322 # Overwrite the provided run method with our own, to 325 """
323 # intercept the thread creation by threading.Thread 326 def __init__(self, *args, **kwargs):
324 self.run = lambda s=self, run=self.run: _bootstrap(s, run) 327 """
325 328 Constructor
326 super(ThreadWrapper, self).__init__(*args, **kwargs) 329 """
327 330 super(DummyThreadWrapper, self).__init__(*args, **kwargs)
328 module.Thread = ThreadWrapper 331
329 332 module._DummyThread = DummyThreadWrapper
330 # Special handling of threading.(_)Timer 333
331 timer = module.Timer 334 def patchQThread(self, module):
335 """
336 Public method to patch the QtCore module's QThread.
337
338 @param module reference to the imported module to be patched
339 @type module
340 """
341 # _debugClient as a class attribute can't be accessed in following
342 # class. Therefore we need a global variable.
343 _debugClient = self
344
345 def _bootstrapQThread(self, run):
346 """
347 Bootstrap for QThread, which reports exceptions correctly.
348
349 @param run the run method of *.QThread
350 @type method pointer
351 """
352 global _qtThreadNumber
353
354 newThread = DebugBase(_debugClient)
355 ident = _thread.get_ident()
356 name = 'QtThread-{0}'.format(_qtThreadNumber)
357
358 _qtThreadNumber += 1
359
360 newThread.id = ident
361 newThread.name = name
362
363 _debugClient.threads[ident] = newThread
364 _debugClient.dumpThreadList()
365
366 # see DebugBase.bootstrap
367 sys.settrace(newThread.trace_dispatch)
368 try:
369 run()
370 except SystemExit:
371 # *.QThreads doesn't like SystemExit
372 pass
373 except Exception:
374 excinfo = sys.exc_info()
375 newThread.user_exception(excinfo, True)
376 finally:
377 sys.settrace(None)
378 _debugClient.dumpThreadList()
379
380 class QThreadWrapper(module.QThread):
381 """
382 Wrapper class for *.QThread.
383 """
384 def __init__(self, *args, **kwargs):
385 """
386 Constructor
387 """
388 # Overwrite the provided run method with our own, to
389 # intercept the thread creation by Qt
390 self.run = lambda s=self, run=self.run: (
391 _bootstrapQThread(s, run))
332 392
333 class TimerWrapper(timer, ThreadWrapper): 393 super(QThreadWrapper, self).__init__(*args, **kwargs)
334 """ 394
335 Wrapper class for threading.(_)Timer. 395 class QRunnableWrapper(module.QRunnable):
336 """ 396 """
337 def __init__(self, interval, function, *args, **kwargs): 397 Wrapper class for *.QRunnable.
338 """ 398 """
339 Constructor 399 def __init__(self, *args, **kwargs):
340 """ 400 """
341 super(TimerWrapper, self).__init__( 401 Constructor
342 interval, function, *args, **kwargs) 402 """
343 403 # Overwrite the provided run method with our own, to
344 module.Timer = TimerWrapper 404 # intercept the thread creation by Qt
345 405 self.run = lambda s=self, run=self.run: (
346 # Special handling of threading._DummyThread 406 _bootstrapQThread(s, run))
347 class DummyThreadWrapper(module._DummyThread, ThreadWrapper):
348 """
349 Wrapper class for threading._DummyThread.
350 """
351 def __init__(self, *args, **kwargs):
352 """
353 Constructor
354 """
355 super(DummyThreadWrapper, self).__init__(*args, **kwargs)
356
357 module._DummyThread = DummyThreadWrapper
358
359 # Add hook for *.QThread
360 elif (fullname in ['PyQt5.QtCore', 'PySide2.QtCore'] and
361 self.qtThreadAttached is False):
362 self.qtThreadAttached = True
363 # _debugClient as a class attribute can't be accessed in following
364 # class. Therefore we need a global variable.
365 _debugClient = self
366
367 def _bootstrapQThread(self, run):
368 """
369 Bootstrap for QThread, which reports exceptions correctly.
370 407
371 @param run the run method of *.QThread 408 super(QRunnableWrapper, self).__init__(*args, **kwargs)
372 @type method pointer 409
373 """ 410 module.QThread = QThreadWrapper
374 global _qtThreadNumber 411 module.QRunnable = QRunnableWrapper
375
376 newThread = DebugBase(_debugClient)
377 ident = _thread.get_ident()
378 name = 'QtThread-{0}'.format(_qtThreadNumber)
379
380 _qtThreadNumber += 1
381
382 newThread.id = ident
383 newThread.name = name
384
385 _debugClient.threads[ident] = newThread
386
387 # see DebugBase.bootstrap
388 sys.settrace(newThread.trace_dispatch)
389 try:
390 run()
391 except SystemExit:
392 # *.QThreads doesn't like SystemExit
393 pass
394 except Exception:
395 excinfo = sys.exc_info()
396 newThread.user_exception(excinfo, True)
397 finally:
398 sys.settrace(None)
399
400 class QThreadWrapper(module.QThread):
401 """
402 Wrapper class for *.QThread.
403 """
404 def __init__(self, *args, **kwargs):
405 """
406 Constructor
407 """
408 # Overwrite the provided run method with our own, to
409 # intercept the thread creation by Qt
410 self.run = lambda s=self, run=self.run: (
411 _bootstrapQThread(s, run))
412
413 super(QThreadWrapper, self).__init__(*args, **kwargs)
414
415 class QRunnableWrapper(module.QRunnable):
416 """
417 Wrapper class for *.QRunnable.
418 """
419 def __init__(self, *args, **kwargs):
420 """
421 Constructor
422 """
423 # Overwrite the provided run method with our own, to
424 # intercept the thread creation by Qt
425 self.run = lambda s=self, run=self.run: (
426 _bootstrapQThread(s, run))
427
428 super(QRunnableWrapper, self).__init__(*args, **kwargs)
429
430 module.QThread = QThreadWrapper
431 module.QRunnable = QRunnableWrapper
432
433 self.enableImportHooks = True
434 return module

eric ide

mercurial