eric6/DebugClients/Python/ThreadExtension.py

changeset 6942
2602857055c5
parent 6891
93f82da09f22
child 6989
8b8cadf8d7e9
child 7360
9190402e4505
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
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

eric ide

mercurial