Merged with default branch to get latest bug fixes. multi_processing

Sun, 02 Feb 2020 12:01:27 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 02 Feb 2020 12:01:27 +0100
branch
multi_processing
changeset 7386
3de001de249c
parent 7379
72a72fd56494 (diff)
parent 7385
e8d48a7eb84b (current diff)
child 7389
770ffcb88be5

Merged with default branch to get latest bug fixes.

eric6/QScintilla/Shell.py file | annotate | diff | comparison | revisions
--- a/eric6/DebugClients/Python/AsyncFile.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/DebugClients/Python/AsyncFile.py	Sun Feb 02 12:01:27 2020 +0100
@@ -345,6 +345,7 @@
         
         cmd = prepareJsonCommand("ClientOutput", {
             "text": s,
+            "debuggerId": "",
         })
         self.wpending.append(cmd)
         self.flush()
--- a/eric6/DebugClients/Python/DebugBase.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/DebugClients/Python/DebugBase.py	Sun Feb 02 12:01:27 2020 +0100
@@ -89,6 +89,7 @@
         self.skipFrames = 0
         
         self.isBroken = False
+        self.isException = False
         self.cFrame = None
         
         # current frame we are at
@@ -881,6 +882,7 @@
             self.stop_everywhere = False
         
         self.isBroken = True
+        self.isException = True
         
         stack = []
         if exctb:
@@ -904,6 +906,7 @@
         self.skipFrames = 0
         
         self.isBroken = False
+        self.isException = False
         stop_everywhere = self.stop_everywhere
         self.stop_everywhere = False
         self.eventPollFlag = False
@@ -925,7 +928,7 @@
             else:
                 return exctype
         else:
-            return str(exctype).replace("<class '", "").replace("'>", "")
+            return exctype.__name__
     
     def __extract_stack(self, exctb):
         """
--- a/eric6/DebugClients/Python/DebugClientBase.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/DebugClients/Python/DebugClientBase.py	Sun Feb 02 12:01:27 2020 +0100
@@ -79,25 +79,36 @@
         DebugClientOrigRawInput = __builtins__.__dict__['raw_input']
         __builtins__.__dict__['raw_input'] = DebugClientRawInput
     except (AttributeError, KeyError):
-        import __main__
-        DebugClientOrigRawInput = __main__.__builtins__.__dict__['raw_input']
-        __main__.__builtins__.__dict__['raw_input'] = DebugClientRawInput
+        try:
+            import __main__
+            DebugClientOrigRawInput = __main__.__builtins__.__dict__[
+                'raw_input'
+            ]
+            __main__.__builtins__.__dict__['raw_input'] = DebugClientRawInput
+        except (AttributeError, KeyError):
+            DebugClientOrigRawInput = lambda x: ''  # __IGNORE_WARNING__
 
     try:
         DebugClientOrigInput = __builtins__.__dict__['input']
         __builtins__.__dict__['input'] = DebugClientInput
     except (AttributeError, KeyError):
-        import __main__
-        DebugClientOrigInput = __main__.__builtins__.__dict__['input']
-        __main__.__builtins__.__dict__['input'] = DebugClientInput
+        try:
+            import __main__
+            DebugClientOrigInput = __main__.__builtins__.__dict__['input']
+            __main__.__builtins__.__dict__['input'] = DebugClientInput
+        except (AttributeError, KeyError):
+            DebugClientOrigInput = lambda x: ''  # __IGNORE_WARNING__
 else:
     try:
         DebugClientOrigInput = __builtins__.__dict__['input']
         __builtins__.__dict__['input'] = DebugClientRawInput
     except (AttributeError, KeyError):
-        import __main__
-        DebugClientOrigInput = __main__.__builtins__.__dict__['input']
-        __main__.__builtins__.__dict__['input'] = DebugClientRawInput
+        try:
+            import __main__
+            DebugClientOrigInput = __main__.__builtins__.__dict__['input']
+            __main__.__builtins__.__dict__['input'] = DebugClientRawInput
+        except (AttributeError, KeyError):
+            DebugClientOrigInput = lambda x: ''  # __IGNORE_WARNING__
 
 ###############################################################################
 
@@ -223,6 +234,8 @@
         self.errorstream = None
         self.pollingDisabled = False
         
+        self.__debuggerId = ""
+        
         self.callTraceEnabled = None
         
         self.variant = 'You should not see this'
@@ -233,6 +246,8 @@
         self.defaultCoding = 'utf-8'
         self.__coding = self.defaultCoding
         self.noencoding = False
+        
+        self.startOptions = None
     
     def getCoding(self):
         """
@@ -390,12 +405,24 @@
                 params["variable"], params["frameNumber"],
                 params["scope"], params["filters"])
         
+        elif method == "RequestStack":
+            stack = self.mainThread.getStack()
+            self.sendResponseLine(stack)
+        
         elif method == "RequestThreadList":
             self.dumpThreadList()
         
         elif method == "RequestThreadSet":
