eric6/Debugger/DebuggerInterfacePython.py

branch
maintenance
changeset 8043
0acf98cd089a
parent 7924
8a96736d465e
parent 7986
2971d5d19951
child 8142
43248bafe9b2
diff -r 866adc8c315b -r 0acf98cd089a eric6/Debugger/DebuggerInterfacePython.py
--- a/eric6/Debugger/DebuggerInterfacePython.py	Sun Jan 17 13:53:08 2021 +0100
+++ b/eric6/Debugger/DebuggerInterfacePython.py	Mon Feb 01 10:38:16 2021 +0100
@@ -14,7 +14,6 @@
 from PyQt5.QtCore import (
     QObject, QProcess, QProcessEnvironment, QTimer
 )
-from PyQt5.QtWidgets import QInputDialog
 
 from E5Gui.E5Application import e5App
 from E5Gui import E5MessageBox
@@ -48,14 +47,18 @@
         
         self.__isNetworked = True
         self.__autoContinue = False
+        self.__autoContinued = []
+        self.__isStepCommand = False
         
         self.debugServer = debugServer
         self.passive = passive
         self.process = None
         self.__startedVenv = ""
         
-        self.qsock = None
         self.queue = []
+        self.__master = None
+        self.__connections = {}
+        self.__pendingConnections = []
         
         # set default values for capabilities of clients
         self.clientCapabilities = ClientDefaultCapabilities
@@ -78,26 +81,32 @@
         
         # attribute to remember the name of the executed script
         self.__scriptName = ""
-
+    
     def __identityTranslation(self, fn, remote2local=True):
         """
         Private method to perform the identity path translation.
         
-        @param fn filename to be translated (string)
+        @param fn filename to be translated
+        @type str
         @param remote2local flag indicating the direction of translation
             (False = local to remote, True = remote to local [default])
-        @return translated filename (string)
+        @type bool
+        @return translated filename
+        @rtype str
         """
         return fn
-        
+    
     def __remoteTranslation(self, fn, remote2local=True):
         """
         Private method to perform the path translation.
         
-        @param fn filename to be translated (string)
+        @param fn filename to be translated
+        @type str
         @param remote2local flag indicating the direction of translation
             (False = local to remote, True = remote to local [default])
-        @return translated filename (string)
+        @type bool
+        @return translated filename
+        @rtype str
         """
         if remote2local:
             path = fn.replace(self.translateRemote, self.translateLocal)
@@ -109,7 +118,7 @@
                 path = path.replace("\\", "/")
         
         return path
-        
+    
     def __startProcess(self, program, arguments, environment=None,
                        workingDir=None):
         """
@@ -140,7 +149,7 @@
             proc = None
         
         return proc
-        
+    
     def startRemote(self, port, runInConsole, venvName, originalPathString,
                     workingDir=None):
         """
@@ -195,6 +204,10 @@
         redirect = str(Preferences.getDebugger("Python3Redirect"))
         noencoding = (Preferences.getDebugger("Python3NoEncoding") and
                       '--no-encoding' or '')
+        multiprocessEnabled = (
+            '--multiprocess' if Preferences.getDebugger("MultiProcessEnabled")
+            else ''
+        )
         
         if Preferences.getDebugger("RemoteDbgEnabled"):
             ipaddr = self.debugServer.getHostAddress(False)
@@ -204,8 +217,12 @@
                 rhost = "localhost"
             if rexec:
                 args = Utilities.parseOptionString(rexec) + [
-                    rhost, interpreter, debugClient, noencoding, str(port),
-                    redirect, ipaddr]
+                    rhost, interpreter, debugClient]
+                if noencoding:
+                    args.append(noencoding)
+                if multiprocessEnabled:
+                    args.append(multiprocessEnabled)
+                args.extend([str(port), redirect, ipaddr])
                 if Utilities.isWindowsPlatform():
                     if not os.path.splitext(args[0])[1]:
                         for ext in [".exe", ".com", ".cmd", ".bat"]:
@@ -270,8 +287,12 @@
             ccmd = Preferences.getDebugger("ConsoleDbgCommand")
             if ccmd:
                 args = Utilities.parseOptionString(ccmd) + [
-                    interpreter, os.path.abspath(debugClient), noencoding,
-                    str(port), '0', ipaddr]
+                    interpreter, os.path.abspath(debugClient)]
+                if noencoding:
+                    args.append(noencoding)
+                if multiprocessEnabled:
+                    args.append(multiprocessEnabled)
+                args.extend([str(port), '0', ipaddr])
                 args[0] = Utilities.getExecutablePath(args[0])
                 process = self.__startProcess(args[0], args[1:], clientEnv,
                                               workingDir=workingDir)
@@ -284,11 +305,14 @@
                             """ started.</p>"""))
                 return process, self.__isNetworked, interpreter
         
