|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2014 - 2021 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 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 |