-            if params["threadID"] in self.threads:
-                self.setCurrentThread(params["threadID"])
+            if params["threadID"] == -1:
+                # -1 is indication for the main thread
+                threadId = -1
+                for thread in self.threads.values():
+                    if thread.name == "MainThread":
+                        threadId = thread.id
+            else:
+                threadId = params["threadID"]
+            if threadId in self.threads:
+                self.setCurrentThread(threadId)
                 self.sendJsonCommand("ResponseThreadSet", {})
                 stack = self.currentThread.getStack()
                 self.sendJsonCommand("ResponseStack", {
@@ -999,6 +1026,10 @@
             response
         @type dict
         """
+        # send debugger ID with all responses
+        if "debuggerId" not in params:
+            params["debuggerId"] = self.__debuggerId
+        
         cmd = prepareJsonCommand(method, params)
         
         self.writestream.write_p(cmd)
@@ -1110,6 +1141,17 @@
             "exceptions": exceptions,
         })
     
+    def sendDebuggerId(self, debuggerId):
+        """
+        Public method to send the debug client id.
+        
+        @param debuggerId id of this debug client instance (made up of
+            hostname and process ID)
+        @type str
+        """
+        # debugger ID is added automatically by sendJsonCommand
+        self.sendJsonCommand("DebuggerId", {})
+    
     def __clientCapabilities(self):
         """
         Private method to determine the clients capabilities.
@@ -1273,7 +1315,9 @@
         elif "@@i" in remoteAddress:
             remoteAddress = remoteAddress.split("@@i")[0]
         sock = socket.create_connection((remoteAddress, port))
-
+        
+        self.__debuggerId = "{0}-{1}".format(socket.gethostname(), os.getpid())
+        
         self.readstream = AsyncFile(sock, sys.stdin.mode, sys.stdin.name)
         self.writestream = AsyncFile(sock, sys.stdout.mode, sys.stdout.name)
         self.errorstream = AsyncFile(sock, sys.stderr.mode, sys.stderr.name)
@@ -1286,6 +1330,8 @@
         
         # attach to the main thread here
         self.attachThread(mainThread=True)
+        
+        self.sendDebuggerId(self.__debuggerId)
 
     def __unhandled_exception(self, exctype, excval, exctb):
         """
@@ -2159,6 +2205,12 @@
                 print("No program given. Aborting!")
                 # __IGNORE_WARNING_M801__
             else:
+                # Store options if a process is spawn
+                # TODO: check which ones are really needed
+                self.startOptions = (
+                    wd, host, port, exceptions, tracePython, redirect,
+                    self.noencoding, self.fork_auto, self.fork_child,
+                )
                 if not self.noencoding:
                     self.__coding = self.defaultCoding
                 self.startProgInDebugger(args, wd, host, port,
@@ -2203,6 +2255,12 @@
                 sys.path.insert(0, '')
             
             if port >= 0:
+                # Store options if a process is spawn
+                # TODO: check which ones are really needed
+                self.startOptions = (
+                    '', remoteAddress, port, True, False, redirect,
+                    self.noencoding, self.fork_auto, self.fork_child,
+                )
                 if not self.noencoding:
                     self.__coding = self.defaultCoding
                 self.connectDebugger(port, remoteAddress, redirect)
--- a/eric6/DebugClients/Python/ThreadExtension.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/DebugClients/Python/ThreadExtension.py	Sun Feb 02 12:01:27 2020 +0100
@@ -7,7 +7,7 @@
 Module implementing an import hook patching thread modules to get debugged too.
 """
 
-import os.path
+import os
 import sys
 import importlib
 
@@ -176,9 +176,11 @@
                 try:
                     d["name"] = threadNames.get(threadId, thd.name)
                     d["broken"] = thd.isBroken
+                    d["except"] = thd.isException
                 except Exception:
                     d["name"] = 'UnknownThread'
                     d["broken"] = False
+                    d["except"] = False
                 
                 threadList.append(d)
         else:
@@ -186,6 +188,7 @@
             d = {"id": -1}
             d["name"] = "MainThread"
             d["broken"] = self.isBroken
+            d["except"] = self.isException
             threadList.append(d)
         
         self.sendJsonCommand("ResponseThreadList", {
--- a/eric6/Debugger/CallStackViewer.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/Debugger/CallStackViewer.py	Sun Feb 02 12:01:27 2020 +0100
@@ -105,6 +105,7 @@
         
         @param stack list of tuples with call stack data (file name,
             line number, function name, formatted argument/values list)
+        @type list of tuples of (str, str, str, str)
         """
         self.clear()
         for fname, fline, ffunc, fargs in stack:
--- a/eric6/Debugger/CallTraceViewer.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/Debugger/CallTraceViewer.py	Sun Feb 02 12:01:27 2020 +0100
@@ -30,17 +30,22 @@
     """
     sourceFile = pyqtSignal(str, int)
     
-    def __init__(self, debugServer, parent=None):
+    def __init__(self, debugServer, debugViewer, parent=None):
         """
         Constructor
         
-        @param debugServer reference to the debug server object (DebugServer)
-        @param parent reference to the parent widget (QWidget)
+        @param debugServer reference to the debug server object
+        @type DebugServer
+        @param debugViewer reference to the debug viewer object
+        @type DebugViewer
+        @param parent reference to the parent widget
+        @type QWidget
         """
         super(CallTraceViewer, self).__init__(parent)
         self.setupUi(self)
         
         self.__dbs = debugServer
+        self.__debugViewer = debugViewer
         
         self.startTraceButton.setIcon(
             UI.PixmapCache.getIcon("callTraceStart.png"))
@@ -62,6 +67,7 @@
         
         self.__projectMode = False
         self.__project = None
+        self.__tracedDebuggerId = ""
         
         stopOnExit = Preferences.toBool(
             Preferences.Prefs.settings.value("CallTrace/StopOnExit", True))
@@ -83,9 +89,14 @@
         """
         Private slot to set the call trace enabled status.
         
-        @param enabled flag indicating the new state (boolean)
+        @param enabled flag indicating the new state
+        @type bool
         """
-        self.__dbs.setCallTraceEnabled(enabled)
+        if enabled:
+            self.__tracedDebuggerId = (
+                self.__debugViewer.getSelectedDebuggerId()
+            )
+        self.__dbs.setCallTraceEnabled(self.__tracedDebuggerId, enabled)
         self.stopTraceButton.setEnabled(enabled)
         self.startTraceButton.setEnabled(not enabled)
         self.__callTraceEnabled = enabled
@@ -192,8 +203,10 @@
         """
         Private slot to open the double clicked file in an editor.
         
-        @param item reference to the double clicked item (QTreeWidgetItem)
-        @param column column that was double clicked (integer)
+        @param item reference to the double clicked item
+        @type QTreeWidgetItem
+        @param column column that was double clicked
+        @type int
         """
         if item is not None and column > 0:
             columnStr = item.text(column)
@@ -222,63 +235,86 @@
         In project mode the call trace info is shown with project relative
         path names.
         
-        @param enabled flag indicating to enable the project mode (boolean)
+        @param enabled flag indicating to enable the project mode
+        @type bool
         """
         self.__projectMode = enabled
         if enabled and self.__project is None:
             self.__project = e5App().getObject("Project")
     
     def __addCallTraceInfo(self, isCall, fromFile, fromLine, fromFunction,
-                           toFile, toLine, toFunction):
+                           toFile, toLine, toFunction, debuggerId):
         """
         Private method to add an entry to the call trace viewer.
         
-        @param isCall flag indicating a 'call' (boolean)
-        @param fromFile name of the originating file (string)
-        @param fromLine line number in the originating file (string)
-        @param fromFunction name of the originating function (string)
-        @param toFile name of the target file (string)
-        @param toLine line number in the target file (string)
-        @param toFunction name of the target function (string)
+        @param isCall flag indicating a 'call'
+        @type bool
+        @param fromFile name of the originating file
+        @type str
+        @param fromLine line number in the originating file
+        @type str
+        @param fromFunction name of the originating function
+        @type str
+        @param toFile name of the target file
+        @type str
+        @param toLine line number in the target file
+        @type str
+        @param toFunction name of the target function
+        @type str
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        if isCall:
-            icon = UI.PixmapCache.getIcon("forward.png")
-        else:
-            icon = UI.PixmapCache.getIcon("back.png")
-        parentItem = (
-            self.__callStack[-1] if self.__callStack else self.callTrace)
-        
-        if self.__projectMode:
-            fromFile = self.__project.getRelativePath(fromFile)
-            toFile = self.__project.getRelativePath(toFile)
-        
-        itm = QTreeWidgetItem(
-            parentItem,
-            ["",
-             self.__entryFormat.format(fromFile, fromLine, fromFunction),
-             self.__entryFormat.format(toFile, toLine, toFunction)])
-        itm.setIcon(0, icon)
-        itm.setData(0, Qt.UserRole, isCall)
-        itm.setExpanded(True)
-        
-        if isCall:
-            self.__callStack.append(itm)
-        else:
-            if self.__callStack:
-                self.__callStack.pop(-1)
+        if debuggerId == self.__tracedDebuggerId:
+            if isCall:
+                icon = UI.PixmapCache.getIcon("forward.png")
+            else:
+                icon = UI.PixmapCache.getIcon("back.png")
+            parentItem = (
+                self.__callStack[-1] if self.__callStack else self.callTrace)
+            
+            if self.__projectMode:
+                fromFile = self.__project.getRelativePath(fromFile)
+                toFile = self.__project.getRelativePath(toFile)
+            
+            itm = QTreeWidgetItem(
+                parentItem,
+                ["",
+                 self.__entryFormat.format(fromFile, fromLine, fromFunction),
+                 self.__entryFormat.format(toFile, toLine, toFunction)])
+            itm.setIcon(0, icon)
+            itm.setData(0, Qt.UserRole, isCall)
+            itm.setExpanded(True)
+            
+            if isCall:
+                self.__callStack.append(itm)
+            else:
+                if self.__callStack:
+                    self.__callStack.pop(-1)
     
     def isCallTraceEnabled(self):
         """
         Public method to get the state of the call trace function.
         
-        @return flag indicating the state of the call trace function (boolean)
+        @return flag indicating the state of the call trace function
+        @rtype bool
         """
         return self.__callTraceEnabled
     
-    @pyqtSlot()
-    def __clientExit(self):
+    @pyqtSlot(int, str, bool, str)
+    def __clientExit(self, status, message, quiet, debuggerId):
         """
-        Private slot handling a client exiting.
+        Private slot to handle a debug client terminating.
+        
+        @param status exit code of the debugged program
+        @type int
+        @param message exit message of the debugged program
+        @type str
+        @param quiet flag indicating to suppress exit info display
+        @type bool
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        if self.stopCheckBox.isChecked():
-            self.__setCallTraceEnabled(False)
+        if debuggerId == self.__tracedDebuggerId:
+            if self.stopCheckBox.isChecked():
+                self.__setCallTraceEnabled(False)
+            self.__tracedDebuggerId = ""
--- a/eric6/Debugger/DebugServer.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/Debugger/DebugServer.py	Sun Feb 02 12:01:27 2020 +0100
@@ -44,29 +44,34 @@
     @signal clientOutput(str) emitted after the client has sent some output
     @signal clientRawInputSent() emitted after the data was sent to the
         debug client
-    @signal clientLine(filename, lineno, forStack) emitted after the
-        debug client has executed a line of code
-    @signal clientStack(stack) emitted after the debug client has executed a
-        line of code
-    @signal clientThreadList(currentId, threadList) emitted after a thread list
-        has been received
-    @signal clientThreadSet() emitted after the client has acknowledged the
-        change of the current thread
-    @signal clientVariables(scope, variables) emitted after a variables dump
-        has been received
-    @signal clientVariable(scope, variables) emitted after a dump for one class
-        variable has been received
-    @signal clientStatement(bool) emitted after an interactive command has
-        been executed. The parameter is 0 to indicate that the command is
-        complete and 1 if it needs more input.
-    @signal clientException(exception) emitted after an exception occured on
-        the client side
-    @signal clientSyntaxError(exception) emitted after a syntax error has been
-        detected on the client side
-    @signal clientSignal(signal) emitted after a signal has been generated on
-        the client side
-    @signal clientExit(int, str, bool) emitted after the client has exited
-        giving the exit status, an exit message and an indication to be quiet
+    @signal clientLine(filename, lineno, debuggerId, forStack) emitted after
+        the debug client has executed a line of code
+    @signal clientStack(stack, debuggerId) emitted after the debug client has
+        executed a line of code
+    @signal clientThreadList(currentId, threadList, debuggerId) emitted after
+        a thread list has been received
+    @signal clientThreadSet(debuggerId) emitted after the client has
+        acknowledged the change of the current thread
+    @signal clientVariables(scope, variables, debuggerId) emitted after a
+        variables dump has been received
+    @signal clientVariable(scope, variables, debuggerId) emitted after a dump
+        for one class variable has been received
+    @signal clientStatement(continue, debuggerId) emitted after an interactive
+        command has been executed. The parameter is False to indicate that the
+        command is complete and True if it needs more input.
+    @signal clientException(exceptionType, exceptionMessage, stackTrace,
+        debuggerId) emitted after an exception occured on the client side
+    @signal clientSyntaxError(message, filename, linenumber, characternumber,
+        debuggerId) emitted after a syntax error has been detected on the
+        client side
+    @signal clientSignal(message, filename, linenumber, function name,
+        function arguments, debuggerId) emitted after a signal has been
+        generated on the client side
+    @signal clientExit(int, str, bool, str) emitted after the client has exited
+        giving the exit status, an exit message, an indication to be quiet and
+        the ID of the exited client
+    @signal lastClientExited() emitted to indicate that the last connected
+        debug client has terminated
     @signal clientClearBreak(filename, lineno) emitted after the debug client
         has decided to clear a temporary breakpoint
     @signal clientBreakConditionError(fn, lineno) emitted after the client has
@@ -111,9 +116,11 @@
         reported an unexpected test success
     @signal callTraceInfo emitted after the client reported the call trace
         data (isCall, fromFile, fromLine, fromFunction, toFile, toLine,
-        toFunction)
+        toFunction, debuggerId)
     @signal appendStdout(msg) emitted when a passive debug connection is
         established or lost
+    @signal clientDebuggerIds(debuggerIds) emitted to give the list of IDs of
+        attached debugger backends
     """
     clientClearBreak = pyqtSignal(str, int)
     clientClearWatch = pyqtSignal(str)
@@ -122,17 +129,18 @@
     clientProcessStderr = pyqtSignal(str)
     clientRawInputSent = pyqtSignal()
     clientOutput = pyqtSignal(str)
-    clientLine = pyqtSignal(str, int, bool)
-    clientStack = pyqtSignal(list)
-    clientThreadList = pyqtSignal('PyQt_PyObject', list)
-    clientThreadSet = pyqtSignal()
-    clientVariables = pyqtSignal(int, list)
-    clientVariable = pyqtSignal(int, list)
-    clientStatement = pyqtSignal(bool)
-    clientException = pyqtSignal(str, str, list)
-    clientSyntaxError = pyqtSignal(str, str, int, int)
-    clientSignal = pyqtSignal(str, str, int, str, str)
-    clientExit = pyqtSignal(int, str, bool)
+    clientLine = pyqtSignal(str, int, str, bool)
+    clientStack = pyqtSignal(list, str)
+    clientThreadList = pyqtSignal(int, list, str)
+    clientThreadSet = pyqtSignal(str)
+    clientVariables = pyqtSignal(int, list, str)
+    clientVariable = pyqtSignal(int, list, str)
+    clientStatement = pyqtSignal(bool, str)
+    clientException = pyqtSignal(str, str, list, str)
+    clientSyntaxError = pyqtSignal(str, str, int, int, str)
+    clientSignal = pyqtSignal(str, str, int, str, str, str)
+    clientExit = pyqtSignal(int, str, bool, str)
+    lastClientExited = pyqtSignal()
     clientBreakConditionError = pyqtSignal(str, int)
     clientWatchConditionError = pyqtSignal(str)
     clientRawInput = pyqtSignal(str, bool)
@@ -140,6 +148,7 @@
     clientCapabilities = pyqtSignal(int, str, str)
     clientCompletionList = pyqtSignal(list, str)
     clientInterpreterChanged = pyqtSignal(str)
+    clientDebuggerIds = pyqtSignal(list)
     utDiscovered = pyqtSignal(list, str, str)
     utPrepared = pyqtSignal(int, str, str)
     utStartTest = pyqtSignal(str, str)
@@ -151,7 +160,7 @@
     utTestSucceededUnexpected = pyqtSignal(str, str)
     utFinished = pyqtSignal()
     passiveDebugStarted = pyqtSignal(str, bool)
-    callTraceInfo = pyqtSignal(bool, str, str, str, str, str, str)
+    callTraceInfo = pyqtSignal(bool, str, str, str, str, str, str, str)
     appendStdout = pyqtSignal(str)
     
     def __init__(self, originalPathString, preventPassiveDebugging=False):
@@ -528,8 +537,6 @@
                 elif self.__autoClearShell:
                     self.__autoClearShell = False
                     self.remoteBanner()
-##                self.remoteClientVariables(0, [], 0)
-##                self.remoteClientVariables(1, [], 0)
             else:
                 if clType and self.lastClientType:
                     self.__setClientType(self.lastClientType)
@@ -583,7 +590,8 @@
                 index = self.breakpointModel.index(row, 0, parentIndex)
                 fn, lineno = (
                     self.breakpointModel.getBreakPointByIndex(index)[0:2])
-                self.remoteBreakpoint(fn, lineno, False)
+                # delete the breakpoints of all connected backends
+                self.remoteBreakpoint("", fn, lineno, False)
 
     def __changeBreakPoints(self, startIndex, endIndex):
         """
@@ -608,24 +616,31 @@
             self.__deleteBreakPoints(
                 QModelIndex(), startIndex.row(), endIndex.row())
         
-    def __addBreakPoints(self, parentIndex, start, end):
+    def __addBreakPoints(self, parentIndex, start, end, debuggerId=""):
         """
         Private slot to add breakpoints.
         
-        @param parentIndex index of parent item (QModelIndex)
-        @param start start row (integer)
-        @param end end row (integer)
+        @param parentIndex index of parent item
+        @type QModelIndex
+        @param start start row
+        @type int
+        @param end end row
+        @type int
+        @param debuggerId ID of the debugger backend to send to. If this is
+            empty, they will be broadcast to all connected backends.
+        @type str
         """
         if self.debugging:
             for row in range(start, end + 1):
                 index = self.breakpointModel.index(row, 0, parentIndex)
                 fn, line, cond, temp, enabled, ignorecount = (
                     self.breakpointModel.getBreakPointByIndex(index)[:6])
-                self.remoteBreakpoint(fn, line, True, cond, temp)
+                self.remoteBreakpoint(debuggerId, fn, line, True, cond, temp)
                 if not enabled:
-                    self.__remoteBreakpointEnable(fn, line, False)
+                    self.__remoteBreakpointEnable(debuggerId, fn, line, False)
                 if ignorecount:
-                    self.__remoteBreakpointIgnore(fn, line, ignorecount)
+                    self.__remoteBreakpointIgnore(
+                        debuggerId, fn, line, ignorecount)
 
     def __makeWatchCondition(self, cond, special):
         """
@@ -679,9 +694,12 @@
         """
         Private slot to delete watch expressions.
         
-        @param parentIndex index of parent item (QModelIndex)
-        @param start start row (integer)
-        @param end end row (integer)
+        @param parentIndex index of parent item
+        @type QModelIndex
+        @param start start row
+        @type int
+        @param end end row
+        @type int
         """
         if self.debugging:
             for row in range(start, end + 1):
@@ -689,27 +707,35 @@
                 cond, special = (
                     self.watchpointModel.getWatchPointByIndex(index)[0:2])
                 cond = self.__makeWatchCondition(cond, special)
-                self.__remoteWatchpoint(cond, False)
+                self.__remoteWatchpoint("", cond, False)
         
     def __watchPointDataAboutToBeChanged(self, startIndex, endIndex):
         """
         Private slot to handle the dataAboutToBeChanged signal of the
         watch expression model.
         
-        @param startIndex start index of the rows to be changed (QModelIndex)
-        @param endIndex end index of the rows to be changed (QModelIndex)
+        @param startIndex start index of the rows to be changed
+        @type QModelIndex
+        @param endIndex end index of the rows to be changed
+        @type QModelIndex
         """
         if self.debugging:
             self.__deleteWatchPoints(
                 QModelIndex(), startIndex.row(), endIndex.row())
         
-    def __addWatchPoints(self, parentIndex, start, end):
+    def __addWatchPoints(self, parentIndex, start, end, debuggerId=""):
         """
         Private slot to set a watch expression.
         
-        @param parentIndex index of parent item (QModelIndex)
-        @param start start row (integer)
-        @param end end row (integer)
+        @param parentIndex index of parent item
+        @type QModelIndex
+        @param start start row
+        @type int
+        @param end end row
+        @type int
+        @param debuggerId ID of the debugger backend to send to. If this is
+            empty, they will be broadcast to all connected backends.
+        @type str
         """
         if self.debugging:
             for row in range(start, end + 1):
@@ -717,11 +743,12 @@
                 cond, special, temp, enabled, ignorecount = (
                     self.watchpointModel.getWatchPointByIndex(index)[:5])
                 cond = self.__makeWatchCondition(cond, special)
-                self.__remoteWatchpoint(cond, True, temp)
+                self.__remoteWatchpoint(debuggerId, cond, True, temp)
                 if not enabled:
-                    self.__remoteWatchpointEnable(cond, False)
+                    self.__remoteWatchpointEnable(debuggerId, cond, False)
                 if ignorecount:
-                    self.__remoteWatchpointIgnore(cond, ignorecount)
+                    self.__remoteWatchpointIgnore(debuggerId, cond,
+                                                  ignorecount)
         
     def __changeWatchPoints(self, startIndex, endIndex):
         """
@@ -800,19 +827,22 @@
             self.__createDebuggerInterface(
                 Preferences.getDebugger("PassiveDbgType"))
         
-        accepted = self.debuggerInterface.newConnection(sock)
-        if accepted:
-            # Perform actions necessary, if client type has changed
-            if self.lastClientType != self.clientType:
-                self.lastClientType = self.clientType
-                self.remoteBanner()
-            elif self.__autoClearShell:
-                self.__autoClearShell = False
-                self.remoteBanner()
-            elif self.passive:
-                self.remoteBanner()
-            
-            self.debuggerInterface.flush()
+        self.debuggerInterface.newConnection(sock)
+    
+    def masterClientConnected(self):
+        """
+        Public method to perform actions after the master client has finally
+        established the connection.
+        """
+        # Perform actions necessary, if client type has changed
+        if self.lastClientType != self.clientType:
+            self.lastClientType = self.clientType
+            self.remoteBanner()
+        elif self.__autoClearShell:
+            self.__autoClearShell = False
+            self.remoteBanner()
+        elif self.passive:
+            self.remoteBanner()
 
     def shutdownServer(self):
         """
@@ -909,7 +939,7 @@
         self.startClient(False, forProject=forProject,
                          runInConsole=runInConsole, venvName=venvName)
         
-        self.setCallTraceEnabled(enableCallTrace)
+        self.setCallTraceEnabled("", enableCallTrace)
         self.remoteEnvironment(env)
         
         self.debuggerInterface.remoteLoad(fn, argv, wd, tracePython,
@@ -1103,123 +1133,172 @@
         self.debugging = False
         self.running = True
 
-    def remoteStatement(self, stmt):
+    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.debuggerInterface.remoteStatement(stmt)
+        self.debuggerInterface.remoteStatement(debuggerId, stmt.rstrip())
 
-    def remoteStep(self):
+    def remoteStep(self, debuggerId):
         """
         Public method to single step the debugged program.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.debuggerInterface.remoteStep()
+        self.debuggerInterface.remoteStep(debuggerId)
 
-    def remoteStepOver(self):
+    def remoteStepOver(self, debuggerId):
         """
         Public method to step over the debugged program.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.debuggerInterface.remoteStepOver()
+        self.debuggerInterface.remoteStepOver(debuggerId)
 
-    def remoteStepOut(self):
+    def remoteStepOut(self, debuggerId):
         """
         Public method to step out the debugged program.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.debuggerInterface.remoteStepOut()
+        self.debuggerInterface.remoteStepOut(debuggerId)
 
-    def remoteStepQuit(self):
+    def remoteStepQuit(self, debuggerId):
         """
         Public method to stop the debugged program.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.debuggerInterface.remoteStepQuit()
+        self.debuggerInterface.remoteStepQuit(debuggerId)
 
-    def remoteContinue(self, special=False):
+    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
         """
-        self.debuggerInterface.remoteContinue(special)
+        self.debuggerInterface.remoteContinue(debuggerId, special)
 
-    def remoteMoveIP(self, line):
+    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.debuggerInterface.remoteMoveIP(line)
+        self.debuggerInterface.remoteMoveIP(debuggerId, line)
 
-    def remoteBreakpoint(self, fn, line, setBreakpoint, cond=None, temp=False):
+    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 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
-            (boolean)
-        @param cond condition of the breakpoint (string)
-        @param temp flag indicating a temporary breakpoint (boolean)
+        @type bool
+        @param cond condition of the breakpoint
+        @type str
+        @param temp flag indicating a temporary breakpoint
+        @type bool
         """
-        self.debuggerInterface.remoteBreakpoint(fn, line, setBreakpoint, cond,
-                                                temp)
+        self.debuggerInterface.remoteBreakpoint(
+            debuggerId, fn, line, setBreakpoint, cond, temp)
         
-    def __remoteBreakpointEnable(self, fn, line, enable):
+    def __remoteBreakpointEnable(self, debuggerId, fn, line, enable):
         """
         Private 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.debuggerInterface.remoteBreakpointEnable(fn, line, enable)
+        self.debuggerInterface.remoteBreakpointEnable(
+            debuggerId, fn, line, enable)
         
-    def __remoteBreakpointIgnore(self, fn, line, count):
+    def __remoteBreakpointIgnore(self, debuggerId, fn, line, count):
         """
         Private 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.debuggerInterface.remoteBreakpointIgnore(fn, line, count)
+        self.debuggerInterface.remoteBreakpointIgnore(
+            debuggerId, fn, line, count)
         
-    def __remoteWatchpoint(self, cond, setWatch, temp=False):
+    def __remoteWatchpoint(self, debuggerId, cond, setWatch, temp=False):
         """
         Private 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.debuggerInterface.remoteWatchpoint(cond, setWatch, temp)
+        self.debuggerInterface.remoteWatchpoint(debuggerId, cond, setWatch,
+                                                temp)
     
-    def __remoteWatchpointEnable(self, cond, enable):
+    def __remoteWatchpointEnable(self, debuggerId, cond, enable):
         """
         Private 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.debuggerInterface.remoteWatchpointEnable(cond, enable)
+        self.debuggerInterface.remoteWatchpointEnable(debuggerId, cond, enable)
     
-    def __remoteWatchpointIgnore(self, cond, count):
+    def __remoteWatchpointIgnore(self, debuggerId, cond, count):
         """
         Private 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.debuggerInterface.remoteWatchpointIgnore(cond, count)
+        self.debuggerInterface.remoteWatchpointIgnore(debuggerId, cond, count)
     
     def remoteRawInput(self, s):
         """
@@ -1230,42 +1309,74 @@
         self.debuggerInterface.remoteRawInput(s)
         self.clientRawInputSent.emit()
         
-    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.debuggerInterface.remoteThreadList()
+        self.debuggerInterface.remoteThreadList(debuggerId)
         
-    def remoteSetThread(self, tid):
+    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.debuggerInterface.remoteSetThread(debuggerId, tid)
+    
+    def remoteClientStack(self, debuggerId):
         """
-        self.debuggerInterface.remoteSetThread(tid)
+        Public method to request the stack of the main thread.
         
-    def remoteClientVariables(self, scope, filterList, framenr=0):
+        @param debuggerId ID of the debugger backend
+        @type str
+        """
+        self.debuggerInterface.remoteClientStack(debuggerId)
+    
+    def remoteClientVariables(self, debuggerId, scope, filterList, framenr=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)
-        @param filterList list of variable types to filter out (list of int)
-        @param framenr framenumber of the variables to retrieve (int)
+        @type int
+        @param filterList list of variable types to filter out
+        @type list of int
+        @param framenr framenumber of the variables to retrieve
+        @type int
         """
         self.debuggerInterface.remoteClientVariables(
-            scope, filterList, framenr, self.__maxVariableSize)
+            debuggerId, scope, filterList, framenr, self.__maxVariableSize)
         
-    def remoteClientVariable(self, scope, filterList, var, framenr=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)
-        @param filterList list of variable types to filter out (list of int)
-        @param var list encoded name of variable to retrieve (string)
-        @param framenr framenumber of the variables to retrieve (int)
+        @type int
+        @param filterList list of variable types to filter out
+        @type list of int
+        @param var list encoded name of variable to retrieve
+        @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
+            be shown. If it is bigger than that, a 'too big' indication will
+            be given (@@TOO_BIG_TO_SHOW@@).
+        @type int
         """
         self.debuggerInterface.remoteClientVariable(
-            scope, filterList, var, framenr, self.__maxVariableSize)
+            debuggerId, scope, filterList, var, framenr,
+            self.__maxVariableSize)
         
     def remoteClientSetFilter(self, scope, filterStr):
         """
@@ -1277,13 +1388,16 @@
         """
         self.debuggerInterface.remoteClientSetFilter(scope, filterStr)
         
-    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.debuggerInterface.setCallTraceEnabled(on)
+        self.debuggerInterface.setCallTraceEnabled(debuggerId, on)
         
     def remoteBanner(self):
         """
@@ -1437,108 +1551,150 @@
         """
         self.debuggerInterface.remoteUTStop()
         
-    def signalClientOutput(self, line):
+    def signalClientOutput(self, line, debuggerId):
         """
         Public method to process a line of client output.
         
-        @param line client output (string)
+        @param line client output
+        @type str
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.clientOutput.emit(line)
+        if debuggerId:
+            self.clientOutput.emit("{0}: {1}".format(debuggerId, line))
+        else:
+            self.clientOutput.emit(line)
         
-    def signalClientLine(self, filename, lineno, forStack=False):
+    def signalClientLine(self, filename, lineno, debuggerId, forStack=False):
         """
         Public method to process client position feedback.
         
-        @param filename name of the file currently being executed (string)
-        @param lineno line of code currently being executed (integer)
-        @param forStack flag indicating this is for a stack dump (boolean)
+        @param filename name of the file currently being executed
+        @type str
+        @param lineno line of code currently being executed
+        @type int
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param forStack flag indicating this is for a stack dump
+        @type bool
         """
-        self.clientLine.emit(filename, lineno, forStack)
+        self.clientLine.emit(filename, lineno, debuggerId, forStack)
         
-    def signalClientStack(self, stack):
+    def signalClientStack(self, stack, debuggerId):
         """
         Public method to process a client's stack information.
         
         @param stack list of stack entries. Each entry is a tuple of three
             values giving the filename, linenumber and method
-            (list of lists of (string, integer, string))
+        @type list of lists of (string, integer, string)
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.clientStack.emit(stack)
+        self.clientStack.emit(stack, debuggerId)
         
-    def signalClientThreadList(self, currentId, threadList):
+    def signalClientThreadList(self, currentId, threadList, debuggerId):
         """
         Public method to process the client thread list info.
         
-        @param currentId id of the current thread (integer)
+        @param currentId id of the current thread
+        @type int
         @param threadList list of dictionaries containing the thread data
+        @type list of dict
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.clientThreadList.emit(currentId, threadList)
+        self.clientThreadList.emit(currentId, threadList, debuggerId)
         
-    def signalClientThreadSet(self):
+    def signalClientThreadSet(self, debuggerId):
         """
         Public method to handle the change of the client thread.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.clientThreadSet.emit()
+        self.clientThreadSet.emit(debuggerId)
         
-    def signalClientVariables(self, scope, variables):
+    def signalClientVariables(self, scope, variables, debuggerId):
         """
         Public method to process the client variables info.
         
         @param scope scope of the variables (-1 = empty global, 1 = global,
             0 = local)
+        @type int
         @param variables the list of variables from the client
+        @type list
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.clientVariables.emit(scope, variables)
+        self.clientVariables.emit(scope, variables, debuggerId)
         
-    def signalClientVariable(self, scope, variables):
+    def signalClientVariable(self, scope, variables, debuggerId):
         """
         Public method to process the client variable info.
         
         @param scope scope of the variables (-1 = empty global, 1 = global,
             0 = local)
+        @type int
         @param variables the list of members of a classvariable from the client
+        @type list
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.clientVariable.emit(scope, variables)
+        self.clientVariable.emit(scope, variables, debuggerId)
         
-    def signalClientStatement(self, more):
+    def signalClientStatement(self, more, debuggerId):
         """
         Public method to process the input response from the client.
         
         @param more flag indicating that more user input is required
+        @type bool
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.clientStatement.emit(more)
+        self.clientStatement.emit(more, debuggerId)
         
     def signalClientException(self, exceptionType, exceptionMessage,
-                              stackTrace):
+                              stackTrace, debuggerId):
         """
         Public method to process the exception info from the client.
         
-        @param exceptionType type of exception raised (string)
-        @param exceptionMessage message given by the exception (string)
+        @param exceptionType type of exception raised
+        @type str
+        @param exceptionMessage message given by the exception
+        @type str
         @param stackTrace list of stack entries with the exception position
             first. Each stack entry is a list giving the filename and the
             linenumber.
+        @type list
+        @param debuggerId ID of the debugger backend
+        @type str
         """
         if self.running:
             self.clientException.emit(exceptionType, exceptionMessage,
-                                      stackTrace)
+                                      stackTrace, debuggerId)
         
-    def signalClientSyntaxError(self, message, filename, lineNo, characterNo):
+    def signalClientSyntaxError(self, message, filename, lineNo, characterNo,
+                                debuggerId):
         """
-        Public method to process the syntax error info from the client.
+        Public method to process a syntax error info from the client.
         
-        @param message message of the syntax error (string)
+        @param message message of the syntax error
+        @type str
         @param filename translated filename of the syntax error position
-            (string)
-        @param lineNo line number of the syntax error position (integer)
+        @type str
+        @param lineNo line number of the syntax error position
+        @type int
         @param characterNo character number of the syntax error position
-            (integer)
+        @type int
+        @param debuggerId ID of the debugger backend
+        @type str
         """
         if self.running:
-            self.clientSyntaxError.emit(message, filename, lineNo, characterNo)
+            self.clientSyntaxError.emit(message, filename, lineNo, characterNo,
+                                        debuggerId)
         
     def signalClientSignal(self, message, filename, lineNo,
-                           funcName, funcArgs):
+                           funcName, funcArgs, debuggerId):
         """
         Public method to process a signal generated on the client side.
         
@@ -1552,12 +1708,14 @@
         @type str
         @param funcArgs function arguments
         @type str
+        @param debuggerId ID of the debugger backend
+        @type str
         """
         if self.running:
             self.clientSignal.emit(message, filename, lineNo,
-                                   funcName, funcArgs)
+                                   funcName, funcArgs, debuggerId)
         
-    def signalClientExit(self, status, message=""):
+    def signalClientExit(self, status, message, debuggerId):
         """
         Public method to process the client exit status.
         
@@ -1565,10 +1723,18 @@
         @type int
         @param message message sent with the exit
         @type str
+        @param debuggerId ID of the debugger backend
+        @type str
+        """
+        self.clientExit.emit(int(status), message, False, debuggerId)
+    
+    def signalLastClientExited(self):
+        """
+        Public method to process the last client exit event.
         """
         if self.passive:
             self.__passiveShutDown()
-        self.clientExit.emit(int(status), message, False)
+        self.lastClientExited.emit()
         if Preferences.getDebugger("AutomaticReset") or (self.running and
                                                          not self.debugging):
             self.debugging = False
@@ -1576,9 +1742,9 @@
         if self.passive:
             self.__createDebuggerInterface("None")
             self.signalClientOutput(self.tr('\nNot connected\n'))
-            self.signalClientStatement(False)
+            self.signalClientStatement(False, "")
         self.running = False
-        
+    
     def signalClientClearBreak(self, filename, lineno):
         """
         Public method to process the client clear breakpoint command.
@@ -1665,22 +1831,31 @@
         self.clientCompletionList.emit(completionList, text)
         
     def signalClientCallTrace(self, isCall, fromFile, fromLine, fromFunction,
-                              toFile, toLine, toFunction):
+                              toFile, toLine, toFunction, debuggerId):
         """
         Public method to process the client call trace data.
         
-        @param isCall flag indicating a 'call' (boolean)
-        @param fromFile name of the originating file (string)
-        @param fromLine line number in the originating file (string)
-        @param fromFunction name of the originating function (string)
-        @param toFile name of the target file (string)
-        @param toLine line number in the target file (string)
-        @param toFunction name of the target function (string)
+        @param isCall flag indicating a 'call'
+        @type bool
+        @param fromFile name of the originating file
+        @type str
+        @param fromLine line number in the originating file
+        @type str
+        @param fromFunction name of the originating function
+        @type str
+        @param toFile name of the target file
+        @type str
+        @param toLine line number in the target file
+        @type str
+        @param toFunction name of the target function
+        @type str
+        @param debuggerId ID of the debugger backend
+        @type str
         """
         self.callTraceInfo.emit(
             isCall, fromFile, fromLine, fromFunction,
-            toFile, toLine, toFunction)
-        
+            toFile, toLine, toFunction, debuggerId)
+    
     def clientUtDiscovered(self, testCases, exceptionType, exceptionValue):
         """
         Public method to process the client unittest discover info.
@@ -1777,7 +1952,7 @@
         """
         self.utFinished.emit()
         
-        self.clientExit.emit(int(status), "", True)
+        self.clientExit.emit(int(status), "", True, "")
         self.debugging = False
         self.running = False
         
@@ -1804,21 +1979,31 @@
         self.shutdownServer()
         self.appendStdout.emit(self.tr("Passive debug connection closed\n"))
         
-    def __restoreBreakpoints(self):
+    def __restoreBreakpoints(self, debuggerId=""):
         """
         Private method to restore the breakpoints after a restart.
+        
+        @param debuggerId ID of the debugger backend to send to. If this is
+            empty, they will be broadcast to all connected backends.
+        @type str
         """
         if self.debugging:
             self.__addBreakPoints(
-                QModelIndex(), 0, self.breakpointModel.rowCount() - 1)
+                QModelIndex(), 0, self.breakpointModel.rowCount() - 1,
+                debuggerId)
     
-    def __restoreWatchpoints(self):
+    def __restoreWatchpoints(self, debuggerId=""):
         """
         Private method to restore the watch expressions after a restart.
+        
+        @param debuggerId ID of the debugger backend to send to. If this is
+            empty, they will be broadcast to all connected backends.
+        @type str
         """
         if self.debugging:
             self.__addWatchPoints(
-                QModelIndex(), 0, self.watchpointModel.rowCount() - 1)
+                QModelIndex(), 0, self.watchpointModel.rowCount() - 1,
+                debuggerId)
     
     def getBreakPointModel(self):
         """
@@ -1862,3 +2047,36 @@
         @type bool
         """
         self.debugging = on