-        process = self.__startProcess(
-            interpreter,
-            [debugClient, noencoding, str(port), redirect, ipaddr],
-            clientEnv,
-            workingDir=workingDir)
+        args = [debugClient]
+        if noencoding:
+            args.append(noencoding)
+        if multiprocessEnabled:
+            args.append(multiprocessEnabled)
+        args.extend([str(port), redirect, ipaddr])
+        process = self.__startProcess(interpreter, args, clientEnv,
+                                      workingDir=workingDir)
         if process is None:
             self.__startedVenv = ""
             E5MessageBox.critical(
@@ -300,7 +324,7 @@
             self.__startedVenv = venvName
         
         return process, self.__isNetworked, interpreter
-
+    
     def startRemoteForProject(self, port, runInConsole, venvName,
                               originalPathString, workingDir=None):
         """
@@ -337,7 +361,12 @@
         
         redirect = str(project.getDebugProperty("REDIRECT"))
         noencoding = (
-            project.getDebugProperty("NOENCODING") and '--no-encoding' or '')
+            '--no-encoding' if project.getDebugProperty("NOENCODING") else ''
+        )
+        multiprocessEnabled = (
+            '--multiprocess' if Preferences.getDebugger("MultiProcessEnabled")
+            else ''
+        )
         
         venvManager = e5App().getObject("VirtualEnvManager")
         interpreter = venvManager.getVirtualenvInterpreter(venvName)
@@ -364,8 +393,12 @@
                 rhost = "localhost"
             if rexec:
                 args = Utilities.parseOptionString(rexec) + [
-                    rhost, interpreter, debugClient, noencoding, str(port),
-                    redirect, ipaddr]
+                    rhost, interpreter, debugClient]
+                if noencoding:
+                    args.append(noencoding)
+                if multiprocessEnabled:
+                    args.append(multiprocessEnabled)
+                args.extend([str(port), redirect, ipaddr])
                 if Utilities.isWindowsPlatform():
                     if not os.path.splitext(args[0])[1]:
                         for ext in [".exe", ".com", ".cmd", ".bat"]:
@@ -398,7 +431,7 @@
             else:
                 # remote shell command is missing
                 return None, self.__isNetworked, ""
-    
+        
         # set translation function
         self.translate = self.__identityTranslation
         
@@ -432,8 +465,12 @@
                     Preferences.getDebugger("ConsoleDbgCommand"))
             if ccmd:
                 args = Utilities.parseOptionString(ccmd) + [
-                    interpreter, os.path.abspath(debugClient), noencoding,
-                    str(port), '0', ipaddr]
+                    interpreter, os.path.abspath(debugClient)]
+                if noencoding:
+                    args.append(noencoding)
+                if multiprocessEnabled:
+                    args.append(multiprocessEnabled)
+                args.extend([str(port), '0', ipaddr])
                 args[0] = Utilities.getExecutablePath(args[0])
                 process = self.__startProcess(args[0], args[1:], clientEnv,
                                               workingDir=workingDir)
@@ -446,11 +483,14 @@
                             """ started.</p>"""))
                 return process, self.__isNetworked, interpreter
         
-        process = self.__startProcess(
-            interpreter,
-            [debugClient, noencoding, str(port), redirect, ipaddr],
-            clientEnv,
-            workingDir=workingDir)
+        args = [debugClient]
+        if noencoding:
+            args.append(noencoding)
+        if multiprocessEnabled:
+            args.append(multiprocessEnabled)
+        args.extend([str(port), redirect, ipaddr])
+        process = self.__startProcess(interpreter, args, clientEnv,
+                                      workingDir=workingDir)
         if process is None:
             self.__startedVenv = ""
             E5MessageBox.critical(
@@ -462,12 +502,13 @@
             self.__startedVenv = venvName
         
         return process, self.__isNetworked, interpreter
-
+    
     def getClientCapabilities(self):
         """
         Public method to retrieve the debug clients capabilities.
         
-        @return debug client capabilities (integer)
+        @return debug client capabilities
+        @rtype int
         """
         return self.clientCapabilities
     
@@ -475,91 +516,193 @@
         """
         Public slot to handle a new connection.
         
-        @param sock reference to the socket object (QTcpSocket)
-        @return flag indicating success (boolean)
+        @param sock reference to the socket object
+        @type QTcpSocket
+        @return flag indicating success
+        @rtype bool
         """
-        # If we already have a connection, refuse this one.  It will be closed
-        # automatically.
-        if self.qsock is not None:
-            return False
+        self.__pendingConnections.append(sock)
         
-        sock.disconnected.connect(self.debugServer.startClient)
-        sock.readyRead.connect(self.__parseClientLine)
+        sock.readyRead.connect(lambda: self.__parseClientLine(sock))
+        sock.disconnected.connect(lambda: self.__socketDisconnected(sock))
         
-        self.qsock = sock
-        
-        # Get the remote clients capabilities
-        self.remoteCapabilities()
         return True
     
-    def flush(self):
+    def __assignDebuggerId(self, sock, debuggerId):
+        """
+        Private method to set the debugger id for a recent debugger connection
+        attempt.
+        
+        @param sock reference to the socket object
+        @type QTcpSocket
+        @param debuggerId id of the connected debug client
+        @type str
+        """
+        if sock in self.__pendingConnections:
+            self.__connections[debuggerId] = sock
+            self.__pendingConnections.remove(sock)
+            
+            if self.__master is None:
+                self.__master = debuggerId
+                # Get the remote clients capabilities
+                self.remoteCapabilities(debuggerId)
+            
+            self.debugServer.signalClientDebuggerId(debuggerId)
+            
+            if debuggerId == self.__master:
+                self.__flush()
+                self.debugServer.masterClientConnected()
+            
+            self.debugServer.initializeClient(debuggerId)
+            
+            # perform auto-continue except for master
+            if (
+                debuggerId != self.__master and
+                self.__autoContinue and
+                not self.__isStepCommand
+            ):
+                QTimer.singleShot(
+                    0, lambda: self.remoteContinue(debuggerId))
+    
+    def __socketDisconnected(self, sock):
         """
-        Public slot to flush the queue.
+        Private slot handling a socket disconnecting.
+        
+        @param sock reference to the disconnected socket
+        @type QTcpSocket
         """
-        if self.qsock is not None:
+        for debuggerId in self.__connections:
+            if self.__connections[debuggerId] is sock:
+                del self.__connections[debuggerId]
+                if debuggerId == self.__master:
+                    self.__master = None
+                if debuggerId in self.__autoContinued:
+                    self.__autoContinued.remove(debuggerId)
+                self.debugServer.signalClientDisconnected(debuggerId)
+                break
+        else:
+            if sock in self.__pendingConnections:
+                self.__pendingConnections.remove(sock)
+        
+        if not self.__connections:
+            # no active connections anymore
+            try:
+                self.debugServer.signalLastClientExited()
+            except RuntimeError:
+                # debug server object might have been deleted already
+                # ignore this
+                pass
+            self.__autoContinued.clear()
+            self.debugServer.startClient()
+    
+    def getDebuggerIds(self):
+        """
+        Public method to return the IDs of the connected debugger backends.
+        
+        @return list of connected debugger backend IDs
+        @rtype list of str
+        """
+        return sorted(self.__connections.keys())
+    
+    def __flush(self):
+        """
+        Private slot to flush the queue.
+        """
+        if self.__master:
             # Send commands that were waiting for the connection.
             for cmd in self.queue:
-                self.__writeJsonCommandToSocket(cmd)
-            
-            self.queue = []
+                self.__writeJsonCommandToSocket(
+                    cmd, self.__connections[self.__master])
+        
+        self.queue = []
     
     def shutdown(self):
         """
         Public method to cleanly shut down.
         
-        It closes our socket and shuts down
-        the debug client. (Needed on Win OS)
+        It closes our sockets and shuts down the debug clients.
+        (Needed on Win OS)
         """
-        if self.qsock is None:
+        if not self.__master:
             return
         
-        # do not want any slots called during shutdown
-        self.qsock.disconnected.disconnect(self.debugServer.startClient)
-        self.qsock.readyRead.disconnect(self.__parseClientLine)
+        while self.__connections:
+            debuggerId, sock = self.__connections.popitem()
+            self.__shutdownSocket(sock)
         
-        # close down socket, and shut down client as well.
-        self.__sendJsonCommand("RequestShutdown", {})
-        self.qsock.flush()
-        self.qsock.close()
+        while self.__pendingConnections:
+            sock = self.__pendingConnections.pop()
+            self.__shutdownSocket(sock)
         
         # reinitialize
-        self.qsock = None
         self.queue = []
+        
+        self.__master = None
+    
+    def __shutdownSocket(self, sock):
+        """
+        Private slot to shut down a socket.
+        
+        @param sock reference to the socket
+        @type QTcpSocket
+        """
+        # do not want any slots called during shutdown
+        sock.readyRead.disconnect()
+        sock.disconnected.disconnect()
+
+        # close down socket, and shut down client as well.
+        self.__sendJsonCommand("RequestShutdown", {}, sock=sock)
+        sock.flush()
+        sock.close()
+        
+        sock.setParent(None)
+        sock.deleteLater()
+        del sock
     
     def isConnected(self):
         """
         Public method to test, if a debug client has connected.
         
-        @return flag indicating the connection status (boolean)
+        @return flag indicating the connection status
+        @rtype bool
         """
-        return self.qsock is not None
+        return bool(self.__connections)
     
     def remoteEnvironment(self, env):
         """
         Public method to set the environment for a program to debug, run, ...
         
-        @param env environment settings (dictionary)
+        @param env environment settings
+        @type dict
         """
-        self.__sendJsonCommand("RequestEnvironment", {"environment": env})
+        if self.__master:
+            self.__sendJsonCommand("RequestEnvironment", {"environment": env},
+                                   self.__master)
     
     def remoteLoad(self, fn, argv, wd, traceInterpreter=False,
-                   autoContinue=True, autoFork=False, forkChild=False):
+                   autoContinue=True, enableMultiprocess=False):
         """
         Public method to load a new program to debug.
         
-        @param fn the filename to debug (string)
-        @param argv the commandline arguments to pass to the program (string)
-        @param wd the working directory for the program (string)
-        @keyparam traceInterpreter flag indicating if the interpreter library
-            should be traced as well (boolean)
-        @keyparam autoContinue flag indicating, that the debugger should not
-            stop at the first executable line (boolean)
-        @keyparam autoFork flag indicating the automatic fork mode (boolean)
-        @keyparam forkChild flag indicating to debug the child after forking
-            (boolean)
+        @param fn the filename to debug
+        @type str
+        @param argv the commandline arguments to pass to the program
+        @type str
+        @param wd the working directory for the program
+        @type str
+        @param traceInterpreter flag indicating if the interpreter library
+            should be traced as well
+        @type bool
+        @param autoContinue flag indicating, that the debugger should not
+            stop at the first executable line
+        @type bool
+        @param enableMultiprocess flag indicating to perform multiprocess
+            debugging
+        @type bool
         """
         self.__autoContinue = autoContinue
         self.__scriptName = os.path.abspath(fn)
+        self.__isStepCommand = False
         
         wd = self.translate(wd, False)
         fn = self.translate(os.path.abspath(fn), False)
@@ -568,20 +711,19 @@
             "filename": fn,
             "argv": Utilities.parseOptionString(argv),
             "traceInterpreter": traceInterpreter,
-            "autofork": autoFork,
-            "forkChild": forkChild,
-        })
+            "multiprocess": enableMultiprocess,
+        }, self.__master)
     
-    def remoteRun(self, fn, argv, wd, autoFork=False, forkChild=False):
+    def remoteRun(self, fn, argv, wd):
         """
         Public method to load a new program to run.
         
-        @param fn the filename to run (string)
-        @param argv the commandline arguments to pass to the program (string)
-        @param wd the working directory for the program (string)
-        @keyparam autoFork flag indicating the automatic fork mode (boolean)
-        @keyparam forkChild flag indicating to debug the child after forking
-            (boolean)
+        @param fn the filename to run
+        @type str
+        @param argv the commandline arguments to pass to the program
+        @type str
+        @param wd the working directory for the program
+        @type str
         """
         self.__scriptName = os.path.abspath(fn)
         
@@ -591,19 +733,21 @@
             "workdir": wd,
             "filename": fn,
             "argv": Utilities.parseOptionString(argv),
-            "autofork": autoFork,
-            "forkChild": forkChild,
-        })
+        }, self.__master)
     
     def remoteCoverage(self, fn, argv, wd, erase=False):
         """
         Public method to load a new program to collect coverage data.
         
-        @param fn the filename to run (string)
-        @param argv the commandline arguments to pass to the program (string)
-        @param wd the working directory for the program (string)
-        @keyparam erase flag indicating that coverage info should be
-            cleared first (boolean)
+        @param fn the filename to run
+        @type str
+        @param argv the commandline arguments to pass to the program
+        @type str
+        @param wd the working directory for the program
+        @type str
+        @param erase flag indicating that coverage info should be
+            cleared first
+        @type bool
         """
         self.__scriptName = os.path.abspath(fn)
         
@@ -614,17 +758,21 @@
             "filename": fn,
             "argv": Utilities.parseOptionString(argv),
             "erase": erase,
-        })
-
+        }, self.__master)
+    
     def remoteProfile(self, fn, argv, wd, erase=False):
         """
         Public method to load a new program to collect profiling data.
         
-        @param fn the filename to run (string)
-        @param argv the commandline arguments to pass to the program (string)
-        @param wd the working directory for the program (string)
-        @keyparam erase flag indicating that timing info should be cleared
-            first (boolean)
+        @param fn the filename to run
+        @type str
+        @param argv the commandline arguments to pass to the program
+        @type str
+        @param wd the working directory for the program
+        @type str
+        @param erase flag indicating that timing info should be cleared
+            first
+        @type bool
         """
         self.__scriptName = os.path.abspath(fn)
         
@@ -635,189 +783,310 @@
             "filename": fn,
             "argv": Utilities.parseOptionString(argv),
             "erase": erase,
-        })
-
-    def remoteStatement(self, stmt):
+        }, self.__master)
+    
+    def remoteStatement(self, debuggerId, stmt):
         """
         Public method to execute a Python statement.
         
-        @param stmt the Python statement to execute (string). It
-              should not have a trailing newline.
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param stmt the Python statement to execute.
+        @type str
         """
         self.__sendJsonCommand("ExecuteStatement", {
             "statement": stmt,
-        })
-
-    def remoteStep(self):
+        }, debuggerId)
+    
+    def remoteStep(self, debuggerId):
         """
         Public method to single step the debugged program.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.__sendJsonCommand("RequestStep", {})
-
-    def remoteStepOver(self):
+        self.__isStepCommand = True
+        self.__sendJsonCommand("RequestStep", {}, debuggerId)
+    
+    def remoteStepOver(self, debuggerId):
         """
         Public method to step over the debugged program.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.__sendJsonCommand("RequestStepOver", {})
-
-    def remoteStepOut(self):
+        self.__isStepCommand = True
+        self.__sendJsonCommand("RequestStepOver", {}, debuggerId)
+    
+    def remoteStepOut(self, debuggerId):
         """
         Public method to step out the debugged program.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.__sendJsonCommand("RequestStepOut", {})
-
-    def remoteStepQuit(self):
+        self.__isStepCommand = True
+        self.__sendJsonCommand("RequestStepOut", {}, debuggerId)
+    
+    def remoteStepQuit(self, debuggerId):
         """
         Public method to stop the debugged program.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.__sendJsonCommand("RequestStepQuit", {})
-
-    def remoteContinue(self, special=False):
+        self.__isStepCommand = True
+        self.__sendJsonCommand("RequestStepQuit", {}, debuggerId)
+    
+    def remoteContinue(self, debuggerId, special=False):
         """
         Public method to continue the debugged program.
         
+        @param debuggerId ID of the debugger backend
+        @type str
         @param special flag indicating a special continue operation
+        @type bool
         """
+        self.__isStepCommand = False
         self.__sendJsonCommand("RequestContinue", {
             "special": special,
-        })
-
-    def remoteMoveIP(self, line):
+        }, debuggerId)
+    
+    def remoteContinueUntil(self, debuggerId, line):
+        """
+        Public method to continue the debugged program to the given line
+        or until returning from the current frame.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param line the new line, where execution should be continued to
+        @type int
+        """
+        self.__isStepCommand = False
+        self.__sendJsonCommand("RequestContinueUntil", {
+            "newLine": line,
+        }, debuggerId)
+    
+    def remoteMoveIP(self, debuggerId, line):
         """
         Public method to move the instruction pointer to a different line.
         
+        @param debuggerId ID of the debugger backend
+        @type str
         @param line the new line, where execution should be continued
+        @type int
         """
         self.__sendJsonCommand("RequestMoveIP", {
             "newLine": line,
-        })
-
-    def remoteBreakpoint(self, fn, line, setBreakpoint, cond=None, temp=False):
+        }, debuggerId)
+    
+    def remoteBreakpoint(self, debuggerId, fn, line, setBreakpoint, cond=None,
+                         temp=False):
         """
         Public method to set or clear a breakpoint.
         
-        @param fn filename the breakpoint belongs to (string)
-        @param line linenumber of the breakpoint (int)
-        @param setBreakpoint flag indicating setting or resetting a
-            breakpoint (boolean)
-        @param cond condition of the breakpoint (string)
-        @param temp flag indicating a temporary breakpoint (boolean)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param fn filename the breakpoint belongs to
+        @type str
+        @param line linenumber of the breakpoint
+        @type int
+        @param setBreakpoint flag indicating setting or resetting a breakpoint
+        @type bool
+        @param cond condition of the breakpoint
+        @type str
+        @param temp flag indicating a temporary breakpoint
+        @type bool
         """
-        self.__sendJsonCommand("RequestBreakpoint", {
-            "filename": self.translate(fn, False),
-            "line": line,
-            "temporary": temp,
-            "setBreakpoint": setBreakpoint,
-            "condition": cond,
-        })
+        if debuggerId:
+            debuggerList = [debuggerId]
+        else:
+            debuggerList = list(self.__connections.keys())
+        for debuggerId in debuggerList:
+            self.__sendJsonCommand("RequestBreakpoint", {
+                "filename": self.translate(fn, False),
+                "line": line,
+                "temporary": temp,
+                "setBreakpoint": setBreakpoint,
+                "condition": cond,
+            }, debuggerId)
     
-    def remoteBreakpointEnable(self, fn, line, enable):
+    def remoteBreakpointEnable(self, debuggerId, fn, line, enable):
         """
         Public method to enable or disable a breakpoint.
         
-        @param fn filename the breakpoint belongs to (string)
-        @param line linenumber of the breakpoint (int)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param fn filename the breakpoint belongs to
+        @type str
+        @param line linenumber of the breakpoint
+        @type int
         @param enable flag indicating enabling or disabling a breakpoint
-            (boolean)
+        @type bool
         """
-        self.__sendJsonCommand("RequestBreakpointEnable", {
-            "filename": self.translate(fn, False),
-            "line": line,
-            "enable": enable,
-        })
+        if debuggerId:
+            debuggerList = [debuggerId]
+        else:
+            debuggerList = list(self.__connections.keys())
+        for debuggerId in debuggerList:
+            self.__sendJsonCommand("RequestBreakpointEnable", {
+                "filename": self.translate(fn, False),
+                "line": line,
+                "enable": enable,
+            }, debuggerId)
     
-    def remoteBreakpointIgnore(self, fn, line, count):
+    def remoteBreakpointIgnore(self, debuggerId, fn, line, count):
         """
         Public method to ignore a breakpoint the next couple of occurrences.
         
-        @param fn filename the breakpoint belongs to (string)
-        @param line linenumber of the breakpoint (int)
-        @param count number of occurrences to ignore (int)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param fn filename the breakpoint belongs to
+        @type str
+        @param line linenumber of the breakpoint
+        @type int
+        @param count number of occurrences to ignore
+        @type int
         """
-        self.__sendJsonCommand("RequestBreakpointIgnore", {
-            "filename": self.translate(fn, False),
-            "line": line,
-            "count": count,
-        })
+        if debuggerId:
+            debuggerList = [debuggerId]
+        else:
+            debuggerList = list(self.__connections.keys())
+        for debuggerId in debuggerList:
+            self.__sendJsonCommand("RequestBreakpointIgnore", {
+                "filename": self.translate(fn, False),
+                "line": line,
+                "count": count,
+            }, debuggerId)
     
-    def remoteWatchpoint(self, cond, setWatch, temp=False):
+    def remoteWatchpoint(self, debuggerId, cond, setWatch, temp=False):
         """
         Public method to set or clear a watch expression.
         
-        @param cond expression of the watch expression (string)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param cond expression of the watch expression
+        @type str
         @param setWatch flag indicating setting or resetting a watch expression
-            (boolean)
-        @param temp flag indicating a temporary watch expression (boolean)
+        @type bool
+        @param temp flag indicating a temporary watch expression
+        @type bool
         """
-        # cond is combination of cond and special (s. watch expression viewer)
-        self.__sendJsonCommand("RequestWatch", {
-            "temporary": temp,
-            "setWatch": setWatch,
-            "condition": cond,
-        })
+        if debuggerId:
+            debuggerList = [debuggerId]
+        else:
+            debuggerList = list(self.__connections.keys())
+        for debuggerId in debuggerList:
+            # cond is combination of cond and special (s. watch expression
+            # viewer)
+            self.__sendJsonCommand("RequestWatch", {
+                "temporary": temp,
+                "setWatch": setWatch,
+                "condition": cond,
+            }, debuggerId)
     
-    def remoteWatchpointEnable(self, cond, enable):
+    def remoteWatchpointEnable(self, debuggerId, cond, enable):
         """
         Public method to enable or disable a watch expression.
         
-        @param cond expression of the watch expression (string)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param cond expression of the watch expression
+        @type str
         @param enable flag indicating enabling or disabling a watch expression
-            (boolean)
+        @type bool
         """
-        # cond is combination of cond and special (s. watch expression viewer)
-        self.__sendJsonCommand("RequestWatchEnable", {
-            "condition": cond,
-            "enable": enable,
-        })
+        if debuggerId:
+            debuggerList = [debuggerId]
+        else:
+            debuggerList = list(self.__connections.keys())
+        for debuggerId in debuggerList:
+            # cond is combination of cond and special (s. watch expression
+            # viewer)
+            self.__sendJsonCommand("RequestWatchEnable", {
+                "condition": cond,
+                "enable": enable,
+            }, debuggerId)
     
-    def remoteWatchpointIgnore(self, cond, count):
+    def remoteWatchpointIgnore(self, debuggerId, cond, count):
         """
         Public method to ignore a watch expression the next couple of
         occurrences.
         
-        @param cond expression of the watch expression (string)
-        @param count number of occurrences to ignore (int)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param cond expression of the watch expression
+        @type str
+        @param count number of occurrences to ignore
+        @type int
         """
-        # cond is combination of cond and special (s. watch expression viewer)
-        self.__sendJsonCommand("RequestWatchIgnore", {
-            "condition": cond,
-            "count": count,
-        })
+        if debuggerId:
+            debuggerList = [debuggerId]
+        else:
+            debuggerList = list(self.__connections.keys())
+        for debuggerId in debuggerList:
+            # cond is combination of cond and special (s. watch expression
+            # viewer)
+            self.__sendJsonCommand("RequestWatchIgnore", {
+                "condition": cond,
+                "count": count,
+            }, debuggerId)
     
-    def remoteRawInput(self, s):
+    def remoteRawInput(self, debuggerId, inputString):
         """
         Public method to send the raw input to the debugged program.
         
-        @param s the raw input (string)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param inputString the raw input
+        @type str
         """
         self.__sendJsonCommand("RawInput", {
-            "input": s,
-        })
+            "input": inputString,
+        }, debuggerId)
     
-    def remoteThreadList(self):
+    def remoteThreadList(self, debuggerId):
         """
         Public method to request the list of threads from the client.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.__sendJsonCommand("RequestThreadList", {})
-        
-    def remoteSetThread(self, tid):
+        self.__sendJsonCommand("RequestThreadList", {}, debuggerId)
+    
+    def remoteSetThread(self, debuggerId, tid):
         """
         Public method to request to set the given thread as current thread.
         
-        @param tid id of the thread (integer)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param tid id of the thread
+        @type int
         """
         self.__sendJsonCommand("RequestThreadSet", {
             "threadID": tid,
-        })
+        }, debuggerId)
+    
+    def remoteClientStack(self, debuggerId):
+        """
+        Public method to request the stack of the main thread.
         
-    def remoteClientVariables(self, scope, filterList, framenr=0, maxSize=0):
+        @param debuggerId ID of the debugger backend
+        @type str
+        """
+        self.__sendJsonCommand("RequestStack", {}, debuggerId)
+    
+    def remoteClientVariables(self, debuggerId, scope, filterList, framenr=0,
+                              maxSize=0):
         """
         Public method to request the variables of the debugged program.
         
+        @param debuggerId ID of the debugger backend
+        @type str
         @param scope the scope of the variables (0 = local, 1 = global)
         @type int
         @param filterList list of variable types to filter out
-        @type list of int
+        @type list of str
         @param framenr framenumber of the variables to retrieve
         @type int
         @param maxSize maximum size the formatted value of a variable will
@@ -830,17 +1099,19 @@
             "scope": scope,
             "filters": filterList,
             "maxSize": maxSize,
-        })
+        }, debuggerId)
     
-    def remoteClientVariable(self, scope, filterList, var, framenr=0,
-                             maxSize=0):
+    def remoteClientVariable(self, debuggerId, scope, filterList, var,
+                             framenr=0, maxSize=0):
         """
         Public method to request the variables of the debugged program.
         
