src/eric7/DebugClients/Python/ThreadExtension.py

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

eric ide

mercurial