+    
+    def signalClientDebuggerIds(self, debuggerIds):
+        """
+        Public method to signal the receipt of a new debugger ID.
+        
+        This signal indicates, that a new debugger backend has connected.
+        
+        @param debuggerIds list of IDs of the connected debugger backends
+        @type list of str
+        """
+        self.clientDebuggerIds.emit(debuggerIds)
+    
+    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
+        """
+        if self.debuggerInterface:
+            return self.debuggerInterface.getDebuggerIds()
+        else:
+            return []
+    
+    def initializeClient(self, debuggerId):
+        """
+        Public method to initialize a freshly connected debug client.
+        
+        @param debuggerId ID of the connected debugger
+        @type str
+        """
+        self.__restoreBreakpoints(debuggerId)
+        self.__restoreWatchpoints(debuggerId)
--- a/eric6/Debugger/DebugUI.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/Debugger/DebugUI.py	Sun Feb 02 12:01:27 2020 +0100
@@ -10,7 +10,7 @@
 
 import os
 
-from PyQt5.QtCore import pyqtSignal, QObject, Qt
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt
 from PyQt5.QtGui import QKeySequence
 from PyQt5.QtWidgets import QMenu, QToolBar, QApplication, QDialog
 
@@ -34,7 +34,8 @@
     """
     Class implementing the debugger part of the UI.
     