+        @param debuggerId ID of the debugger backend
+        @type str
         @param scope the scope of the variables (0 = local, 1 = global)
         @type int
         @param filterList list of variable types to filter out
-        @type list of int
+        @type list of str
         @param var list encoded name of variable to retrieve
         @type list of str
         @param framenr framenumber of the variables to retrieve
@@ -856,36 +1127,61 @@
             "scope": scope,
             "filters": filterList,
             "maxSize": maxSize,
-        })
+        }, debuggerId)
     
-    def remoteClientDisassembly(self):
+    def remoteClientDisassembly(self, debuggerId):
         """
         Public method to ask the client for the latest traceback disassembly.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.__sendJsonCommand("RequestDisassembly", {})
+        self.__sendJsonCommand("RequestDisassembly", {}, debuggerId)
     
-    def remoteClientSetFilter(self, scope, filterStr):
+    def remoteClientSetFilter(self, debuggerId, scope, filterStr):
         """
         Public method to set a variables filter list.
         
+        @param debuggerId ID of the debugger backend
+        @type str
         @param scope the scope of the variables (0 = local, 1 = global)
+        @type int
         @param filterStr regexp string for variable names to filter out
-            (string)
+        @type str
         """
         self.__sendJsonCommand("RequestSetFilter", {
             "scope": scope,
             "filter": filterStr,
-        })
+        }, debuggerId)
     
-    def setCallTraceEnabled(self, on):
+    def setCallTraceEnabled(self, debuggerId, on):
         """
         Public method to set the call trace state.
         
