1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2014 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing an import hook patching thread modules to get debugged too.
8"""
9
10import os
11import sys
12import contextlib
13
14import _thread
15import threading
16
17from DebugBase import DebugBase
18
19_qtThreadNumber = 1
20
21
22class 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
410