-    @signal clientStack(stack) emitted at breaking after a reported exception
+    @signal clientStack(stack, debuggerId) emitted at breaking after a reported
+        exception
     @signal compileForms() emitted if changed project forms should be compiled
     @signal compileResources() emitted if changed project resources should be
         compiled
@@ -48,7 +49,7 @@
     @signal appendStdout(msg) emitted when the client program has terminated
         and the display of the termination dialog is suppressed
     """
-    clientStack = pyqtSignal(list)
+    clientStack = pyqtSignal(list, str)
     resetUI = pyqtSignal()
     exceptionInterrupt = pyqtSignal()
     compileForms = pyqtSignal()
@@ -137,8 +138,6 @@
         debugServer.passiveDebugStarted.connect(self.__passiveDebugStarted)
         debugServer.clientThreadSet.connect(self.__clientThreadSet)
         
-        debugServer.clientThreadList.connect(debugViewer.showThreadList)
-        
         # Connect the signals emitted by the viewmanager
         vm.editorOpened.connect(self.__editorOpened)
         vm.lastEditorClosed.connect(self.__lastEditorClosed)
@@ -995,13 +994,18 @@
             self.stopAct.setEnabled(False)
         self.resetUI.emit()
         
-    def __clientLine(self, fn, line, forStack):
+    def __clientLine(self, fn, line, debuggerId, forStack):
         """
         Private method to handle a change to the current line.
         