-        @param on flag indicating to enable the call trace function (boolean)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param on flag indicating to enable the call trace function
+        @type bool
         """
         self.__sendJsonCommand("RequestCallTrace", {
             "enable": on,
-        })
+        }, debuggerId)
+    
+    def remoteNoDebugList(self, debuggerId, noDebugList):
+        """
+        Public method to set a list of programs not to be debugged.
+        
+        The programs given in the list will not be run under the control
+        of the multi process debugger.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param noDebugList list of Python programs not to be debugged
+        @type list of str
+        """
+        self.__sendJsonCommand("RequestSetNoDebugList", {
+            "noDebug": noDebugList,
+        }, debuggerId)
     
     def remoteBanner(self):
         """
@@ -893,22 +1189,28 @@
         """
         self.__sendJsonCommand("RequestBanner", {})
     
-    def remoteCapabilities(self):
+    def remoteCapabilities(self, debuggerId):
         """
         Public slot to get the debug clients capabilities.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.__sendJsonCommand("RequestCapabilities", {})
+        self.__sendJsonCommand("RequestCapabilities", {}, debuggerId)
     
-    def remoteCompletion(self, text):
+    def remoteCompletion(self, debuggerId, text):
         """
         Public slot to get the a list of possible commandline completions
         from the remote client.
         
-        @param text the text to be completed (string)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param text the text to be completed
+        @type str
         """
         self.__sendJsonCommand("RequestCompletion", {
             "text": text,
-        })
+        }, debuggerId)
     
     def remoteUTDiscover(self, syspath, workdir, discoveryStart):
         """
@@ -1008,43 +1310,24 @@
         """
         self.__sendJsonCommand("RequestUTStop", {})
     
