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