-        @param fn filename (string)
-        @param line linenumber (int)
-        @param forStack flag indicating this is for a stack dump (boolean)
+        @param fn filename
+        @type str
+        @param line linenumber
+        @type int
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param forStack flag indicating this is for a stack dump
+        @type bool
         """
         self.ui.raise_()
         self.ui.activateWindow()
@@ -1010,10 +1014,11 @@
         self.viewmanager.setFileLine(fn, line)
         if not forStack:
             self.__getThreadList()
-            self.__getClientVariables()
+            self.__getClientVariables(debuggerId)
 
         self.debugActGrp.setEnabled(True)
-        
+    
+    @pyqtSlot(int, str, bool)
     def __clientExit(self, status, message, quiet):
         """
         Private method to handle the debugged program terminating.
@@ -1125,13 +1130,19 @@
                     '</p>')
                 .format(filename, message, lineNo, characterNo))
         
-    def __clientException(self, exceptionType, exceptionMessage, stackTrace):
+    def __clientException(self, exceptionType, exceptionMessage, stackTrace,
+                          debuggerId):
         """
         Private method to handle an exception of the debugged program.
         
-        @param exceptionType type of exception raised (string)
-        @param exceptionMessage message given by the exception (string)
-        @param stackTrace list of stack entries (list of string)
+        @param exceptionType type of exception raised
+        @type str
+        @param exceptionMessage message given by the exception
+        @type (str
+        @param stackTrace list of stack entries
+        @type list of str
+        @param debuggerId ID of the debugger backend
+        @type str
         """
         self.ui.raise_()
         QApplication.processEvents()
@@ -1215,8 +1226,8 @@
                 stack = []
                 for fn, ln, func, args in stackTrace:
                     stack.append((fn, ln, func, args))
-                self.clientStack.emit(stack)
-                self.__getClientVariables()
+                self.clientStack.emit(stack, debuggerId)
+                self.__getClientVariables(debuggerId)
                 self.ui.setDebugProfile()
                 self.debugActGrp.setEnabled(True)
                 return
@@ -1232,7 +1243,8 @@
         else:
             self.__continue()
         
-    def __clientSignal(self, message, filename, lineNo, funcName, funcArgs):
+    def __clientSignal(self, message, filename, lineNo, funcName, funcArgs,
+                       debuggerId):
         """
         Private method to handle a signal generated on the client side.
         
@@ -1246,6 +1258,8 @@
         @type str
         @param funcArgs function arguments
         @type str
+        @param debuggerId ID of the debugger backend
+        @type str
         """
         self.ui.raise_()
         self.ui.activateWindow()
@@ -1270,64 +1284,87 @@
                 self.tr('The program being debugged has terminated'
                         ' unexpectedly.'))
         
-    def __getThreadList(self):
+    def __getThreadList(self, debuggerId=""):
         """
         Private method to get the list of threads from the client.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.debugServer.remoteThreadList()
+        self.debugServer.remoteThreadList(debuggerId)
         
-    def __clientThreadSet(self):
+    def __clientThreadSet(self, debuggerId):
         """
         Private method to handle a change of the client's current thread.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.debugServer.remoteClientVariables(0, self.localsVarFilter)
+        self.debugServer.remoteClientVariables(
+            debuggerId, 0, self.localsVarFilter)
         
-    def __getClientVariables(self):
+    def __getClientVariables(self, debuggerId):
         """
         Private method to request the global and local variables.
         
         In the first step, the global variables are requested from the client.
         Once these have been received, the local variables are requested.
         This happens in the method '__clientVariables'.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
         # get globals first
-        self.debugServer.remoteClientVariables(1, self.globalsVarFilter)
+        self.debugServer.remoteClientVariables(
+            debuggerId, 1, self.globalsVarFilter)
         # the local variables are requested once we have received the globals
         
-    def __clientVariables(self, scope, variables):
+    def __clientVariables(self, scope, variables, debuggerId):
         """
         Private method to write the clients variables to the user interface.
         