-    def __askForkTo(self):
-        """
-        Private method to ask the user which branch of a fork to follow.
-        """
-        selections = [self.tr("Parent Process"),
-                      self.tr("Child process")]
-        res, ok = QInputDialog.getItem(
-            None,
-            self.tr("Client forking"),
-            self.tr("Select the fork branch to follow."),
-            selections,
-            0, False)
-        if not ok or res == selections[0]:
-            self.__sendJsonCommand("ResponseForkTo", {
-                "target": "parent",
-            })
-        else:
-            self.__sendJsonCommand("ResponseForkTo", {
-                "target": "child",
-            })
-    
-    def __parseClientLine(self):
+    def __parseClientLine(self, sock):
         """
         Private method to handle data from the client.
+        
+        @param sock reference to the socket to read data from
+        @type QTcpSocket
         """
-        while self.qsock and self.qsock.canReadLine():
-            qs = self.qsock.readLine()
+        while sock and sock.canReadLine():
+            qs = sock.readLine()
             line = bytes(qs).decode(
                 encoding=Preferences.getSystem("StringEncoding"))
             
             logging.debug("<Debug-Server> %s", line)
-##            print("Server: ", line)          ##debug
+##            print("Server: ", line)    ## debug    # __IGNORE_WARNING_M891__
             
