eric6/DebugClients/Python/ThreadExtension.py

changeset 6942
2602857055c5
parent 6891
93f82da09f22
child 6989
8b8cadf8d7e9
child 7360
9190402e4505
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/DebugClients/Python/ThreadExtension.py	Sun Apr 14 15:09:21 2019 +0200
@@ -0,0 +1,424 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2014 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing an import hook patching thread modules to get debugged too.
+"""
+
+import os.path
+import sys
+import importlib
+
+if sys.version_info[0] == 2:
+    import thread as _thread
+else:
+    import _thread
+
+import threading
+
+from DebugBase import DebugBase
+
+_qtThreadNumber = 1
+
+
+class ThreadExtension(object):
+    """
+    Class implementing the thread support for the debugger.
+    
+    Provides methods for intercepting thread creation, retriving the running
+    threads and their name and state.
+    """
+    def __init__(self):
+        """
+        Constructor
+        """
+        self.threadNumber = 1
+        self.enableImportHooks = True
+        self._original_start_new_thread = None
+        self.threadingAttached = False
+        self.qtThreadAttached = False
+        self.greenlet = False
+        
+        self.clientLock = threading.RLock()
+        
+        # dictionary of all threads running {id: DebugBase}
+        self.threads = {_thread.get_ident(): self}
+
+        # the "current" thread, basically for variables view
+        self.currentThread = self
+        # the thread we are at a breakpoint continuing at next command
+        self.currentThreadExec = self
+        
+        # special objects representing the main scripts thread and frame
+        self.mainThread = self
+        
+        if sys.version_info[0] == 2:
+            self.threadModName = 'thread'
+        else:
+            self.threadModName = '_thread'
+        
+        # reset already imported thread module to apply hooks at next import
+        del sys.modules[self.threadModName]
+        del sys.modules['threading']
+        
+        sys.meta_path.insert(0, self)
+
+    def attachThread(self, target=None, args=None, kwargs=None,
+                     mainThread=False):
+        """
+        Public method to setup a standard thread for DebugClient to debug.
+        
+        If mainThread is True, then we are attaching to the already
+        started mainthread of the app and the rest of the args are ignored.
+        
+        @param target the start function of the target thread (i.e. the user
+            code)
+        @param args arguments to pass to target
+        @param kwargs keyword arguments to pass to target
+        @param mainThread True, if we are attaching to the already
+              started mainthread of the app
+        @return identifier of the created thread
+        """
+        if kwargs is None:
+            kwargs = {}
+        
+        if mainThread:
+            ident = _thread.get_ident()
+            name = 'MainThread'
+            newThread = self.mainThread
+            newThread.isMainThread = True
+            if self.debugging:
+                sys.setprofile(newThread.profile)
+            
+        else:
+            newThread = DebugBase(self)
+            ident = self._original_start_new_thread(
+                newThread.bootstrap, (target, args, kwargs))
+            name = 'Thread-{0}'.format(self.threadNumber)
+            self.threadNumber += 1
+        
+        newThread.id = ident
+        newThread.name = name
+        
+        self.threads[ident] = newThread
+
+        return ident
+    
+    def threadTerminated(self, threadId):
+        """
+        Public method called when a DebugThread has exited.
+        
+        @param threadId id of the DebugThread that has exited
+        @type int
+        """
+        self.lockClient()
+        try:
+            del self.threads[threadId]
+        except KeyError:
+            pass
+        finally:
+            self.unlockClient()
+    
+    def lockClient(self, blocking=True):
+        """
+        Public method to acquire the lock for this client.
+        
+        @param blocking flag to indicating a blocking lock
+        @type bool
+        @return flag indicating successful locking
+        @rtype bool
+        """
+        if blocking:
+            return self.clientLock.acquire()
+        else:
+            return self.clientLock.acquire(blocking)
+    
+    def unlockClient(self):
+        """
+        Public method to release the lock for this client.
+        """
+        try:
+            self.clientLock.release()
+        except AssertionError:
+            pass
+    
+    def setCurrentThread(self, threadId):
+        """
+        Public method to set the current thread.
+
+        @param threadId the id the current thread should be set to.
+        @type int
+        """
+        try:
+            self.lockClient()
+            if threadId is None:
+                self.currentThread = None
+            else:
+                self.currentThread = self.threads.get(threadId)
+        finally:
+            self.unlockClient()
+    
+    def dumpThreadList(self):
+        """
+        Public method to send the list of threads.
+        """
+        self.updateThreadList()
+        threadList = []
+        if len(self.threads) > 1:
+            currentId = _thread.get_ident()
+            # update thread names set by user (threading.setName)
+            threadNames = {t.ident: t.getName() for t in threading.enumerate()}
+            
+            for threadId, thd in self.threads.items():
+                d = {"id": threadId}
+                try:
+                    d["name"] = threadNames.get(threadId, thd.name)
+                    d["broken"] = thd.isBroken
+                except Exception:
+                    d["name"] = 'UnknownThread'
+                    d["broken"] = False
+                
+                threadList.append(d)
+        else:
+            currentId = -1
+            d = {"id": -1}
+            d["name"] = "MainThread"
+            d["broken"] = self.isBroken
+            threadList.append(d)
+        
+        self.sendJsonCommand("ResponseThreadList", {
+            "currentID": currentId,
+            "threadList": threadList,
+        })
+    
+    def getExecutedFrame(self, frame):
+        """
+        Public method to return the currently executed frame.
+        
+        @param frame the current frame
+        @type frame object
+        @return the frame which is excecuted (without debugger frames)
+        @rtype frame object
+        """
+        # to get the currently executed frame, skip all frames belonging to the
+        # debugger
+        while frame is not None:
+            baseName = os.path.basename(frame.f_code.co_filename)
+            if not baseName.startswith(
+                    ('DebugClientBase.py', 'DebugBase.py', 'AsyncFile.py',
+                     'ThreadExtension.py')):
+                break
+            frame = frame.f_back
+        
+        return frame
+    
+    def updateThreadList(self):
+        """
+        Public method to update the list of running threads.
+        """
+        frames = sys._current_frames()
+        for threadId, frame in frames.items():
+            # skip our own timer thread
+            if frame.f_code.co_name == '__eventPollTimer':
+                continue
+            
+            # Unknown thread
+            if threadId not in self.threads:
+                newThread = DebugBase(self)
+                name = 'Thread-{0}'.format(self.threadNumber)
+                self.threadNumber += 1
+                
+                newThread.id = threadId
+                newThread.name = name
+                self.threads[threadId] = newThread
+            
+            # adjust current frame
+            if "__pypy__" not in sys.builtin_module_names:
+                # Don't update with None
+                currentFrame = self.getExecutedFrame(frame)
+                if (currentFrame is not None and
+                        self.threads[threadId].isBroken is False):
+                    self.threads[threadId].currentFrame = currentFrame
+        
+        # Clean up obsolet because terminated threads
+        self.threads = {id_: thrd for id_, thrd in self.threads.items()
+                        if id_ in frames}
+    
+    def find_module(self, fullname, path=None):
+        """
+        Public method returning the module loader.
+        
+        @param fullname name of the module to be loaded
+        @type str
+        @param path path to resolve the module name
+        @type str
+        @return module loader object
+        @rtype object
+        """
+        if fullname in sys.modules or not self.debugging:
+            return None
+        
+        if fullname in [self.threadModName, 'PyQt4.QtCore', 'PyQt5.QtCore',
+                        'PySide.QtCore', 'PySide2.QtCore', 'greenlet',
+                        'threading'] and self.enableImportHooks:
+            # Disable hook to be able to import original module
+            self.enableImportHooks = False
+            return self
+        
+        return None
+    
+    def load_module(self, fullname):
+        """
+        Public method to load a module.
+        
+        @param fullname name of the module to be loaded
+        @type str
+        @return reference to the loaded module
+        @rtype module
+        """
+        module = importlib.import_module(fullname)
+        sys.modules[fullname] = module
+        if (fullname == self.threadModName and
+                self._original_start_new_thread is None):
+            # make thread hooks available to system
+            self._original_start_new_thread = module.start_new_thread
+            module.start_new_thread = self.attachThread
+
+        elif (fullname == 'greenlet' and self.greenlet is False):
+            # Check for greenlet.settrace
+            if hasattr(module, 'settrace'):
+                self.greenlet = True
+                DebugBase.pollTimerEnabled = False
+        
+        # Add hook for threading.run()
+        elif (fullname == "threading" and self.threadingAttached is False):
+            self.threadingAttached = True
+            
+            # _debugClient as a class attribute can't be accessed in following
+            # class. Therefore we need a global variable.
+            _debugClient = self
+            
+            def _bootstrap(self, run):
+                """
+                Bootstrap for threading, which reports exceptions correctly.
+                
+                @param run the run method of threading.Thread
+                @type method pointer
+                """
+                newThread = DebugBase(_debugClient)
+                _debugClient.threads[self.ident] = newThread
+                newThread.name = self.name
+                # see DebugBase.bootstrap
+                sys.settrace(newThread.trace_dispatch)
+                try:
+                    run()
+                except Exception:
+                    excinfo = sys.exc_info()
+                    newThread.user_exception(excinfo, True)
+                finally:
+                    sys.settrace(None)
+            
+            class ThreadWrapper(module.Thread):
+                """
+                Wrapper class for threading.Thread.
+                """
+                def __init__(self, *args, **kwargs):
+                    """
+                    Constructor
+                    """
+                    # Overwrite the provided run method with our own, to
+                    # intercept the thread creation by threading.Thread
+                    self.run = lambda s=self, run=self.run: _bootstrap(s, run)
+                    
+                    super(ThreadWrapper, self).__init__(*args, **kwargs)
+            
+            module.Thread = ThreadWrapper
+            
+            # Special handling of threading.(_)Timer
+            if sys.version_info[0] == 2:
+                timer = module._Timer
+            else:
+                timer = module.Timer
+                
+            class TimerWrapper(timer, ThreadWrapper):
+                """
+                Wrapper class for threading.(_)Timer.
+                """
+                def __init__(self, interval, function, *args, **kwargs):
+                    """
+                    Constructor
+                    """
+                    super(TimerWrapper, self).__init__(
+                        interval, function, *args, **kwargs)
+            
+            if sys.version_info[0] == 2:
+                module._Timer = TimerWrapper
+            else:
+                module.Timer = TimerWrapper
+        
+        # Add hook for *.QThread
+        elif (fullname in ['PyQt4.QtCore', 'PyQt5.QtCore',
+                           'PySide.QtCore', 'PySide2.QtCore'] and
+                self.qtThreadAttached is False):
+            self.qtThreadAttached = True
+            # _debugClient as a class attribute can't be accessed in following
+            # class. Therefore we need a global variable.
+            _debugClient = self
+
+            def _bootstrapQThread(self, run):
+                """
+                Bootstrap for QThread, which reports exceptions correctly.
+                
+                @param run the run method of *.QThread
+                @type method pointer
+                """
+                global _qtThreadNumber
+                
+                newThread = DebugBase(_debugClient)
+                ident = _thread.get_ident()
+                name = 'QtThread-{0}'.format(_qtThreadNumber)
+                
+                _qtThreadNumber += 1
+            
+                newThread.id = ident
+                newThread.name = name
+                
+                _debugClient.threads[ident] = newThread
+                
+                # see DebugBase.bootstrap
+                sys.settrace(newThread.trace_dispatch)
+                try:
+                    run()
+                except SystemExit:
+                    # *.QThreads doesn't like SystemExit
+                    pass
+                except Exception:
+                    excinfo = sys.exc_info()
+                    newThread.user_exception(excinfo, True)
+                finally:
+                    sys.settrace(None)
+        
+            class QThreadWrapper(module.QThread):
+                """
+                Wrapper class for *.QThread.
+                """
+                def __init__(self, *args, **kwargs):
+                    """
+                    Constructor
+                    """
+                    # Overwrite the provided run method with our own, to
+                    # intercept the thread creation by Qt
+                    self.run = lambda s=self, run=self.run: (
+                        _bootstrapQThread(s, run))
+                    
+                    super(QThreadWrapper, self).__init__(*args, **kwargs)
+            
+            module.QThread = QThreadWrapper
+        
+        self.enableImportHooks = True
+        return module
+
+#
+# eflag: noqa = M702

eric ide

mercurial