-        @param scope scope of the variables (-1 = empty global, 1 = global,
+        @param scope scope of the variables (-1 = empty locals, 1 = global,
             0 = local)
+        @type int
         @param variables the list of variables from the client
+        @type list
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.ui.activateDebugViewer()
-        if scope > 0:
-            self.debugViewer.showVariables(variables, True)
-            if scope == 1:
-                # now get the local variables
-                self.debugServer.remoteClientVariables(0, self.localsVarFilter)
-        elif scope == 0:
-            self.debugViewer.showVariables(variables, False)
-        elif scope == -1:
-            vlist = [(self.tr('No locals available.'), '', '')]
-            self.debugViewer.showVariables(vlist, False)
+        if debuggerId == self.getSelectedDebuggerId():
+            self.ui.activateDebugViewer()
+            if scope > 0:
+                self.debugViewer.showVariables(variables, True)
+                if scope == 1:
+                    # now get the local variables
+                    self.debugServer.remoteClientVariables(
+                        self.getSelectedDebuggerId(),
+                        0, self.localsVarFilter)
+            elif scope == 0:
+                self.debugViewer.showVariables(variables, False)
+            elif scope == -1:
+                vlist = [(self.tr('No locals available.'), '', '')]
+                self.debugViewer.showVariables(vlist, False)
         
-    def __clientVariable(self, scope, variables):
+    def __clientVariable(self, scope, variables, debuggerId):
         """
         Private method to write the contents of a clients classvariable to
         the user interface.
         
-        @param scope scope of the variables (-1 = empty global, 1 = global,
+        @param scope scope of the variables (-1 = empty locals, 1 = global,
             0 = local)
-        @param variables the list of members of a classvariable from the client
+        @type int
+        @param variables the list of variables from the client
+        @type list
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.ui.activateDebugViewer()
-        if scope == 1:
-            self.debugViewer.showVariable(variables, True)
-        elif scope == 0:
-            self.debugViewer.showVariable(variables, False)
+        if debuggerId == self.getSelectedDebuggerId():
+            self.ui.activateDebugViewer()
+            if scope == 1:
+                self.debugViewer.showVariable(variables, True)
+            elif scope == 0:
+                self.debugViewer.showVariable(variables, False)
             
     def __clientBreakConditionError(self, filename, lineno):
         """
@@ -2242,7 +2279,8 @@
         """
         self.lastAction = 0
         self.__enterRemote()
-        self.debugServer.remoteContinue()
+        self.debugServer.remoteContinue(self.getSelectedDebuggerId())
+        self.__getThreadList()
 
     def __specialContinue(self):
         """
@@ -2250,7 +2288,8 @@
         """
         self.lastAction = 2
         self.__enterRemote()
-        self.debugServer.remoteContinue(1)
+        self.debugServer.remoteContinue(self.getSelectedDebuggerId(), 1)
+        self.__getThreadList()
 
     def __step(self):
         """
@@ -2258,7 +2297,7 @@
         """
         self.lastAction = 1
         self.__enterRemote()
-        self.debugServer.remoteStep()
+        self.debugServer.remoteStep(self.getSelectedDebuggerId())
 
     def __stepOver(self):
         """
@@ -2266,7 +2305,8 @@
         """
         self.lastAction = 2
         self.__enterRemote()
-        self.debugServer.remoteStepOver()
+        self.debugServer.remoteStepOver(self.getSelectedDebuggerId())
+        self.__getThreadList()
 
     def __stepOut(self):
         """
@@ -2274,7 +2314,8 @@
         """
         self.lastAction = 3
         self.__enterRemote()
-        self.debugServer.remoteStepOut()
+        self.debugServer.remoteStepOut(self.getSelectedDebuggerId())
+        self.__getThreadList()
 
     def __stepQuit(self):
         """
@@ -2282,7 +2323,7 @@
         """
         self.lastAction = 4
         self.__enterRemote()
-        self.debugServer.remoteStepQuit()
+        self.debugServer.remoteStepQuit(self.getSelectedDebuggerId())
         self.__resetUI()
 
     def __runToCursor(self):
@@ -2294,8 +2335,10 @@
         line = aw.getCursorPosition()[0] + 1
         self.__enterRemote()
         self.debugServer.remoteBreakpoint(
+            self.getSelectedDebuggerId(),
             aw.getFileName(), line, 1, None, 1)
-        self.debugServer.remoteContinue()
+        self.debugServer.remoteContinue(self.getSelectedDebuggerId())
+        self.__getThreadList()
 
     def __moveInstructionPointer(self):
         """
@@ -2304,7 +2347,7 @@
         self.lastAction = 0
         aw = self.viewmanager.activeWindow()
         line = aw.getCursorPosition()[0] + 1
-        self.debugServer.remoteMoveIP(line)
+        self.debugServer.remoteMoveIP(self.getSelectedDebuggerId(), line)
 
     def __enterRemote(self):
         """
@@ -2325,3 +2368,24 @@
         @return list of all actions (list of E5Action)
         """
         return self.actions[:]
+    
+    def getDebuggerData(self, debuggerId):
+        """
+        Public method to refresh the debugging data of a specific debugger
+        backend.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
+        """
+        self.debugServer.remoteClientStack(debuggerId)
+        self.__getThreadList(debuggerId)
+        self.__getClientVariables(debuggerId)
+    
+    def getSelectedDebuggerId(self):
+        """
+        Public method to get the currently selected debugger ID.
+        
+        @return selected debugger ID
+        @rtype str
+        """
+        return self.debugViewer.getSelectedDebuggerId()
--- a/eric6/Debugger/DebugViewer.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/Debugger/DebugViewer.py	Sun Feb 02 12:01:27 2020 +0100
@@ -23,10 +23,10 @@
 
 import os
 
-from PyQt5.QtCore import pyqtSignal
+from PyQt5.QtCore import pyqtSignal, Qt
 from PyQt5.QtWidgets import (
     QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QSizePolicy, QPushButton,
-    QComboBox, QLabel, QTreeWidget, QTreeWidgetItem, QHeaderView
+    QComboBox, QLabel, QTreeWidget, QTreeWidgetItem, QHeaderView, QFrame
 )
 
 import UI.PixmapCache
@@ -62,12 +62,31 @@
         self.debugServer = debugServer
         self.debugUI = None
         
-        self.setWindowIcon(UI.PixmapCache.getIcon("eric.png"))
+        self.setWindowIcon(UI.PixmapCache.getIcon("eric"))
         
         self.__mainLayout = QVBoxLayout()
         self.__mainLayout.setContentsMargins(0, 0, 0, 0)
         self.setLayout(self.__mainLayout)
         
+        # add the viewer showing the connected debug backends
+        self.__debuggersLayout = QHBoxLayout()
+        self.__debuggersLayout.addWidget(QLabel(self.tr("Debuggers:")))
+        self.__debuggersCombo = QComboBox(self)
+        self.__debuggersCombo.setSizePolicy(
+            QSizePolicy.Expanding, QSizePolicy.Fixed)
+        self.__debuggersLayout.addWidget(self.__debuggersCombo)
+        self.__mainLayout.addLayout(self.__debuggersLayout)
+        
+        self.__debuggersCombo.currentIndexChanged[str].connect(
+            self.__debuggerSelected)
+        
+        # add a line to separate debugger selector from debugger specific parts
+        hline = QFrame(self)
+        hline.setFrameStyle(QFrame.Sunken)
+        hline.setFrameShape(QFrame.HLine)
+        self.__mainLayout.addWidget(hline)
+        
+        # add the tab widget containing various debug related views
         self.__tabWidget = E5TabWidget()
         self.__mainLayout.addWidget(self.__tabWidget)
         
@@ -105,7 +124,7 @@
         
         index = self.__tabWidget.addTab(
             self.glvWidget,
-            UI.PixmapCache.getIcon("globalVariables.png"), '')
+            UI.PixmapCache.getIcon("globalVariables"), '')
         self.__tabWidget.setTabToolTip(index, self.globalsViewer.windowTitle())
         
         self.setGlobalsFilterButton.clicked.connect(
@@ -159,7 +178,7 @@
         
         index = self.__tabWidget.addTab(
             self.lvWidget,
-            UI.PixmapCache.getIcon("localVariables.png"), '')
+            UI.PixmapCache.getIcon("localVariables"), '')
         self.__tabWidget.setTabToolTip(index, self.localsViewer.windowTitle())
         
         self.sourceButton.clicked.connect(self.__showSource)
@@ -177,7 +196,7 @@
         self.callStackViewer = CallStackViewer(self.debugServer)
         index = self.__tabWidget.addTab(
             self.callStackViewer,
-            UI.PixmapCache.getIcon("step.png"), "")
+            UI.PixmapCache.getIcon("step"), "")
         self.__tabWidget.setTabToolTip(
             index, self.callStackViewer.windowTitle())
         self.callStackViewer.sourceFile.connect(self.sourceFile)
@@ -186,10 +205,10 @@
         
         from .CallTraceViewer import CallTraceViewer
         # add the call trace viewer
-        self.callTraceViewer = CallTraceViewer(self.debugServer)
+        self.callTraceViewer = CallTraceViewer(self.debugServer, self)
         index = self.__tabWidget.addTab(
             self.callTraceViewer,
-            UI.PixmapCache.getIcon("callTrace.png"), "")
+            UI.PixmapCache.getIcon("callTrace"), "")
         self.__tabWidget.setTabToolTip(
             index, self.callTraceViewer.windowTitle())
         self.callTraceViewer.sourceFile.connect(self.sourceFile)
@@ -200,7 +219,7 @@
         self.breakpointViewer.setModel(self.debugServer.getBreakPointModel())
         index = self.__tabWidget.addTab(
             self.breakpointViewer,
-            UI.PixmapCache.getIcon("breakpoints.png"), '')
+            UI.PixmapCache.getIcon("breakpoints"), '')
         self.__tabWidget.setTabToolTip(
             index, self.breakpointViewer.windowTitle())
         self.breakpointViewer.sourceFile.connect(self.sourceFile)
@@ -211,7 +230,7 @@
         self.watchpointViewer.setModel(self.debugServer.getWatchPointModel())
         index = self.__tabWidget.addTab(
             self.watchpointViewer,
-            UI.PixmapCache.getIcon("watchpoints.png"), '')
+            UI.PixmapCache.getIcon("watchpoints"), '')
         self.__tabWidget.setTabToolTip(
             index, self.watchpointViewer.windowTitle())
         
@@ -220,7 +239,7 @@
         self.exceptionLogger = ExceptionLogger()
         index = self.__tabWidget.addTab(
             self.exceptionLogger,
-            UI.PixmapCache.getIcon("exceptions.png"), '')
+            UI.PixmapCache.getIcon("exceptions"), '')
         self.__tabWidget.setTabToolTip(
             index, self.exceptionLogger.windowTitle())
         
@@ -245,11 +264,33 @@
         self.currentStack = None
         self.framenr = 0
         
-        self.debugServer.clientStack.connect(self.handleClientStack)
-        
         self.__autoViewSource = Preferences.getDebugger("AutoViewSourceCode")
         self.sourceButton.setVisible(not self.__autoViewSource)
         
+        # connect somer debug server signals
+        self.debugServer.clientStack.connect(
+            self.handleClientStack)
+        self.debugServer.clientThreadList.connect(
+            self.showThreadList)
+        self.debugServer.clientDebuggerIds.connect(
+            self.showDebuggersList)
+        self.debugServer.passiveDebugStarted.connect(
+            self.handleDebuggingStarted)
+        self.debugServer.clientLine.connect(
+            self.__clientLine)
+        self.debugServer.clientSyntaxError.connect(
+            self.__clientSyntaxError)
+        self.debugServer.clientException.connect(
+            self.__clientException)
+        
+        self.debugServer.clientException.connect(
+            self.exceptionLogger.addException)
+        self.debugServer.passiveDebugStarted.connect(
+            self.exceptionLogger.debuggingStarted)
+        
+        self.debugServer.clientLine.connect(
+            self.breakpointViewer.highlightBreakpoint)
+    
     def handlePreferencesChanged(self):
         """
         Public slot to handle the preferencesChanged signal.
@@ -264,9 +305,15 @@
         @param debugUI reference to the DebugUI object (DebugUI)
         """
         self.debugUI = debugUI
-        self.debugUI.clientStack.connect(self.handleClientStack)
         self.callStackViewer.setDebugger(debugUI)
         
+        # connect some debugUI signals
+        self.debugUI.clientStack.connect(self.handleClientStack)
+        self.debugUI.debuggingStarted.connect(
+            self.exceptionLogger.debuggingStarted)
+        self.debugUI.debuggingStarted.connect(
+            self.handleDebuggingStarted)
+    
     def handleResetUI(self):
         """
         Public method to reset the SBVviewer.
@@ -281,6 +328,7 @@
         self.__threadList.clear()
         self.__tabWidget.setCurrentWidget(self.glvWidget)
         self.breakpointViewer.handleResetUI()
+        self.__debuggersCombo.clear()
         
     def initCallStackViewer(self, projectMode):
         """
@@ -351,24 +399,88 @@
         else:
             self.__tabWidget.setCurrentWidget(self.lvWidget)
         