-            self.__handleJsonCommand(line)
-            continue
+            self.__handleJsonCommand(line, sock)
     
-    def __handleJsonCommand(self, jsonStr):
+    def __handleJsonCommand(self, jsonStr, sock):
         """
         Private method to handle a command or response serialized as a
         JSON string.
@@ -1052,6 +1335,8 @@
         @param jsonStr string containing the command or response received
             from the debug backend
         @type str
+        @param sock reference to the socket the data was received from
+        @type QTcpSocket
         """
         import json
         
@@ -1075,26 +1360,36 @@
         method = commandDict["method"]
         params = commandDict["params"]
         
-        if method == "ClientOutput":
-            self.debugServer.signalClientOutput(params["text"])
+        if method == "DebuggerId":
+            self.__assignDebuggerId(sock, params["debuggerId"])
+        
+        elif method == "ClientOutput":
+            self.debugServer.signalClientOutput(
+                params["text"], params["debuggerId"])
         
         elif method in ["ResponseLine", "ResponseStack"]:
-            # Check if obsolet thread was clicked
+            # Check if obsolete thread was clicked
             if params["stack"] == []:
                 # Request updated list
-                self.remoteThreadList()
+                self.remoteThreadList(params["debuggerId"])
                 return
             for s in params["stack"]:
                 s[0] = self.translate(s[0], True)
             cf = params["stack"][0]
