eric7/DebugClients/Python/ThreadExtension.py

branch
eric7
changeset 8312
800c432b34c8
parent 8240
93b8a353c4bf
child 8334
c113428ecff3
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
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

eric ide

mercurial