-    def handleClientStack(self, stack):
+    def handleClientStack(self, stack, debuggerId):
         """
         Public slot to show the call stack of the program being debugged.
         
         @param stack list of tuples with call stack data (file name,
             line number, function name, formatted argument/values list)
+        @type list of tuples of (str, str, str, str)
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        block = self.stackComboBox.blockSignals(True)
-        self.framenr = 0
-        self.stackComboBox.clear()
-        self.currentStack = stack
-        self.sourceButton.setEnabled(len(stack) > 0)
-        for s in stack:
-            # just show base filename to make it readable
-            s = (os.path.basename(s[0]), s[1], s[2])
-            self.stackComboBox.addItem('{0}:{1}:{2}'.format(*s))
-        self.stackComboBox.blockSignals(block)
+        if debuggerId == self.__debuggersCombo.currentText():
+            block = self.stackComboBox.blockSignals(True)
+            self.framenr = 0
+            self.stackComboBox.clear()
+            self.currentStack = stack
+            self.sourceButton.setEnabled(len(stack) > 0)
+            for s in stack:
+                # just show base filename to make it readable
+                s = (os.path.basename(s[0]), s[1], s[2])
+                self.stackComboBox.addItem('{0}:{1}:{2}'.format(*s))
+            self.stackComboBox.blockSignals(block)
+    
+    def __clientLine(self, fn, line, debuggerId):
+        """
+        Private method to handle a change to the current line.
+        
+        @param fn filename
+        @type str
+        @param line linenumber
+        @type int
+        @param debuggerId ID of the debugger backend
+        @type str
+        """
+        if debuggerId:
+            index = self.__debuggersCombo.findText(debuggerId, Qt.MatchExactly)
+            if index >= 0:
+                self.__debuggersCombo.setItemIcon(
+                    index, UI.PixmapCache.getIcon("exceptions"))
+    
+    # TODO: Refactor the icon setting code into a method
+    def __clientSyntaxError(self, message, filename, lineNo, characterNo,
+                            debuggerId):
+        """
+        Private method to handle a syntax error in the debugged program.
         
+        @param message message of the syntax error
+        @type str
+        @param filename translated filename of the syntax error position
+        @type str
+        @param lineNo line number of the syntax error position
+        @type int
+        @param characterNo character number of the syntax error position
+        @type int
+        @param debuggerId ID of the debugger backend
+        @type str
+        """
+        if debuggerId:
+            index = self.__debuggersCombo.findText(debuggerId, Qt.MatchExactly)
+            if index >= 0:
+                self.__debuggersCombo.setItemIcon(
+                    index, UI.PixmapCache.getIcon("syntaxError22"))
+    
+    def __clientException(self, exceptionType, exceptionMessage, stackTrace,
+                          debuggerId):
+        """
+        Private method to handle an exception of the debugged program.
+        
+        @param exceptionType type of exception raised
+        @type str
+        @param exceptionMessage message given by the exception
+        @type (str
+        @param stackTrace list of stack entries
+        @type list of str
+        @param debuggerId ID of the debugger backend
+        @type str
+        """
+        if debuggerId:
+            index = self.__debuggersCombo.findText(debuggerId, Qt.MatchExactly)
+            if index >= 0:
+                self.__debuggersCombo.setItemIcon(
+                    index, UI.PixmapCache.getIcon("exceptions"))
+    
     def setVariablesFilter(self, globalsFilter, localsFilter):
         """
         Public slot to set the local variables filter.
@@ -396,12 +508,14 @@
         
         @param frmnr frame number (0 is the current frame) (int)
         """
-        self.framenr = frmnr
-        if self.debugServer.isDebugging():
-            self.debugServer.remoteClientVariables(0, self.localsFilter, frmnr)
-        
-        if self.__autoViewSource:
-            self.__showSource()
+        if frmnr >= 0:
+            self.framenr = frmnr
+            if self.debugServer.isDebugging():
+                self.debugServer.remoteClientVariables(
+                    self.getSelectedDebuggerId(), 0, self.localsFilter, frmnr)
+            
+            if self.__autoViewSource:
+                self.__showSource()
         
     def setGlobalsFilter(self):
         """
@@ -410,7 +524,8 @@
         if self.debugServer.isDebugging():
             filterStr = self.globalsFilterEdit.text()
             self.debugServer.remoteClientSetFilter(1, filterStr)
-            self.debugServer.remoteClientVariables(2, self.globalsFilter)
+            self.debugServer.remoteClientVariables(
+                self.getSelectedDebuggerId(), 2, self.globalsFilter)
         
     def setLocalsFilter(self):
         """
@@ -421,7 +536,8 @@
             self.debugServer.remoteClientSetFilter(0, filterStr)
             if self.currentStack:
                 self.debugServer.remoteClientVariables(
-                    0, self.localsFilter, self.framenr)
+                    self.getSelectedDebuggerId(), 0, self.localsFilter,
+                    self.framenr)
         
     def handleDebuggingStarted(self):
         """
@@ -449,35 +565,68 @@
         """
         self.__tabWidget.setCurrentWidget(widget)
         
-    def showThreadList(self, currentID, threadList):
+    def showThreadList(self, currentID, threadList, debuggerId):
         """
         Public method to show the thread list.
         
-        @param currentID id of the current thread (integer)
+        @param currentID id of the current thread
+        @type int
         @param threadList list of dictionaries containing the thread data
+        @type list of dict
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        citm = None
+        debugStatus = -1    # i.e. running
         
-        self.__threadList.clear()
-        for thread in threadList:
-            if thread['broken']:
-                state = self.tr("waiting at breakpoint")
-            else:
-                state = self.tr("running")
-            itm = QTreeWidgetItem(self.__threadList,
-                                  ["{0:d}".format(thread['id']),
-                                   thread['name'], state])
-            if thread['id'] == currentID:
-                citm = itm
+        if debuggerId == self.__debuggersCombo.currentText():
+            citm = None
+            
+            self.__threadList.clear()
+            for thread in threadList:
+                if thread.get('except', False):
+                    state = self.tr("waiting at exception")
+                    icon = "exceptions"
+                    debugStatus = 1
+                elif thread['broken']:
+                    state = self.tr("waiting at breakpoint")
+                    icon = "mediaPlaybackPause"
+                    if debugStatus < 1:
+                        debugStatus = 0
+                else:
+                    state = self.tr("running")
+                    icon = "mediaPlaybackStart"
+                itm = QTreeWidgetItem(self.__threadList,
+                                      ["{0:d}".format(thread['id']),
+                                       thread['name'], state])
+                itm.setIcon(0, UI.PixmapCache.getIcon(icon))
+                if thread['id'] == currentID:
+                    citm = itm
+            
+            self.__threadList.header().resizeSections(
+                QHeaderView.ResizeToContents)
+            self.__threadList.header().setStretchLastSection(True)
+            
+            if citm:
+                self.__doThreadListUpdate = False
+                self.__threadList.setCurrentItem(citm)
+                self.__doThreadListUpdate = True
+        else:
+            for thread in threadList:
+                if thread.get('except', False):
+                    debugStatus = 1
+                elif thread['broken']:
+                    if debugStatus < 1:
+                        debugStatus = 0
         
-        self.__threadList.header().resizeSections(QHeaderView.ResizeToContents)
-        self.__threadList.header().setStretchLastSection(True)
-        
-        if citm:
-            self.__doThreadListUpdate = False
-            self.__threadList.setCurrentItem(citm)
-            self.__doThreadListUpdate = True
-        
+        if debugStatus == -1:
+            icon = "mediaPlaybackStart"
+        elif debugStatus == 0:
+            icon = "mediaPlaybackPause"
+        else:
+            icon = "exceptions"
+        self.__debuggersCombo.setItemIcon(self.__debuggersCombo.currentIndex(),
+                                          UI.PixmapCache.getIcon(icon))
+    
     def __threadSelected(self, current, previous):
         """
         Private slot to handle the selection of a thread in the thread list.
@@ -488,7 +637,7 @@
         """
         if current is not None and self.__doThreadListUpdate:
             tid = int(current.text(0))
-            self.debugServer.remoteSetThread(tid)
+            self.debugServer.remoteSetThread(self.getSelectedDebuggerId(), tid)
     
     def __callStackFrameSelected(self, frameNo):
         """
@@ -499,3 +648,48 @@
         """
         if frameNo >= 0:
             self.stackComboBox.setCurrentIndex(frameNo)
+    
+    def __debuggerSelected(self, debuggerId):
+        """
+        Private slot to handle the selection of a debugger backend ID.
+        
+        @param debuggerId ID of the selected debugger backend
+        @type str
+        """
+        if debuggerId:
+            self.globalsViewer.handleResetUI()
+            self.localsViewer.handleResetUI()
+            self.currentStack = None
+            self.stackComboBox.clear()
+            self.__threadList.clear()
+            self.callStackViewer.clear()
+            
+            self.debugUI.getDebuggerData(debuggerId)
+    
+    def showDebuggersList(self, debuggerIds):
+        """
+        Public slot to receive the list of debugger backend IDs.
+        
+        @param debuggerIds list of debugger backend IDs
+        @type list of str
+        """
+        block = self.__debuggersCombo.blockSignals(True)
+        
+        currentDebuggerId = self.__debuggersCombo.currentText()
+        self.__debuggersCombo.clear()
+        self.__debuggersCombo.addItems(debuggerIds)
+        if currentDebuggerId:
+            self.__debuggersCombo.setCurrentText(currentDebuggerId)
+        else:
+            self.__debuggersCombo.setCurrentIndex(0)
+        
+        self.__debuggersCombo.blockSignals(block)
+    
+    def getSelectedDebuggerId(self):
+        """
+        Public method to get the currently selected debugger ID.
+        
+        @return selected debugger ID
+        @rtype str
+        """
+        return self.__debuggersCombo.currentText()
--- a/eric6/Debugger/DebuggerInterfacePython.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/Debugger/DebuggerInterfacePython.py	Sun Feb 02 12:01:27 2020 +0100
@@ -58,8 +58,10 @@
         self.__variant = pythonVariant
         self.__startedVenv = ""
         
-        self.qsock = None
         self.queue = []
+        self.__master = None
+        self.__connections = {}
+        self.__pendingConnections = []
         
         # set default values for capabilities of clients
         self.clientCapabilities = ClientDefaultCapabilities
@@ -485,7 +487,7 @@
             self.__startedVenv = venvName
         
         return process, self.__isNetworked, interpreter
-
+    
     def getClientCapabilities(self):
         """
         Public method to retrieve the debug clients capabilities.
@@ -498,30 +500,83 @@
         """
         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
         """
-        Public slot to flush the queue.
+        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()
+            
+            self.debugServer.signalClientDebuggerIds(
+                sorted(self.__connections.keys()))
+            
+            if debuggerId == self.__master:
+                self.__flush()
+                self.debugServer.masterClientConnected()
+            
+            self.debugServer.initializeClient(debuggerId)
+    
+    def __socketDisconnected(self, sock):
         """
-        # Send commands that were waiting for the connection.
-        for cmd in self.queue:
-            self.__writeJsonCommandToSocket(cmd)
+        Private slot handling a socket disconnecting.
+        
+        @param sock reference to the disconnected socket
+        @type QTcpSocket
+        """
+        for debuggerId in self.__connections:
+            if self.__connections[debuggerId] is sock:
+                del self.__connections[debuggerId]
+                break
+        else:
+            if sock in self.__pendingConnections:
+                self.__pendingConnections.remove(sock)
+        
+        if not self.__connections:
+            # no active connections anymore => restart the backend
+            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.__connections[self.__master])
         
         self.queue = []
     
@@ -529,32 +584,53 @@
         """
         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):
         """
@@ -562,7 +638,9 @@
         
         @param env environment settings (dictionary)
         """