-            if self.__autoContinue:
-                self.__autoContinue = False
-                QTimer.singleShot(0, self.remoteContinue)
+            if (
+                self.__autoContinue and
+                params["debuggerId"] not in self.__autoContinued
+            ):
+                self.__autoContinued.append(params["debuggerId"])
+                QTimer.singleShot(
+                    0, lambda: self.remoteContinue(params["debuggerId"]))
             else:
                 self.debugServer.signalClientLine(
-                    cf[0], int(cf[1]),
-                    method == "ResponseStack")
-                self.debugServer.signalClientStack(params["stack"])
+                    cf[0], int(cf[1]), params["debuggerId"],
+                    method == "ResponseStack", threadName=params["threadName"])
+                self.debugServer.signalClientStack(
+                    params["stack"], params["debuggerId"],
+                    threadName=params["threadName"])
         
         elif method == "CallTrace":
             isCall = params["event"].lower() == "c"
@@ -1105,112 +1400,125 @@
                 fromInfo["filename"], str(fromInfo["linenumber"]),
                 fromInfo["codename"],
                 toInfo["filename"], str(toInfo["linenumber"]),
-                toInfo["codename"])
+                toInfo["codename"],
+                params["debuggerId"])
         
         elif method == "ResponseVariables":
             self.debugServer.signalClientVariables(
-                params["scope"], params["variables"])
+                params["scope"], params["variables"], params["debuggerId"])
         
         elif method == "ResponseVariable":
             self.debugServer.signalClientVariable(
-                params["scope"], [params["variable"]] + params["variables"])
+                params["scope"], [params["variable"]] + params["variables"],
+                params["debuggerId"])
         
         elif method == "ResponseThreadList":
             self.debugServer.signalClientThreadList(
-                params["currentID"], params["threadList"])
+                params["currentID"], params["threadList"],
+                params["debuggerId"])
         
         elif method == "ResponseThreadSet":
-            self.debugServer.signalClientThreadSet()
+            self.debugServer.signalClientThreadSet(params["debuggerId"])
         
         elif method == "ResponseCapabilities":
             self.clientCapabilities = params["capabilities"]
-            self.debugServer.signalClientCapabilities(
-                params["capabilities"],
-                params["clientType"],
-                self.__startedVenv,
-            )
+            if params["debuggerId"] == self.__master:
+                # signal only for the master connection
+                self.debugServer.signalClientCapabilities(
+                    params["capabilities"],
+                    params["clientType"],
+                    self.__startedVenv,
+                )
         
         elif method == "ResponseBanner":
-            self.debugServer.signalClientBanner(
-                params["version"],
-                params["platform"],
-                params["dbgclient"],
-                self.__startedVenv,
-            )
+            if params["debuggerId"] == self.__master:
+                # signal only for the master connection
+                self.debugServer.signalClientBanner(
+                    params["version"],
+                    params["platform"],
+                    self.__startedVenv,
+                )
         
         elif method == "ResponseOK":
-            self.debugServer.signalClientStatement(False)
+            self.debugServer.signalClientStatement(False, params["debuggerId"])
         
         elif method == "ResponseContinue":
