|
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 |