-        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):
@@ -592,7 +670,7 @@
             "traceInterpreter": traceInterpreter,
             "autofork": autoFork,
             "forkChild": forkChild,
-        })
+        }, self.__master)
     
     def remoteRun(self, fn, argv, wd, autoFork=False, forkChild=False):
         """
@@ -615,7 +693,7 @@
             "argv": Utilities.parseOptionString(argv),
             "autofork": autoFork,
             "forkChild": forkChild,
-        })
+        }, self.__master)
     
     def remoteCoverage(self, fn, argv, wd, erase=False):
         """
@@ -636,7 +714,7 @@
             "filename": fn,
             "argv": Utilities.parseOptionString(argv),
             "erase": erase,
-        })
+        }, self.__master)
 
     def remoteProfile(self, fn, argv, wd, erase=False):
         """
@@ -657,155 +735,236 @@
             "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.__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.__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.__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.__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.__sendJsonCommand("RequestContinue", {
             "special": special,
-        })
-
-    def remoteMoveIP(self, 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)
     
+    # TODO: add debuggerId
     def remoteRawInput(self, s):
         """
         Public method to send the raw input to the debugged program.
@@ -816,26 +975,44 @@
             "input": s,
         })
     
-    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", {})
+        self.__sendJsonCommand("RequestThreadList", {}, debuggerId)
         
-    def remoteSetThread(self, tid):
+    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
@@ -852,13 +1029,15 @@
             "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
@@ -878,8 +1057,9 @@
             "scope": scope,
             "filters": filterList,
             "maxSize": maxSize,
-        })
+        }, debuggerId)
     
+    # TODO: add debuggerId
     def remoteClientSetFilter(self, scope, filterStr):
         """
         Public method to set a variables filter list.
@@ -893,15 +1073,18 @@
             "filter": filterStr,
         })
     
-    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 remoteBanner(self):
         """
@@ -909,11 +1092,14 @@
         """
         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):
         """
@@ -1045,12 +1231,15 @@
                 "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()
             if self.codec is not None:
                 line = self.codec.toUnicode(qs)
             else:
@@ -1059,10 +1248,9 @@
             logging.debug("<Debug-Server> %s", line)
 ##            print("Server: ", line)          ##debug
             
-            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.
@@ -1070,6 +1258,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
         
@@ -1093,26 +1283,32 @@
         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
             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)
+                QTimer.singleShot(
+                    0, lambda: self.remoteContinue(params["debuggerId"]))
             else:
                 self.debugServer.signalClientLine(
-                    cf[0], int(cf[1]),
+                    cf[0], int(cf[1]), params["debuggerId"],
                     method == "ResponseStack")
-                self.debugServer.signalClientStack(params["stack"])
+                self.debugServer.signalClientStack(
+                    params["stack"], params["debuggerId"])
         
         elif method == "CallTrace":
             isCall = params["event"].lower() == "c"
@@ -1123,109 +1319,124 @@
                 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"],
+                    params["dbgclient"],
+                    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"])
         
+        # TODO: add debuggerId
         elif method == "RequestRaw":
             self.debugServer.signalClientRawInput(
                 params["prompt"], params["echo"])
         
+        # TODO: add debuggerId
         elif method == "ResponseBPConditionError":
             fn = self.translate(params["filename"], True)
             self.debugServer.signalClientBreakConditionError(
                 fn, params["line"])
         
+        # TODO: add debuggerId
         elif method == "ResponseClearBreakpoint":
             fn = self.translate(params["filename"], True)
             self.debugServer.signalClientClearBreak(fn, params["line"])
         
+        # TODO: add debuggerId
         elif method == "ResponseWatchConditionError":
             self.debugServer.signalClientWatchConditionError(
                 params["condition"])
         
+        # TODO: add debuggerId
         elif method == "ResponseClearWatch":
             self.debugServer.signalClientClearWatch(params["condition"])
         
         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"])
         
         elif method == "ResponseSyntax":
             self.debugServer.signalClientSyntaxError(
                 params["message"], self.translate(params["filename"], True),
-                params["linenumber"], params["characternumber"])
+                params["linenumber"], params["characternumber"],
+                params["debuggerId"])
         
         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["status"], params["message"], params["debuggerId"])
         
         elif method == "PassiveStartup":
             self.debugServer.passiveStartUp(
                 self.translate(params["filename"], True), params["exceptions"])
         
         elif method == "ResponseCompletion":
-            self.debugServer.signalClientCompletionList(
-                params["completions"], params["text"])
+            if params["debuggerId"] == self.__master:
+                # signal only for the master connection
+                self.debugServer.signalClientCompletionList(
+                    params["completions"], params["text"])
+        
+        ###################################################################
+        ## Unit test related stuff is not done with multi processing
+        ###################################################################
         
         elif method == "ResponseUTDiscover":
             self.debugServer.clientUtDiscovered(
@@ -1266,10 +1477,11 @@
             self.debugServer.clientUtTestSucceededUnexpected(
                 params["testname"], params["id"])
         
+        # TODO: add debuggerId
         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.
         
@@ -1277,6 +1489,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
         
@@ -1286,22 +1503,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 createDebuggerInterfacePython2(debugServer, passive):
--- a/eric6/Debugger/ExceptionLogger.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/Debugger/ExceptionLogger.py	Sun Feb 02 12:01:27 2020 +0100
@@ -87,25 +87,34 @@
         else:
             self.menu.popup(coord)
             
-    def addException(self, exceptionType, exceptionMessage, stackTrace):
+    def addException(self, exceptionType, exceptionMessage, stackTrace,
+                     debuggerId):
         """
         Public slot to handle the arrival of a new exception.
         
-        @param exceptionType type of exception raised (string)
-        @param exceptionMessage message given by the exception (string)
-        @param stackTrace list of stack entries.
+        @param exceptionType type of exception raised
+        @type str
+        @param exceptionMessage message given by the exception
+        @type str
+        @param stackTrace list of stack entries
+        @type list
+        @param debuggerId ID of the debugger backend
+        @type str
         """
         itm = QTreeWidgetItem(self)
         if exceptionType is None:
             itm.setText(
-                0, self.tr('An unhandled exception occured.'
-                           ' See the shell window for details.'))
+                0, self.tr('{0}: An unhandled exception occured.'
+                           ' See the shell window for details.')
+                .format(debuggerId))
             return
         
-        if exceptionMessage == '':
-            itm.setText(0, "{0}".format(exceptionType))
+        if not exceptionMessage:
+            itm.setText(0, self.tr("{0}: {1}").format(
+                debuggerId, exceptionType))
         else:
-            itm.setText(0, "{0}, {1}".format(exceptionType, exceptionMessage))
+            itm.setText(0, self.tr("{0}: {1}, {2}").format(
+                debuggerId, exceptionType, exceptionMessage))
         
         # now add the call stack, most recent call first
         for entry in stackTrace:
--- a/eric6/Debugger/VariablesViewer.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/Debugger/VariablesViewer.py	Sun Feb 02 12:01:27 2020 +0100
@@ -792,7 +792,9 @@
         # step 4: request the variable from the debugger
         variablesFilter = e5App().getObject("DebugUI").variablesFilter(
             self.__globalScope)
+        # TODO: refactor this statement
         e5App().getObject("DebugServer").remoteClientVariable(
+            e5App().getObject("DebugUI").debugViewer.getSelectedDebuggerId(),
             self.__globalScope, variablesFilter, pathlist, self.framenr)
     
     def setExpanded(self, index, state):
--- a/eric6/QScintilla/Shell.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/QScintilla/Shell.py	Sun Feb 02 12:01:27 2020 +0100
@@ -862,7 +862,8 @@
         """
         Private method to handle the response from the debugger client.
         
-        @param more flag indicating that more user input is required (boolean)
+        @param more flag indicating that more user input is required
+        @type bool
         """
         if not self.inRawMode:
             self.inContinue = more
@@ -1842,7 +1843,10 @@
                 self.vm.quit()
                 return
             
-            self.dbs.remoteStatement(cmd)
+            self.dbs.remoteStatement(
+                e5App().getObject("DebugUI").getSelectedDebuggerId(),
+                cmd
+            )
             while self.inCommandExecution:
                 try:
                     QApplication.processEvents()
--- a/eric6/UI/UserInterface.py	Sun Feb 02 11:37:08 2020 +0100
+++ b/eric6/UI/UserInterface.py	Sun Feb 02 12:01:27 2020 +0100
@@ -427,10 +427,6 @@
         self.debuggerUI.resetUI.connect(self.viewmanager.handleResetUI)
         self.debuggerUI.resetUI.connect(self.debugViewer.handleResetUI)
         self.debuggerUI.resetUI.connect(self.__debuggingDone)
-        self.debuggerUI.debuggingStarted.connect(
-            self.debugViewer.exceptionLogger.debuggingStarted)
-        self.debuggerUI.debuggingStarted.connect(
-            self.debugViewer.handleDebuggingStarted)
         self.debuggerUI.debuggingStarted.connect(self.__programChange)
         self.debuggerUI.debuggingStarted.connect(self.__debuggingStarted)
         self.debuggerUI.compileForms.connect(
@@ -440,14 +436,6 @@
         self.debuggerUI.executeMake.connect(self.project.executeMake)
         self.debuggerUI.appendStdout.connect(self.appendToStdout)
         
-        debugServer.passiveDebugStarted.connect(
-            self.debugViewer.exceptionLogger.debuggingStarted)
-        debugServer.passiveDebugStarted.connect(
-            self.debugViewer.handleDebuggingStarted)
-        debugServer.clientException.connect(
-            self.debugViewer.exceptionLogger.addException)
-        debugServer.clientLine.connect(
-            self.debugViewer.breakpointViewer.highlightBreakpoint)
         debugServer.clientProcessStdout.connect(self.appendToStdout)
         debugServer.clientProcessStderr.connect(self.appendToStderr)
         debugServer.appendStdout.connect(self.appendToStdout)

eric ide

mercurial