-            self.debugServer.signalClientStatement(True)
+            self.debugServer.signalClientStatement(True, params["debuggerId"])
         
         elif method == "RequestRaw":
             self.debugServer.signalClientRawInput(
-                params["prompt"], params["echo"])
+                params["prompt"], params["echo"], params["debuggerId"])
         
         elif method == "ResponseBPConditionError":
             fn = self.translate(params["filename"], True)
             self.debugServer.signalClientBreakConditionError(
-                fn, params["line"])
+                fn, params["line"], params["debuggerId"])
         
         elif method == "ResponseClearBreakpoint":
             fn = self.translate(params["filename"], True)
-            self.debugServer.signalClientClearBreak(fn, params["line"])
+            self.debugServer.signalClientClearBreak(
+                fn, params["line"], params["debuggerId"])
         
         elif method == "ResponseWatchConditionError":
             self.debugServer.signalClientWatchConditionError(
-                params["condition"])
+                params["condition"], params["debuggerId"])
         
         elif method == "ResponseClearWatch":
-            self.debugServer.signalClientClearWatch(params["condition"])
+            self.debugServer.signalClientClearWatch(
+                params["condition"], params["debuggerId"])
         
         elif method == "ResponseDisassembly":
-            self.debugServer.signalClientDisassembly(params["disassembly"])
+            self.debugServer.signalClientDisassembly(
+                params["disassembly"], params["debuggerId"])
         
         elif method == "ResponseException":
-            if params:
-                exctype = params["type"]
-                excmessage = params["message"]
-                stack = params["stack"]
-                if stack:
+            exctype = params["type"]
+            excmessage = params["message"]
+            stack = params["stack"]
+            if stack:
+                for stackEntry in stack:
+                    stackEntry[0] = self.translate(stackEntry[0], True)
+                if stack[0] and stack[0][0] == "<string>":
                     for stackEntry in stack:
-                        stackEntry[0] = self.translate(stackEntry[0], True)
-                    if stack[0] and stack[0][0] == "<string>":
-                        for stackEntry in stack:
-                            if stackEntry[0] == "<string>":
-                                stackEntry[0] = self.__scriptName
-                            else:
-                                break
-            else:
-                exctype = ''
-                excmessage = ''
-                stack = []
+                        if stackEntry[0] == "<string>":
+                            stackEntry[0] = self.__scriptName
+                        else:
+                            break
             
             self.debugServer.signalClientException(
-                exctype, excmessage, stack)
+                exctype, excmessage, stack, params["debuggerId"],
+                params["threadName"])
         
         elif method == "ResponseSyntax":
             self.debugServer.signalClientSyntaxError(
                 params["message"], self.translate(params["filename"], True),
-                params["linenumber"], params["characternumber"])
+                params["linenumber"], params["characternumber"],
+                params["debuggerId"], params["threadName"])
         
         elif method == "ResponseSignal":
             self.debugServer.signalClientSignal(
                 params["message"], self.translate(params["filename"], True),
-                params["linenumber"], params["function"], params["arguments"])
+                params["linenumber"], params["function"], params["arguments"],
+                params["debuggerId"])
         
         elif method == "ResponseExit":
             self.__scriptName = ""
             self.debugServer.signalClientExit(
-                params["status"], params["message"])
+                params["program"], params["status"], params["message"],
+                params["debuggerId"])
         
         elif method == "PassiveStartup":
             self.debugServer.passiveStartUp(
-                self.translate(params["filename"], True), params["exceptions"])
+                self.translate(params["filename"], True), params["exceptions"],
+                params["debuggerId"])
         
         elif method == "ResponseCompletion":
             self.debugServer.signalClientCompletionList(
-                params["completions"], params["text"])
+                params["completions"], params["text"], params["debuggerId"])
+        
+        ###################################################################
+        ## Unit test related stuff is not done with multi processing
+        ###################################################################
         
         elif method == "ResponseUTDiscover":
             self.debugServer.clientUtDiscovered(
@@ -1250,11 +1558,8 @@
         elif method == "ResponseUTTestSucceededUnexpected":
             self.debugServer.clientUtTestSucceededUnexpected(
                 params["testname"], params["id"])
-        
-        elif method == "RequestForkTo":
-            self.__askForkTo()
     
-    def __sendJsonCommand(self, command, params):
+    def __sendJsonCommand(self, command, params, debuggerId="", sock=None):
         """
         Private method to send a single command to the client.
         
@@ -1262,6 +1567,11 @@
         @type str
         @param params dictionary of named parameters for the command
         @type dict
+        @param debuggerId id of the debug client to send the command to
+        @type str
+        @param sock reference to the socket object to be used (only used if
+            debuggerId is not given)
+        @type QTcpSocket
         """
         import json
         
@@ -1271,22 +1581,29 @@
             "params": params,
         }
         cmd = json.dumps(commandDict) + '\n'
-        if self.qsock is not None:
-            self.__writeJsonCommandToSocket(cmd)
+        
+        if debuggerId and debuggerId in self.__connections:
+            sock = self.__connections[debuggerId]
+        elif sock is None and self.__master is not None:
+            sock = self.__connections[self.__master]
+        if sock is not None:
+            self.__writeJsonCommandToSocket(cmd, sock)
         else:
             self.queue.append(cmd)
     
-    def __writeJsonCommandToSocket(self, cmd):
+    def __writeJsonCommandToSocket(self, cmd, sock):
         """
         Private method to write a JSON command to the socket.
         
         @param cmd JSON command to be sent
         @type str
+        @param sock reference to the socket to write to
+        @type QTcpSocket
         """
         data = cmd.encode('utf8', 'backslashreplace')
         length = "{0:09d}".format(len(data))
-        self.qsock.write(length.encode() + data)
-        self.qsock.flush()
+        sock.write(length.encode() + data)
+        sock.flush()
 
 
 def createDebuggerInterfacePython3(debugServer, passive):

eric ide

mercurial