Merged with default branch. multi_processing

Sat, 22 Feb 2020 17:03:43 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 22 Feb 2020 17:03:43 +0100
branch
multi_processing
changeset 7428
27c55a3d0b89
parent 7424
9bb7d8b0f966 (diff)
parent 7427
362cd1b6f81a (current diff)
child 7563
b0d6b63f2843

Merged with default branch.

eric6.e4p file | annotate | diff | comparison | revisions
eric6/DebugClients/Python/DebugClientBase.py file | annotate | diff | comparison | revisions
eric6/DebugClients/Python/coverage/pickle2json.py file | annotate | diff | comparison | revisions
--- a/eric6.e4p	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6.e4p	Sat Feb 22 17:03:43 2020 +0100
@@ -49,7 +49,11 @@
     <Source>eric6/DebugClients/Python/DebugUtilities.py</Source>
     <Source>eric6/DebugClients/Python/DebugVariables.py</Source>
     <Source>eric6/DebugClients/Python/FlexCompleter.py</Source>
+    <Source>eric6/DebugClients/Python/ModuleLoader.py</Source>
+    <Source>eric6/DebugClients/Python/MultiProcessDebugExtension.py</Source>
     <Source>eric6/DebugClients/Python/PyProfile.py</Source>
+    <Source>eric6/DebugClients/Python/QProcessExtension.py</Source>
+    <Source>eric6/DebugClients/Python/SubprocessExtension.py</Source>
     <Source>eric6/DebugClients/Python/ThreadExtension.py</Source>
     <Source>eric6/DebugClients/Python/__init__.py</Source>
     <Source>eric6/DebugClients/Python/coverage/__init__.py</Source>
--- a/eric6/DebugClients/Python/AsyncFile.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/DebugClients/Python/AsyncFile.py	Sat Feb 22 17:03:43 2020 +0100
@@ -345,6 +345,7 @@
         
         cmd = prepareJsonCommand("ClientOutput", {
             "text": s,
+            "debuggerId": "",
         })
         self.wpending.append(cmd)
         self.flush()
--- a/eric6/DebugClients/Python/DebugBase.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/DebugClients/Python/DebugBase.py	Sat Feb 22 17:03:43 2020 +0100
@@ -14,7 +14,6 @@
 import inspect
 import ctypes
 import time
-from inspect import CO_GENERATOR
 
 from BreakpointWatch import Breakpoint, Watch
 
@@ -89,6 +88,7 @@
         self.skipFrames = 0
         
         self.isBroken = False
+        self.isException = False
         self.cFrame = None
         
         # current frame we are at
@@ -344,7 +344,7 @@
                 # skip the internal StopIteration exception (with no traceback)
                 # triggered by a subiterator run with the 'yield from'
                 # statement.
-                if not (frame.f_code.co_flags & CO_GENERATOR and
+                if not (frame.f_code.co_flags & inspect.CO_GENERATOR and
                         arg[0] is StopIteration and arg[2] is None):
                     self.user_exception(arg)
             # Stop at the StopIteration or GeneratorExit exception when the
@@ -352,7 +352,7 @@
             # command, or a next/until command at the last statement in the
             # generator before the exception.
             elif (self.stopframe and frame is not self.stopframe and
-                    self.stopframe.f_code.co_flags & CO_GENERATOR and
+                    self.stopframe.f_code.co_flags & inspect.CO_GENERATOR and
                     arg[0] in (StopIteration, GeneratorExit)):
                 self.user_exception(arg)
             return None
@@ -434,12 +434,14 @@
         
         @param cmd command / code to execute under debugger control
         @type str or CodeType
-        @keyparam globalsDict dictionary of global variables for cmd
+        @param globalsDict dictionary of global variables for cmd
+        @type dict
+        @param localsDict dictionary of local variables for cmd
         @type dict
-        @keyparam localsDict dictionary of local variables for cmd
-        @type dict
-        @keyparam debug flag if command should run under debugger control
+        @param debug flag if command should run under debugger control
         @type bool
+        @return exit code of the program
+        @rtype int
         """
         if globalsDict is None:
             import __main__
@@ -462,6 +464,7 @@
             exec(cmd, globalsDict, localsDict)
             atexit._run_exitfuncs()
             self._dbgClient.progTerminated(0)
+            exitcode = 0
         except SystemExit:
             atexit._run_exitfuncs()
             excinfo = sys.exc_info()
@@ -470,9 +473,11 @@
         except Exception:
             excinfo = sys.exc_info()
             self.user_exception(excinfo, True)
+            exitcode = 242
         finally:
             self.quitting = True
             sys.settrace(None)
+        return exitcode
 
     def _set_stopinfo(self, stopframe, returnframe):
         """
@@ -788,6 +793,8 @@
         self.isBroken = False
         self._dbgClient.unlockClient()
         
+        self._dbgClient.dumpThreadList()
+    
     def user_exception(self, excinfo, unhandled=False):
         """
         Public method reimplemented to report an exception to the debug server.
@@ -881,6 +888,7 @@
             self.stop_everywhere = False
         
         self.isBroken = True
+        self.isException = True
         
         stack = []
         if exctb:
@@ -904,11 +912,14 @@
         self.skipFrames = 0
         
         self.isBroken = False
+        self.isException = False
         stop_everywhere = self.stop_everywhere
         self.stop_everywhere = False
         self.eventPollFlag = False
         self._dbgClient.unlockClient()
         self.stop_everywhere = stop_everywhere
+        
+        self._dbgClient.dumpThreadList()
     
     def __extractExceptionName(self, exctype):
         """
@@ -925,7 +936,7 @@
             else:
                 return exctype
         else:
-            return str(exctype).replace("<class '", "").replace("'>", "")
+            return exctype.__name__
     
     def __extract_stack(self, exctb):
         """
@@ -975,6 +986,9 @@
             elif isinstance(code, int):
                 exitcode = code
                 message = ""
+            elif code is None:
+                exitcode = 0
+                message = ""
             else:
                 exitcode = 1
                 message = str(code)
--- a/eric6/DebugClients/Python/DebugClient.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/DebugClients/Python/DebugClient.py	Sat Feb 22 17:03:43 2020 +0100
@@ -10,14 +10,12 @@
 from DebugBase import DebugBase
 from DebugClientBase import DebugClientBase
 from ThreadExtension import ThreadExtension
+from ModuleLoader import ModuleLoader
 
 
 class DebugClient(DebugClientBase, DebugBase, ThreadExtension):
     """
     Class implementing the client side of the debugger.
-    
-    This variant of the debugger implements the standard debugger client
-    by subclassing all relevant base classes.
     """
     def __init__(self):
         """
@@ -29,7 +27,7 @@
         
         ThreadExtension.__init__(self)
         
-        self.variant = 'Standard'
+        self.__moduleLoader = ModuleLoader(self)
 
 # We are normally called by the debugger to execute directly.
 
--- a/eric6/DebugClients/Python/DebugClientBase.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/DebugClients/Python/DebugClientBase.py	Sat Feb 22 17:03:43 2020 +0100
@@ -15,11 +15,14 @@
 import traceback
 import os
 import json
-import imp
 import re
 import atexit
 import signal
 import time
+if sys.version_info >= (3, 4):
+    import types
+else:
+    import imp
 
 
 import DebugClientCapabilities
@@ -30,6 +33,7 @@
 from FlexCompleter import Completer
 from DebugUtilities import prepareJsonCommand
 from BreakpointWatch import Breakpoint, Watch
+##from MultiProcessDebugExtension import patchNewProcessFunctions
 
 if sys.version_info[0] == 2:
     from inspect import getargvalues, formatargvalues
@@ -79,25 +83,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'] = 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__
 
 ###############################################################################
 
@@ -198,7 +213,10 @@
         self.framenr = 0
         
         # The context to run the debugged program in.
-        self.debugMod = imp.new_module('__main__')
+        if sys.version_info >= (3, 4):
+            self.debugMod = types.ModuleType("__main__")
+        else:
+            self.debugMod = imp.new_module('__main__')
         self.debugMod.__dict__['__builtins__'] = __builtins__
 
         # The list of complete lines to execute.
@@ -214,6 +232,8 @@
         self.running = None
         self.test = None
         self.debugging = False
+        self.multiprocessSupport = False
+        self.noDebugList = []
         
         self.fork_auto = False
         self.fork_child = False
@@ -223,9 +243,9 @@
         self.errorstream = None
         self.pollingDisabled = False
         
-        self.callTraceEnabled = None
+        self.__debuggerId = ""
         
-        self.variant = 'You should not see this'
+        self.callTraceEnabled = None
         
         self.compile_command = codeop.CommandCompiler()
         
@@ -233,6 +253,8 @@
         self.defaultCoding = 'utf-8'
         self.__coding = self.defaultCoding
         self.noencoding = False
+        
+        self.startOptions = None
     
     def getCoding(self):
         """
@@ -306,6 +328,8 @@
             pass
 
         self.debugging = False
+        self.multiprocessSupport = False
+        self.noDebugList = []
         
         # make sure we close down our end of the socket
         # might be overkill as normally stdin, stdout and stderr
@@ -390,12 +414,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", {
@@ -413,7 +449,6 @@
             self.sendJsonCommand("ResponseBanner", {
                 "version": "Python {0}".format(sys.version),
                 "platform": socket.gethostname(),
-                "dbgclient": self.variant,
             })
         
         elif method == "RequestSetFilter":
@@ -456,6 +491,7 @@
             
             self.running = sys.argv[0]
             self.debugging = True
+            self.multiprocessSupport = params["multiprocess"]
             
             self.fork_auto = params["autofork"]
             self.fork_child = params["forkChild"]
@@ -800,6 +836,9 @@
         elif method == "RequestShutdown":
             self.sessionClose()
         
+        elif method == "RequestSetNoDebugList":
+            self.noDebugList = params["noDebug"][:]
+        
         elif method == "RequestCompletion":
             self.__completionList(params["text"])
         
@@ -880,8 +919,14 @@
                             discoveryStart, top_level_dir=top_level_dir)
                 else:
                     if params["filename"]:
-                        utModule = imp.load_source(
-                            params["testname"], params["filename"])
+                        if sys.version_info >= (3, 4):
+                            import importlib
+                            spec = importlib.util.spec_from_file_location(
+                                params["testname"], params["filename"])
+                            utModule = importlib.util.module_from_spec(spec)
+                        else:
+                            utModule = imp.load_source(
+                                params["testname"], params["filename"])
                     else:
                         utModule = None
                     if params["failed"]:
@@ -999,6 +1044,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 +1159,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.
@@ -1254,7 +1314,8 @@
         if self.errorstream in wrdy:
             self.writeReady(self.errorstream)
     
-    def connectDebugger(self, port, remoteAddress=None, redirect=True):
+    def connectDebugger(self, port, remoteAddress=None, redirect=True,
+                        name=""):
         """
         Public method to establish a session with the debugger.
         
@@ -1262,19 +1323,34 @@
         stdout and stderr and saves these file objects in case the application
         being debugged redirects them itself.
         
-        @param port the port number to connect to (int)
+        @param port the port number to connect to
+        @type int
         @param remoteAddress the network address of the debug server host
-            (string)
+        @type str
         @param redirect flag indicating redirection of stdin, stdout and
-            stderr (boolean)
+            stderr
+        @type bool
+        @param name name to be attached to the debugger ID
+        @type str
         """
         if remoteAddress is None:
             remoteAddress = "127.0.0.1"
         elif "@@i" in remoteAddress:
             remoteAddress = remoteAddress.split("@@i")[0]
         sock = socket.create_connection((remoteAddress, port))
-
-        self.readstream = AsyncFile(sock, sys.stdin.mode, sys.stdin.name)
+        
+        if not name:
+            name = "main"
+        self.__debuggerId = "{0}-{1}-{2}".format(
+            socket.gethostname(), os.getpid(), name
+        )
+        
+        name = sys.stdin.name
+        # Special case if in a multiprocessing.Process
+        if isinstance(name, int):
+            name = '<stdin>'
+        
+        self.readstream = AsyncFile(sock, sys.stdin.mode, name)
         self.writestream = AsyncFile(sock, sys.stdout.mode, sys.stdout.name)
         self.errorstream = AsyncFile(sock, sys.stderr.mode, sys.stderr.name)
         
@@ -1286,6 +1362,8 @@
         
         # attach to the main thread here
         self.attachThread(mainThread=True)
+        
+        self.sendDebuggerId(self.__debuggerId)
 
     def __unhandled_exception(self, exctype, excval, exctb):
         """
@@ -1425,17 +1503,23 @@
         elif not isinstance(status, int):
             message = str(status)
             status = 1
-
+        if message is None:
+            message = ""
+        
         if self.running:
             self.set_quit()
+            program = self.running
             self.running = None
             self.sendJsonCommand("ResponseExit", {
                 "status": status,
                 "message": message,
+                "program": program,
             })
         
         # reset coding
         self.__coding = self.defaultCoding
+        
+        self.sessionClose(False)
 
     def __dumpVariables(self, frmnr, scope, filterList):
         """
@@ -1945,32 +2029,45 @@
                 comp = completer(text, state)
             except Exception:
                 comp = None
-
+    
     def startDebugger(self, filename=None, host=None, port=None,
                       enableTrace=True, exceptions=True, tracePython=False,
-                      redirect=True):
+                      redirect=True, passive=True, multiprocessSupport=False):
         """
         Public method used to start the remote debugger.
         
-        @param filename the program to be debugged (string)
-        @param host hostname of the debug server (string)
-        @param port portnumber of the debug server (int)
-        @param enableTrace flag to enable the tracing function (boolean)
+        @param filename the program to be debugged
+        @type str
+        @param host hostname of the debug server
+        @type str
+        @param port portnumber of the debug server
+        @type int
+        @param enableTrace flag to enable the tracing function
+        @type bool
         @param exceptions flag to enable exception reporting of the IDE
-            (boolean)
+        @type bool
         @param tracePython flag to enable tracing into the Python library
-            (boolean)
+        @type bool
         @param redirect flag indicating redirection of stdin, stdout and
-            stderr (boolean)
+            stderr
+        @type bool
+        @param passive flag indicating a passive debugging session
+        @type bool
+        @param multiprocessSupport flag indicating to enable multiprocess
+            debugging support
+        @type bool
         """
-        global debugClient
         if host is None:
             host = os.getenv('ERICHOST', 'localhost')
         if port is None:
             port = os.getenv('ERICPORT', 42424)
         
         remoteAddress = self.__resolveHost(host)
-        self.connectDebugger(port, remoteAddress, redirect)
+        if filename is not None:
+            name = os.path.basename(filename)
+        else:
+            name = ""
+        self.connectDebugger(port, remoteAddress, redirect, name=name)
         if filename is not None:
             self.running = os.path.abspath(filename)
         else:
@@ -1980,8 +2077,9 @@
                 self.running = None
         if self.running:
             self.__setCoding(self.running)
-        self.passive = True
-        self.sendPassiveStartup(self.running, exceptions)
+        self.passive = passive
+        if passive:
+            self.sendPassiveStartup(self.running, exceptions)
         self.__interact()
         
         # setup the debugger variables
@@ -2003,7 +2101,8 @@
     
     def startProgInDebugger(self, progargs, wd='', host=None,
                             port=None, exceptions=True, tracePython=False,
-                            redirect=True):
+                            redirect=True, passive=True,
+                            multiprocessSupport=False):
         """
         Public method used to start the remote debugger.
         
@@ -2018,6 +2117,13 @@
             (boolean)
         @param redirect flag indicating redirection of stdin, stdout and
             stderr (boolean)
+        @param passive flag indicating a passive debugging session
+        @type bool
+        @param multiprocessSupport flag indicating to enable multiprocess
+            debugging support
+        @type bool
+        @return exit code of the debugged program
+        @rtype int
         """
         if host is None:
             host = os.getenv('ERICHOST', 'localhost')
@@ -2025,7 +2131,8 @@
             port = os.getenv('ERICPORT', 42424)
         
         remoteAddress = self.__resolveHost(host)
-        self.connectDebugger(port, remoteAddress, redirect)
+        name = os.path.basename(progargs[0])
+        self.connectDebugger(port, remoteAddress, redirect, name=name)
         
         self._fncache = {}
         self.dircache = []
@@ -2039,9 +2146,11 @@
         self.running = sys.argv[0]
         self.__setCoding(self.running)
         self.debugging = True
+        self.multiprocessSupport = multiprocessSupport
         
-        self.passive = True
-        self.sendPassiveStartup(self.running, exceptions)
+        self.passive = passive
+        if passive:
+            self.sendPassiveStartup(self.running, exceptions)
         self.__interact()
         
         self.attachThread(mainThread=True)
@@ -2058,9 +2167,9 @@
         code = self.__compileFileSource(self.running)
         if code:
             res = self.mainThread.run(code, self.debugMod.__dict__, debug=True)
-            self.progTerminated(res)
         else:
-            self.progTerminated(42)     # should not happen
+            res = 42        # should not happen
+        return res
 
     def run_call(self, scriptname, func, *args):
         """
@@ -2103,6 +2212,7 @@
                 time.sleep(3)
         return None
     
+    # TODO: add support for the '-m' python invocation => '--module'
     def main(self):
         """
         Public method implementing the main method.
@@ -2115,6 +2225,8 @@
             tracePython = False
             exceptions = True
             redirect = True
+            passive = True
+            multiprocess = False
             while args[0]:
                 if args[0] == '-h':
                     host = args[1]
@@ -2148,6 +2260,12 @@
                     self.fork_auto = True
                     self.fork_child = False
                     del args[0]
+                elif args[0] == '--no-passive':
+                    passive = False
+                    del args[0]
+                elif args[0] == '--multiprocess':
+                    multiprocess = True
+                    del args[0]
                 elif args[0] == '--':
                     del args[0]
                     break
@@ -2156,18 +2274,34 @@
             if not args:
                 print("No program given. Aborting!")
                 # __IGNORE_WARNING_M801__
+            elif "-m" in args:
+                print("Running module as a script is not supported. 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,
-                                         exceptions=exceptions,
-                                         tracePython=tracePython,
-                                         redirect=redirect)
+##                patchNewProcessFunctions(multiprocess, self)
+                res = self.startProgInDebugger(
+                    args, wd, host, port, exceptions=exceptions,
+                    tracePython=tracePython, redirect=redirect,
+                    passive=passive, multiprocessSupport=multiprocess
+                )
+                sys.exit(res)
         else:
             if sys.argv[1] == '--no-encoding':
                 self.noencoding = True
                 del sys.argv[1]
             
+            if sys.argv[1] == '--multiprocess':
+                self.multiprocessSupport = True
+                del sys.argv[1]
+            
             if sys.argv[1] == '':
                 del sys.argv[1]
             
@@ -2201,9 +2335,16 @@
                 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)
+##                patchNewProcessFunctions(self.multiprocessSupport, self)
                 self.__interact()
             else:
                 print("No network port given. Aborting...")
@@ -2226,6 +2367,13 @@
                 sys.settrace(None)
                 sys.setprofile(None)
                 self.sessionClose(False)
+##                (wd, host, port, exceptions, tracePython, redirect,
+##                 noencoding, fork_auto, fork_child) = self.startOptions
+##                self.startDebugger(sys.argv[0], host, port,
+##                                   exceptions=exceptions,
+##                                   tracePython=tracePython,
+##                                   redirect=redirect,
+##                                   passive=False)
         else:
             # parent
             if self.fork_child:
--- a/eric6/DebugClients/Python/DebugUtilities.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/DebugClients/Python/DebugUtilities.py	Sat Feb 22 17:03:43 2020 +0100
@@ -8,6 +8,9 @@
 """
 
 import json
+import os
+import traceback
+import sys
 
 #
 # Taken from inspect.py of Python 3.4
@@ -145,5 +148,178 @@
     }
     return json.dumps(commandDict) + '\n'
 
+###########################################################################
+## Things related to monkey patching below
+###########################################################################
+
+
+PYTHON_NAMES = ["python", "pypy"]
+
+
+def isWindowsPlatform():
+    """
+    Function to check, if this is a Windows platform.
+    
+    @return flag indicating Windows platform
+    @rtype bool
+    """
+    return sys.platform.startswith(("win", "cygwin"))
+
+
+def isExecutable(program):
+    """
+    Function to check, if the given program is executable.
+    
+    @param program program path to be checked
+    @type str
+    @return flag indicating an executable program
+    @rtype bool
+    """
+    return os.access(os.path.abspath(program), os.X_OK)
+
+
+def startsWithShebang(program):
+    """
+    Function to check, if the given program start with a Shebang line.
+    
+    @param program program path to be checked
+    @type str
+    @return flag indicating an existing and valid shebang line
+    @rtype bool
+    """
+    try:
+        with open(program) as f:
+            for line in f:
+                line = line.strip()
+                if line:
+                    for name in PYTHON_NAMES:
+                        if line.startswith('#!/usr/bin/env {0}'.format(name)):
+                            return True
+                    return False
+    except UnicodeDecodeError:
+        return False
+    except Exception:
+        traceback.print_exc()
+        return False
+
+
+def isPythonProgram(program):
+    """
+    Function to check, if the given program is a Python interpreter or
+    program.
+    
+    @param program program to be checked
+    @type str
+    @return flag indicating a Python interpreter or program
+    @rtype bool
+    """
+    if not program:
+        return False
+    
+    prog = os.path.basename(program).lower()
+    for pyname in PYTHON_NAMES:
+        if pyname in prog:
+            return True
+    
+    return (
+        not isWindowsPlatform() and
+        isExecutable(program) and
+        startsWithShebang(program)
+    )
+
+
+def patchArguments(debugClient, arguments, noRedirect=False):
+    """
+    Function to patch the arguments given to start a program in order to
+    execute it in our debugger.
+    
+    @param debugClient reference to the debug client object
+    @type DebugClient
+    @param arguments list of program arguments
+    @type list of str
+    @param noRedirect flag indicating to not redirect stdin and stdout
+    @type bool
+    @return modified argument list
+    @rtype list of str
+    """
+    args = list(arguments[:])    # create a copy of the arguments list
+    
+    # support for shebang line
+    program = os.path.basename(args[0]).lower()
+    for pyname in PYTHON_NAMES:
+        if pyname in program:
+            break
+    else:
+        if not isWindowsPlatform() and startsWithShebang(args[0]):
+            # insert our interpreter as first argument
+            args.insert(0, sys.executable)
+    
+    # check for -c or -m invocation => debugging not supported yet
+    if "-c" in args:
+        cm_position = args.index("-c")
+    elif "-m" in args:
+        cm_position = args.index("-m")
+    else:
+        cm_position = 0
+    if cm_position > 0:
+        # check if it belongs to the interpreter or program
+        for pos in range(1, len(args)):
+            if not args[pos].startswith("-"):
+                # first argument not starting with '-' is the program
+                found = True
+                break
+        else:
+            found = False
+        if found and cm_position < pos:
+            # it belongs to the interpreter
+            return arguments
+    
+    # extract list of interpreter arguments, i.e. all arguments before the
+    # first one not starting with '-'.
+    interpreter = args.pop(0)
+    interpreterArgs = []
+    while args:
+        if args[0].startswith("-"):
+            if args[0] in ("-W", "-X"):
+                # take two elements off the list
+                interpreterArgs.append(args.pop(0))
+                interpreterArgs.append(args.pop(0))
+            else:
+                interpreterArgs.append(args.pop(0))
+        else:
+            break
+    
+    (wd, host, port, exceptions, tracePython, redirect, noencoding
+     ) = debugClient.startOptions[:7]
+    
+    modifiedArguments = [interpreter]
+    modifiedArguments.extend(interpreterArgs)
+    modifiedArguments.extend([
+        os.path.join(os.path.dirname(__file__), "DebugClient.py"),
+        "-h", host,
+        "-p", str(port),
+        "--no-passive",
+    ])
+    
+    if wd:
+        modifiedArguments.extend(["-w", wd])
+    if not exceptions:
+        modifiedArguments.append("-e")
+    if tracePython:
+        modifiedArguments.append("-t")
+    if noRedirect or not redirect:
+        modifiedArguments.append("-n")
+    if noencoding:
+        modifiedArguments.append("--no-encoding")
+    if debugClient.multiprocessSupport:
+        modifiedArguments.append("--multiprocess")
+    modifiedArguments.append("--")
+    # end the arguments for DebugClient
+    
+    # append the arguments for the program to be debugged
+    modifiedArguments.extend(args)
+    
+    return modifiedArguments
+
 #
 # eflag: noqa = M702
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/DebugClients/Python/ModuleLoader.py	Sat Feb 22 17:03:43 2020 +0100
@@ -0,0 +1,186 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing an import hook patching modules to support debugging.
+"""
+
+import sys
+import importlib
+
+from QProcessExtension import patchQProcess
+from SubprocessExtension import patchSubprocess
+
+
+class ModuleLoader(object):
+    """
+    Class implementing an import hook patching modules to support debugging.
+    """
+    def __init__(self, debugClient):
+        """
+        Constructor
+        
+        @param debugClient reference to the debug client object
+        @type DebugClient
+        """
+        self.__dbgClient = debugClient
+        
+        self.__enableImportHooks = True
+        
+        # reset already imported thread module to apply hooks at next import
+        for moduleName in ("thread", "_thread", "threading"):
+            if moduleName in sys.modules:
+                del sys.modules[moduleName]
+        
+        self.__modulesToPatch = (
+            'thread', '_thread', 'threading',
+            'greenlet',
+            'subprocess',
+            'PyQt4.QtCore', 'PyQt5.QtCore',
+            'PySide.QtCore', 'PySide2.QtCore',
+        )
+        
+        sys.meta_path.insert(0, self)
+    
+    def __loadModule(self, fullname):
+        """
+        Private method to load a module.
+        
+        @param fullname name of the module to be loaded
+        @type str
+        @return reference to the loaded module
+        @rtype module
+        """
+        module = importlib.import_module(fullname)
+        sys.modules[fullname] = module
+        
+        ## Add hook for _thread.start_new_thread
+        if (
+            fullname in ('thread', '_thread') and
+            not hasattr(module, 'eric6_patched')
+        ):
+            module.eric6_patched = True
+            self.__dbgClient.patchPyThread(module)
+        
+        ## Add hook for threading.run()
+        elif (
+            fullname == "threading" and
+            not hasattr(module, 'eric6_patched')
+        ):
+            module.eric6_patched = True
+            self.__dbgClient.patchPyThreading(module)
+        
+        ## greenlet support
+        elif (
+            fullname == 'greenlet' and
+            not hasattr(module, 'eric6_patched')
+        ):
+            if self.__dbgClient.patchGreenlet(module):
+                module.eric6_patched = True
+        
+        ## Add hook for subprocess.Popen()
+        elif (
+            fullname == 'subprocess' and
+            not hasattr(module, 'eric6_patched')
+        ):
+            module.eric6_patched = True
+            patchSubprocess(module, self.__dbgClient)
+            
+        ## Add hook for *.QThread and *.QProcess
+        elif (
+            fullname in ('PyQt4.QtCore', 'PyQt5.QtCore',
+                         'PySide.QtCore', 'PySide2.QtCore') and
+            not hasattr(module, 'eric6_patched')
+        ):
+            module.eric6_patched = True
+            self.__dbgClient.patchQThread(module)
+            patchQProcess(module, self.__dbgClient)
+        
+        self.__enableImportHooks = True
+        return module
+    
+    if sys.version_info >= (3, 4):
+        def find_spec(self, fullname, path, target=None):
+            """
+            Public method returning the module spec.
+            
+            @param fullname name of the module to be loaded
+            @type str
+            @param path path to resolve the module name
+            @type str
+            @param target module object to use for a more educated guess
+                about what spec to return
+            @type module
+            @return module spec object pointing to the module loader
+            @type ModuleSpec
+            """
+            if fullname in sys.modules or not self.__dbgClient.debugging:
+                return None
+            
+            if (
+                fullname in self.__modulesToPatch and
+                self.__enableImportHooks
+            ):
+                # Disable hook to be able to import original module
+                self.__enableImportHooks = False
+                return importlib.machinery.ModuleSpec(fullname, self)
+            
+            return None
+        
+        def create_module(self, spec):
+            """
+            Public method to create a module based on the passed in spec.
+            
+            @param spec module spec object for loading the module
+            @type ModuleSpec
+            @return created and patched module
+            @rtype module
+            """
+            return self.__loadModule(spec.name)
+        
+        def exec_module(self, module):
+            """
+            Public method to execute the created module.
+            
+            @param module module to be executed
+            @type module
+            """
+            pass
+    
+    else:
+        def find_module(self, fullname, path=None):
+            """
+            Public method returning the module loader.
+            
+            @param fullname name of the module to be loaded
+            @type str
+            @param path path to resolve the module name
+            @type str
+            @return module loader object
+            @rtype object
+            """
+            if fullname in sys.modules or not self.__dbgClient.debugging:
+                return None
+            
+            if (
+                fullname in self.__modulesToPatch and
+                self.__enableImportHooks
+            ):
+                # Disable hook to be able to import original module
+                self.__enableImportHooks = False
+                return self
+            
+            return None
+        
+        def load_module(self, fullname):
+            """
+            Public method to load a module.
+            
+            @param fullname name of the module to be loaded
+            @type str
+            @return reference to the loaded module
+            @rtype module
+            """
+            return self.__loadModule(fullname)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/DebugClients/Python/MultiProcessDebugExtension.py	Sat Feb 22 17:03:43 2020 +0100
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2002 - 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a function to patch the process creation functions to
+support multiprocess debugging.
+"""
+
+import sys
+
+from DebugUtilities import isWindowsPlatform, patchArguments, isPythonProgram
+
+_debugClient = None
+
+
+def patchModule(module, functionName, createFunction):
+    """
+    Function to replace a function of a module with a modified one.
+    
+    @param module reference to the module
+    @type types.ModuleType
+    @param functionName name of the function to be replaced
+    @type str
+    @param createFunction function creating the replacement
+    @type types.FunctionType
+    """
+    if hasattr(module, functionName):
+        originalName = 'original_' + functionName
+        if not hasattr(module, originalName):
+            setattr(module, originalName, getattr(module, functionName))
+            setattr(module, functionName, createFunction(originalName))
+
+
+def createExecl(originalName):
+    """
+    Function to patch the 'execl' process creation functions.
+    
+    <ul>
+        <li>os.execl(path, arg0, arg1, ...)</li>
+        <li>os.execle(path, arg0, arg1, ..., env)</li>
+        <li>os.execlp(file, arg0, arg1, ...)</li>
+        <li>os.execlpe(file, arg0, arg1, ..., env)</li>
+    </ul>
+    """
+    def newExecl(path, *args):
+        """
+        Function replacing the 'execl' functions of the os module.
+        """
+        print(args)
+        import os
+        if (
+            _debugClient.debugging and
+            _debugClient.multiprocessSupport
+        ):
+            args = patchArguments(_debugClient, args)
+            if isPythonProgram(args[0]):
+                path = args[0]
+            print(args)
+        return getattr(os, originalName)(path, *args)
+    return newExecl
+
+def patchNewProcessFunctions(multiprocessEnabled, debugClient):
+    """
+    Function to patch the process creation functions to support multiprocess
+    debugging.
+    
+    @param multiprocessEnabled flag indicating multiprocess support
+    @type bool
+    @param debugClient reference to the debug client object
+    @type DebugClient
+    """
+    global _debugClient
+    
+    if not multiprocessEnabled:
+        # return without patching
+        return
+    
+    import os
+    
+    # patch 'os.exec...()' functions
+    patchModule(os, "execl", createExecl)
+    patchModule(os, "execle", createExecl)
+    patchModule(os, "execlp", createExecl)
+    patchModule(os, "execlpe", createExecl)
+    
+    # TODO: implement patching of the various functions of the os module
+    
+    _debugClient = debugClient
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/DebugClients/Python/QProcessExtension.py	Sat Feb 22 17:03:43 2020 +0100
@@ -0,0 +1,199 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a function to patch QProcess to support debugging of the
+process.
+"""
+
+import os
+
+from DebugUtilities import isPythonProgram, startsWithShebang, patchArguments
+
+_debugClient = None
+
+
+def patchQProcess(module, debugClient):
+    """
+    Function to patch the QtCore module's QProcess.
+    
+    @param module reference to the imported module to be patched
+    @type module
+    @param debugClient reference to the debug client object
+    @type DebugClient
+    """     # __IGNORE_WARNING_D234__
+    global _debugClient
+    
+    # TODO: implement a process tracer
+    # i.e. report which processes are started
+    class QProcessWrapper(module.QProcess):
+        """
+        Wrapper class for *.QProcess.
+        """
+        _origQProcessStartDetached = module.QProcess.startDetached
+        
+        def __init__(self, parent=None):
+            """
+            Constructor
+            """
+            super(QProcessWrapper, self).__init__(parent)
+        
+        ###################################################################
+        ## Handling of 'start(...)' below
+        ###################################################################
+        
+        def start(self, *args, **kwargs):
+            """
+            Public method to start the process.
+            
+            This method patches the arguments such, that a debug client is
+            started for the Python script. A Python script is assumed, if the
+            program to be started contains the string 'python'.
+            
+            @param args arguments of the start call
+            @type list
+            @param kwargs keyword arguments of the start call
+            @type dict
+            """
+            if (
+                _debugClient.debugging and
+                _debugClient.multiprocessSupport and
+                ((len(args) >= 2 and isinstance(args[1], list)) or
+                 (len(args) == 1 and not isinstance(args[0], str)) or
+                 len(args) == 0)
+            ):
+                if len(args) >= 2:
+                    program = args[0]
+                    arguments = args[1]
+                    if len(args) > 2:
+                        mode = args[2]
+                    else:
+                        mode = module.QIODevice.ReadWrite
+                else:
+                    program = self.program()
+                    arguments = self.arguments()
+                    if len(args) == 1:
+                        mode = args[0]
+                    else:
+                        mode = module.QIODevice.ReadWrite
+                ok = isPythonProgram(program)
+                if ok:
+                    if startsWithShebang(program):
+                        scriptName = os.path.basename(program)
+                    else:
+                        scriptName = os.path.basename(arguments[0])
+                    if scriptName not in _debugClient.noDebugList:
+                        newArgs = patchArguments(
+                            _debugClient,
+                            [program] + arguments,
+                        )
+                        super(QProcessWrapper, self).start(
+                            newArgs[0], newArgs[1:], mode)
+                        return
+            
+            super(QProcessWrapper, self).start(*args, **kwargs)
+        
+        ###################################################################
+        ## Handling of 'startDetached(...)' below
+        ###################################################################
+        
+        def startDetached(self, *args, **kwargs):
+            """
+            Public method to start the detached process.
+            
+            This method patches the arguments such, that a debug client is
+            started for the Python script. A Python script is assumed, if the
+            program to be started contains the string 'python'.
+            
+            @param args arguments of the start call
+            @type list
+            @param kwargs keyword arguments of the start call
+            @type dict
+            @return flag indicating a successful start
+            @rtype bool
+            """
+            if isinstance(self, str):
+                return QProcessWrapper.startDetachedStatic(
+                    self, *args)
+            else:
+                return self.__startDetached(*args, **kwargs)
+        
+        def __startDetached(self, *args, **kwargs):
+            """
+            Private method to start the detached process.
+            
+            This method patches the arguments such, that a debug client is
+            started for the Python script. A Python script is assumed, if the
+            program to be started contains the string 'python'.
+            
+            @param args arguments of the start call
+            @type list
+            @param kwargs keyword arguments of the start call
+            @type dict
+            @return flag indicating a successful start
+            @rtype bool
+            """
+            if (
+                _debugClient.debugging and
+                _debugClient.multiprocessSupport and
+                len(args) == 0
+            ):
+                program = self.program()
+                arguments = self.arguments()
+                wd = self.workingDirectory()
+                
+                ok = isPythonProgram(program)
+                if ok:
+                    return QProcessWrapper.startDetachedStatic(
+                        program, arguments, wd)
+            
+            return super(QProcessWrapper, self).startDetached(*args, **kwargs)
+        
+        @staticmethod
+        def startDetachedStatic(*args, **kwargs):
+            """
+            Static method to start the detached process.
+            
+            This method patches the arguments such, that a debug client is
+            started for the Python script. A Python script is assumed, if the
+            program to be started contains the string 'python'.
+            
+            @param args arguments of the start call
+            @type list
+            @param kwargs keyword arguments of the start call
+            @type dict
+            @return flag indicating a successful start
+            @rtype bool
+            """
+            if (
+                _debugClient.debugging and
+                _debugClient.multiprocessSupport and
+                (len(args) >= 2 and isinstance(args[1], list))
+            ):
+                program = args[0]
+                arguments = args[1]
+                if len(args) >= 3:
+                    wd = args[2]
+                else:
+                    wd = ""
+                ok = isPythonProgram(program)
+                if ok:
+                    if startsWithShebang(program):
+                        scriptName = os.path.basename(program)
+                    else:
+                        scriptName = os.path.basename(arguments[0])
+                    if scriptName not in _debugClient.noDebugList:
+                        newArgs = patchArguments(
+                            _debugClient,
+                            [program] + arguments,
+                        )
+                        return QProcessWrapper._origQProcessStartDetached(
+                            newArgs[0], newArgs[1:], wd)
+            
+            return QProcessWrapper._origQProcessStartDetached(
+                *args, **kwargs)
+    
+    _debugClient = debugClient
+    module.QProcess = QProcessWrapper
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/DebugClients/Python/SubprocessExtension.py	Sat Feb 22 17:03:43 2020 +0100
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a function to patch subprocess.Popen to support debugging
+of the process.
+"""
+
+import os
+import shlex
+
+from DebugUtilities import isPythonProgram, startsWithShebang, patchArguments
+
+_debugClient = None
+
+
+def patchSubprocess(module, debugClient):
+    """
+    Function to patch the subprocess module.
+    
+    @param module reference to the imported module to be patched
+    @type module
+    @param debugClient reference to the debug client object
+    @type DebugClient
+    """     # __IGNORE_WARNING_D234__
+    global _debugClient
+    
+    # TODO: implement a process tracer
+    # i.e. report which processes are started
+    class PopenWrapper(module.Popen):
+        """
+        Wrapper class for subprocess.Popen.
+        """
+        def __init__(self, arguments, *args, **kwargs):
+            """
+            Constructor
+            
+            @param arguments command line arguments for the new process
+            @type list of str or str
+            @param args constructor arguments of Popen
+            @type list
+            @param kwargs constructor keword only arguments of Popen
+            @type dict
+            """
+            if (
+                _debugClient.debugging and
+                _debugClient.multiprocessSupport and
+                isinstance(arguments, (str, list))
+            ):
+                if isinstance(arguments, str):
+                    # convert to arguments list
+                    arguments = shlex.split(arguments)
+                else:
+                    # create a copy of the arguments
+                    arguments = arguments[:]
+                ok = isPythonProgram(arguments[0])
+                if ok:
+                    if startsWithShebang(arguments[0]):
+                        scriptName = os.path.basename(arguments[0])
+                    else:
+                        scriptName = os.path.basename(arguments[0])
+                    if scriptName not in _debugClient.noDebugList:
+                        arguments = patchArguments(
+                            _debugClient, arguments, noRedirect=True
+                        )
+            
+            super(PopenWrapper, self).__init__(arguments, *args, **kwargs)
+    
+    _debugClient = debugClient
+    module.Popen = PopenWrapper
--- a/eric6/DebugClients/Python/ThreadExtension.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/DebugClients/Python/ThreadExtension.py	Sat Feb 22 17:03:43 2020 +0100
@@ -7,9 +7,8 @@
 Module implementing an import hook patching thread modules to get debugged too.
 """
 
-import os.path
+import os
 import sys
-import importlib
 
 if sys.version_info[0] == 2:
     import thread as _thread
@@ -27,7 +26,7 @@
     """
     Class implementing the thread support for the debugger.
     
-    Provides methods for intercepting thread creation, retriving the running
+    Provides methods for intercepting thread creation, retrieving the running
     threads and their name and state.
     """
     def __init__(self):
@@ -35,11 +34,7 @@
         Constructor
         """
         self.threadNumber = 1
-        self.enableImportHooks = True
         self._original_start_new_thread = None
-        self.threadingAttached = False
-        self.qtThreadAttached = False
-        self.greenlet = False
         
         self.clientLock = threading.RLock()
         
@@ -53,17 +48,6 @@
         
         # special objects representing the main scripts thread and frame
         self.mainThread = self
-        
-        if sys.version_info[0] == 2:
-            self.threadModName = 'thread'
-        else:
-            self.threadModName = '_thread'
-        
-        # reset already imported thread module to apply hooks at next import
-        del sys.modules[self.threadModName]
-        del sys.modules['threading']
-        
-        sys.meta_path.insert(0, self)
 
     def attachThread(self, target=None, args=None, kwargs=None,
                      mainThread=False):
@@ -165,27 +149,23 @@
         Public method to send the list of threads.
         """
         self.updateThreadList()
+        
         threadList = []
-        if len(self.threads) > 1:
-            currentId = _thread.get_ident()
-            # update thread names set by user (threading.setName)
-            threadNames = {t.ident: t.getName() for t in threading.enumerate()}
+        currentId = _thread.get_ident()
+        # update thread names set by user (threading.setName)
+        threadNames = {t.ident: t.getName() for t in threading.enumerate()}
+        
+        for threadId, thd in self.threads.items():
+            d = {"id": threadId}
+            try:
+                d["name"] = threadNames.get(threadId, thd.name)
+                d["broken"] = thd.isBroken
+                d["except"] = thd.isException
+            except Exception:
+                d["name"] = 'UnknownThread'
+                d["broken"] = False
+                d["except"] = False
             
-            for threadId, thd in self.threads.items():
-                d = {"id": threadId}
-                try:
-                    d["name"] = threadNames.get(threadId, thd.name)
-                    d["broken"] = thd.isBroken
-                except Exception:
-                    d["name"] = 'UnknownThread'
-                    d["broken"] = False
-                
-                threadList.append(d)
-        else:
-            currentId = -1
-            d = {"id": -1}
-            d["name"] = "MainThread"
-            d["broken"] = self.isBroken
             threadList.append(d)
         
         self.sendJsonCommand("ResponseThreadList", {
@@ -246,208 +226,202 @@
         self.threads = {id_: thrd for id_, thrd in self.threads.items()
                         if id_ in frames}
     
-    def find_module(self, fullname, path=None):
-        """
-        Public method returning the module loader.
-        
-        @param fullname name of the module to be loaded
-        @type str
-        @param path path to resolve the module name
-        @type str
-        @return module loader object
-        @rtype object
+    #######################################################################
+    ## Methods below deal with patching various modules to support
+    ## debugging of threads.
+    #######################################################################
+    
+    def patchPyThread(self, module):
         """
-        if fullname in sys.modules or not self.debugging:
-            return None
+        Public method to patch Python _thread (Python3) and thread (Python2)
+        modules.
         
-        if fullname in [self.threadModName, 'PyQt4.QtCore', 'PyQt5.QtCore',
-                        'PySide.QtCore', 'PySide2.QtCore', 'greenlet',
-                        'threading'] and self.enableImportHooks:
-            # Disable hook to be able to import original module
-            self.enableImportHooks = False
-            return self
-        
-        return None
+        @param module reference to the imported module to be patched
+        @type module
+        """
+        # make thread hooks available to system
+        self._original_start_new_thread = module.start_new_thread
+        module.start_new_thread = self.attachThread
     
-    def load_module(self, fullname):
+    def patchGreenlet(self, module):
         """
-        Public method to load a module.
+        Public method to patch the 'greenlet' module.
         
-        @param fullname name of the module to be loaded
-        @type str
-        @return reference to the loaded module
-        @rtype module
+        @param module reference to the imported module to be patched
+        @type module
+        @return flag indicating that the module was processed
+        @rtype bool
         """
-        module = importlib.import_module(fullname)
-        sys.modules[fullname] = module
-        if (fullname == self.threadModName and
-                self._original_start_new_thread is None):
-            # make thread hooks available to system
-            self._original_start_new_thread = module.start_new_thread
-            module.start_new_thread = self.attachThread
-
-        elif (fullname == 'greenlet' and self.greenlet is False):
-            # Check for greenlet.settrace
-            if hasattr(module, 'settrace'):
-                self.greenlet = True
-                DebugBase.pollTimerEnabled = False
+        # Check for greenlet.settrace
+        if hasattr(module, 'settrace'):
+            DebugBase.pollTimerEnabled = False
+            return True
+        return False
+    
+    def patchPyThreading(self, module):
+        """
+        Public method to patch the Python threading module.
         
-        # Add hook for threading.run()
-        elif (fullname == "threading" and self.threadingAttached is False):
-            self.threadingAttached = True
+        @param module reference to the imported module to be patched
+        @type module
+        """
+        # _debugClient as a class attribute can't be accessed in following
+        # class. Therefore we need a global variable.
+        _debugClient = self
+        
+        def _bootstrap(self, run):
+            """
+            Bootstrap for threading, which reports exceptions correctly.
             
-            # _debugClient as a class attribute can't be accessed in following
-            # class. Therefore we need a global variable.
-            _debugClient = self
+            @param run the run method of threading.Thread
+            @type method pointer
+            """
+            newThread = DebugBase(_debugClient)
+            newThread.name = self.name
             
-            def _bootstrap(self, run):
-                """
-                Bootstrap for threading, which reports exceptions correctly.
-                
-                @param run the run method of threading.Thread
-                @type method pointer
-                """
-                newThread = DebugBase(_debugClient)
-                _debugClient.threads[self.ident] = newThread
-                newThread.name = self.name
-                # see DebugBase.bootstrap
-                sys.settrace(newThread.trace_dispatch)
-                try:
-                    run()
-                except Exception:
-                    excinfo = sys.exc_info()
-                    newThread.user_exception(excinfo, True)
-                finally:
-                    sys.settrace(None)
+            _debugClient.threads[self.ident] = newThread
+            _debugClient.dumpThreadList()
             
-            class ThreadWrapper(module.Thread):
+            # see DebugBase.bootstrap
+            sys.settrace(newThread.trace_dispatch)
+            try:
+                run()
+            except Exception:
+                excinfo = sys.exc_info()
+                newThread.user_exception(excinfo, True)
+            finally:
+                sys.settrace(None)
+                _debugClient.dumpThreadList()
+        
+        class ThreadWrapper(module.Thread):
+            """
+            Wrapper class for threading.Thread.
+            """
+            def __init__(self, *args, **kwargs):
                 """
-                Wrapper class for threading.Thread.
+                Constructor
                 """
-                def __init__(self, *args, **kwargs):
-                    """
-                    Constructor
-                    """
-                    # Overwrite the provided run method with our own, to
-                    # intercept the thread creation by threading.Thread
-                    self.run = lambda s=self, run=self.run: _bootstrap(s, run)
-                    
-                    super(ThreadWrapper, self).__init__(*args, **kwargs)
+                # Overwrite the provided run method with our own, to
+                # intercept the thread creation by threading.Thread
+                self.run = lambda s=self, run=self.run: _bootstrap(s, run)
+                
+                super(ThreadWrapper, self).__init__(*args, **kwargs)
+        
+        module.Thread = ThreadWrapper
+        
+        # Special handling of threading.(_)Timer
+        if sys.version_info[0] == 2:
+            timer = module._Timer
+        else:
+            timer = module.Timer
             
-            module.Thread = ThreadWrapper
-            
-            # Special handling of threading.(_)Timer
-            if sys.version_info[0] == 2:
-                timer = module._Timer
-            else:
-                timer = module.Timer
-                
-            class TimerWrapper(timer, ThreadWrapper):
+        class TimerWrapper(timer, ThreadWrapper):
+            """
+            Wrapper class for threading.(_)Timer.
+            """
+            def __init__(self, interval, function, *args, **kwargs):
                 """
-                Wrapper class for threading.(_)Timer.
+                Constructor
                 """
-                def __init__(self, interval, function, *args, **kwargs):
-                    """
-                    Constructor
-                    """
-                    super(TimerWrapper, self).__init__(
-                        interval, function, *args, **kwargs)
-            
-            if sys.version_info[0] == 2:
-                module._Timer = TimerWrapper
-            else:
-                module.Timer = TimerWrapper
+                super(TimerWrapper, self).__init__(
+                    interval, function, *args, **kwargs)
         
-            # Special handling of threading._DummyThread
-            class DummyThreadWrapper(module._DummyThread, ThreadWrapper):
+        if sys.version_info[0] == 2:
+            module._Timer = TimerWrapper
+        else:
+            module.Timer = TimerWrapper
+    
+        # Special handling of threading._DummyThread
+        class DummyThreadWrapper(module._DummyThread, ThreadWrapper):
+            """
+            Wrapper class for threading._DummyThread.
+            """
+            def __init__(self, *args, **kwargs):
                 """
-                Wrapper class for threading._DummyThread.
+                Constructor
                 """
-                def __init__(self, *args, **kwargs):
-                    """
-                    Constructor
-                    """
-                    super(DummyThreadWrapper, self).__init__(*args, **kwargs)
+                super(DummyThreadWrapper, self).__init__(*args, **kwargs)
+        
+        module._DummyThread = DummyThreadWrapper
+    
+    def patchQThread(self, module):
+        """
+        Public method to patch the QtCore module's QThread.
+        
+        @param module reference to the imported module to be patched
+        @type module
+        """
+        # _debugClient as a class attribute can't be accessed in following
+        # class. Therefore we need a global variable.
+        _debugClient = self
+
+        def _bootstrapQThread(self, run):
+            """
+            Bootstrap for QThread, which reports exceptions correctly.
             
-            module._DummyThread = DummyThreadWrapper
-        
-        # Add hook for *.QThread
-        elif (fullname in ['PyQt4.QtCore', 'PyQt5.QtCore',
-                           'PySide.QtCore', 'PySide2.QtCore'] and
-                self.qtThreadAttached is False):
-            self.qtThreadAttached = True
-            # _debugClient as a class attribute can't be accessed in following
-            # class. Therefore we need a global variable.
-            _debugClient = self
-
-            def _bootstrapQThread(self, run):
-                """
-                Bootstrap for QThread, which reports exceptions correctly.
-                
-                @param run the run method of *.QThread
-                @type method pointer
-                """
-                global _qtThreadNumber
-                
-                newThread = DebugBase(_debugClient)
-                ident = _thread.get_ident()
-                name = 'QtThread-{0}'.format(_qtThreadNumber)
-                
-                _qtThreadNumber += 1
+            @param run the run method of *.QThread
+            @type method pointer
+            """
+            global _qtThreadNumber
+            
+            newThread = DebugBase(_debugClient)
+            ident = _thread.get_ident()
+            name = 'QtThread-{0}'.format(_qtThreadNumber)
+            
+            _qtThreadNumber += 1
+            
+            newThread.id = ident
+            newThread.name = name
             
-                newThread.id = ident
-                newThread.name = name
-                
-                _debugClient.threads[ident] = newThread
-                
-                # see DebugBase.bootstrap
-                sys.settrace(newThread.trace_dispatch)
-                try:
-                    run()
-                except SystemExit:
-                    # *.QThreads doesn't like SystemExit
-                    pass
-                except Exception:
-                    excinfo = sys.exc_info()
-                    newThread.user_exception(excinfo, True)
-                finally:
-                    sys.settrace(None)
-        
-            class QThreadWrapper(module.QThread):
+            _debugClient.threads[ident] = newThread
+            _debugClient.dumpThreadList()
+            
+            # see DebugBase.bootstrap
+            sys.settrace(newThread.trace_dispatch)
+            try:
+                run()
+            except SystemExit:
+                # *.QThreads doesn't like SystemExit
+                pass
+            except Exception:
+                excinfo = sys.exc_info()
+                newThread.user_exception(excinfo, True)
+            finally:
+                sys.settrace(None)
+                _debugClient.dumpThreadList()
+    
+        class QThreadWrapper(module.QThread):
+            """
+            Wrapper class for *.QThread.
+            """
+            def __init__(self, *args, **kwargs):
                 """
-                Wrapper class for *.QThread.
+                Constructor
                 """
-                def __init__(self, *args, **kwargs):
-                    """
-                    Constructor
-                    """
-                    # Overwrite the provided run method with our own, to
-                    # intercept the thread creation by Qt
-                    self.run = lambda s=self, run=self.run: (
-                        _bootstrapQThread(s, run))
-                    
-                    super(QThreadWrapper, self).__init__(*args, **kwargs)
-            
-            class QRunnableWrapper(module.QRunnable):
-                """
-                Wrapper class for *.QRunnable.
+                # Overwrite the provided run method with our own, to
+                # intercept the thread creation by Qt
+                self.run = lambda s=self, run=self.run: (
+                    _bootstrapQThread(s, run))
+                
+                super(QThreadWrapper, self).__init__(*args, **kwargs)
+        
+        class QRunnableWrapper(module.QRunnable):
+            """
+            Wrapper class for *.QRunnable.
+            """
+            def __init__(self, *args, **kwargs):
                 """
-                def __init__(self, *args, **kwargs):
-                    """
-                    Constructor
-                    """
-                    # Overwrite the provided run method with our own, to
-                    # intercept the thread creation by Qt
-                    self.run = lambda s=self, run=self.run: (
-                        _bootstrapQThread(s, run))
-                    
-                    super(QRunnableWrapper, self).__init__(*args, **kwargs)
-            
-            module.QThread = QThreadWrapper
-            module.QRunnable = QRunnableWrapper
+                Constructor
+                """
+                # Overwrite the provided run method with our own, to
+                # intercept the thread creation by Qt
+                self.run = lambda s=self, run=self.run: (
+                    _bootstrapQThread(s, run))
+                
+                super(QRunnableWrapper, self).__init__(*args, **kwargs)
         
-        self.enableImportHooks = True
-        return module
+        module.QThread = QThreadWrapper
+        module.QRunnable = QRunnableWrapper
 
 #
 # eflag: noqa = M702
--- a/eric6/Debugger/CallStackViewer.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Debugger/CallStackViewer.py	Sat Feb 22 17:03:43 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	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Debugger/CallTraceViewer.py	Sat Feb 22 17:03:43 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,88 @@
         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(str, int, str, bool, str)
+    def __clientExit(self, program, status, message, quiet, debuggerId):
         """
-        Private slot handling a client exiting.
+        Private slot to handle a debug client terminating.
+        
+        @param program name of the exited program
+        @type str
+        @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	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Debugger/DebugServer.py	Sat Feb 22 17:03:43 2020 +0100
@@ -11,7 +11,7 @@
 import os
 import sys
 
-from PyQt5.QtCore import pyqtSignal, QModelIndex
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, QModelIndex
 from PyQt5.QtNetwork import (
     QTcpServer, QHostAddress, QHostInfo, QNetworkInterface
 )
@@ -42,48 +42,53 @@
     @signal clientProcessStderr(str) emitted after the client has sent some
         output via stderr
     @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 clientClearBreak(filename, lineno) emitted after the debug client
-        has decided to clear a temporary breakpoint
-    @signal clientBreakConditionError(fn, lineno) emitted after the client has
-        signaled a syntax error in a breakpoint condition
-    @signal clientClearWatch(condition) emitted after the debug client
-            has decided to clear a temporary watch expression
-    @signal clientWatchConditionError(condition) emitted after the client has
-        signaled a syntax error in a watch expression
-    @signal clientRawInput(prompt, echo) emitted after a raw input request was
-        received
-    @signal clientBanner(version, platform, dbgclient, venvname) emitted after
+    @signal clientRawInputSent(debuggerId) emitted after the data was sent
+        to the indicated debug client
+    @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(str, int, str, bool, str) emitted after the client has
+        exited giving the program name, 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, debuggerId) emitted after the
+        debug client has decided to clear a temporary breakpoint
+    @signal clientBreakConditionError(fn, lineno, debuggerId) emitted after the
+        client has signaled a syntax error in a breakpoint condition
+    @signal clientClearWatch(condition, debuggerId) emitted after the debug
+        client has decided to clear a temporary watch expression
+    @signal clientWatchConditionError(condition, debuggerId) emitted after the
+        client has signaled a syntax error in a watch expression
+    @signal clientRawInput(prompt, echo, debuggerId) emitted after a raw input
+        request was received
+    @signal clientBanner(version, platform, venvname) emitted after
         the client banner data was received
     @signal clientCapabilities(capabilities, cltype, venvname) emitted after
         the clients capabilities were received
-    @signal clientCompletionList(completionList, text) emitted after the client
-        the commandline completion list and the reworked searchstring was
-        received from the client
+    @signal clientCompletionList(completionList, text, debuggerId) emitted
+        after the client the commandline completion list and the reworked
+        search string was received from the client
     @signal passiveDebugStarted(str, bool) emitted after the debug client has
         connected in passive debug mode
     @signal clientGone(bool) emitted if the client went away (planned or
@@ -111,35 +116,39 @@
         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 clientDebuggerId(debuggerId) emitted to indicate a newly connected
+        debugger backend
     """
-    clientClearBreak = pyqtSignal(str, int)
-    clientClearWatch = pyqtSignal(str)
+    clientClearBreak = pyqtSignal(str, int, str)
+    clientClearWatch = pyqtSignal(str, str)
     clientGone = pyqtSignal(bool)
     clientProcessStdout = pyqtSignal(str)
     clientProcessStderr = pyqtSignal(str)
-    clientRawInputSent = pyqtSignal()
+    clientRawInputSent = pyqtSignal(str)
     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)
-    clientBreakConditionError = pyqtSignal(str, int)
-    clientWatchConditionError = pyqtSignal(str)
-    clientRawInput = pyqtSignal(str, bool)
-    clientBanner = pyqtSignal(str, str, str, str)
+    clientLine = pyqtSignal(str, int, str, bool)
+    clientStack = pyqtSignal(list, str)
+    clientThreadList = pyqtSignal('PyQt_PyObject', 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(str, int, str, bool, str)
+    lastClientExited = pyqtSignal()
+    clientBreakConditionError = pyqtSignal(str, int, str)
+    clientWatchConditionError = pyqtSignal(str, str)
+    clientRawInput = pyqtSignal(str, bool, str)
+    clientBanner = pyqtSignal(str, str, str)
     clientCapabilities = pyqtSignal(int, str, str)
-    clientCompletionList = pyqtSignal(list, str)
+    clientCompletionList = pyqtSignal(list, str, str)
     clientInterpreterChanged = pyqtSignal(str)
+    clientDebuggerId = pyqtSignal(str)
     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):
@@ -186,6 +195,10 @@
         self.watchSpecialChanged = self.tr(
             "changed", "must be same as in EditWatchpointDialog")
         
+        # arrays to track already reported issues
+        self.__reportedBreakpointIssues = []
+        self.__reportedWatchpointIssues = []
+        
         self.networkInterface = Preferences.getDebugger("NetworkInterface")
         if self.networkInterface == "all":
             hostAddress = QHostAddress("0.0.0.0")  # QHostAddress.Any)
@@ -249,16 +262,19 @@
         
         self.__maxVariableSize = Preferences.getDebugger("MaxVariableSize")
         
+        self.__multiprocessNoDebugList = []
+        
         self.__registerDebuggerInterfaces()
-        
+    
     def getHostAddress(self, localhost):
         """
         Public method to get the IP address or hostname the debug server is
         listening.
         
         @param localhost flag indicating to return the address for localhost
-            (boolean)
-        @return IP address or hostname (string)
+        @type bool
+        @return IP address or hostname
+        @rtype str
         """
         if self.networkInterface == "all":
             if localhost:
@@ -273,14 +289,16 @@
         else:
             return "{0}@@i{1}".format(self.networkInterface,
                                       self.networkInterfaceIndex)
-        
+    
     def __getNetworkInterfaceAndIndex(self, address):
         """
         Private method to determine the network interface and the interface
         index.
         
-        @param address address to determine the info for (string)
-        @return tuple of network interface name (string) and index (integer)
+        @param address address to determine the info for
+        @type str
+        @return tuple of network interface name and index
+        @rtype tuple of (str, int)
         """
         if address not in ["all", "allv6"]:
             for networkInterface in QNetworkInterface.allInterfaces():
@@ -293,7 +311,7 @@
                                     networkInterface.index())
         
         return "", 0
-        
+    
     def preferencesChanged(self):
         """
         Public slot to handle the preferencesChanged signal.
@@ -309,7 +327,7 @@
                                            reregister=True)
         
         self.__maxVariableSize = Preferences.getDebugger("MaxVariableSize")
-        
+    
     def registerDebuggerInterface(self, interfaceName, getRegistryData,
                                   reregister=False):
         """
@@ -343,7 +361,7 @@
                 self.__debuggerInterfaceRegistry[clientLanguage] = [
                     clientCapabilities, clientExtensions, interfaceCreator,
                     interfaceName]
-        
+    
     def unregisterDebuggerInterface(self, interfaceName):
         """
         Public method to unregister a debugger interface.
@@ -360,7 +378,7 @@
             for clientLanguage in clientLanguages:
                 del self.__debuggerInterfaceRegistry[clientLanguage]
             del self.__debuggerInterfaces[interfaceName]
-        
+    
     def __findLanguageForExtension(self, ext):
         """
         Private method to get the language associated with a file extension.
@@ -375,7 +393,7 @@
                 return language
         
         return ""
-        
+    
     def __registerDebuggerInterfaces(self):
         """
         Private method to register the available internal debugger interfaces.
@@ -388,14 +406,16 @@
                 mod = getattr(mod, comp)
             
             self.registerDebuggerInterface(name, mod.getRegistryData)
-        
+    
     def getSupportedLanguages(self, shellOnly=False):
         """
         Public slot to return the supported programming languages.
         
         @param shellOnly flag indicating only languages supporting an
             interactive shell should be returned
-        @return list of supported languages (list of strings)
+        @type bool
+        @return list of supported languages
+        @rtype list of str
         """
         languages = list(self.__debuggerInterfaceRegistry.keys())
         try:
@@ -409,25 +429,27 @@
                          DebugClientCapabilities.HasShell]
         
         return languages[:]
-        
+    
     def getExtensions(self, language):
         """
         Public slot to get the extensions associated with the given language.
         
-        @param language language to get extensions for (string)
+        @param language language to get extensions for
+        @type str
         @return tuple of extensions associated with the language
-            (tuple of strings)
+        @rtype tuple of str
         """
         if language in self.__debuggerInterfaceRegistry:
             return tuple(self.__debuggerInterfaceRegistry[language][1])
         else:
             return ()
-        
+    
     def __createDebuggerInterface(self, clientType=None):
         """
         Private slot to create the debugger interface object.
         
-        @param clientType type of the client interface to be created (string)
+        @param clientType type of the client interface to be created
+        @type str
         """
         if self.lastClientType != self.clientType or clientType is not None:
             if clientType is None:
@@ -441,18 +463,19 @@
                     self.__debuggerInterfaceRegistry["None"][2](
                         self, self.passive))
                 self.clientType = "None"
-        
+    
     def __setClientType(self, clType):
         """
         Private method to set the client type.
         
-        @param clType type of client to be started (string)
+        @param clType type of client to be started
+        @type str
         """
         if clType is not None and clType in self.getSupportedLanguages():
             self.clientType = clType
             Preferences.Prefs.settings.setValue(
                 'DebugClient/Type', self.clientType)
-        
+    
     def startClient(self, unplanned=True, clType=None, forProject=False,
                     runInConsole=False, venvName="", workingDir=None):
         """
@@ -528,8 +551,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)
@@ -540,7 +561,7 @@
         if clientInterpreter != self.clientInterpreter:
             self.clientInterpreter = clientInterpreter
             self.clientInterpreterChanged.emit(clientInterpreter)
-
+    
     def __clientProcessOutput(self):
         """
         Private slot to process client output received via stdout.
@@ -549,7 +570,7 @@
                      Preferences.getSystem("IOEncoding"),
                      'replace')
         self.clientProcessStdout.emit(output)
-        
+    
     def __clientProcessError(self):
         """
         Private slot to process client output received via stderr.
@@ -558,82 +579,112 @@
                     Preferences.getSystem("IOEncoding"),
                     'replace')
         self.clientProcessStderr.emit(error)
-        
+    
+    @pyqtSlot(str, int)
     def __clientClearBreakPoint(self, fn, lineno):
         """
         Private slot to handle the clientClearBreak signal.
         
-        @param fn filename of breakpoint to clear (string)
-        @param lineno line number of breakpoint to clear (integer)
+        @param fn filename of breakpoint to clear
+        @type str
+        @param lineno line number of breakpoint to clear
+        @type int
         """
         if self.debugging:
             index = self.breakpointModel.getBreakPointIndex(fn, lineno)
             self.breakpointModel.deleteBreakPointByIndex(index)
-
+            if (fn, lineno) in self.__reportedBreakpointIssues:
+                self.__reportedBreakpointIssues.remove((fn, lineno))
+    
     def __deleteBreakPoints(self, parentIndex, start, end):
         """
         Private slot to delete 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
         """
         if self.debugging:
             for row in range(start, end + 1):
                 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)
+                if (fn, lineno) in self.__reportedBreakpointIssues:
+                    self.__reportedBreakpointIssues.remove((fn, lineno))
+    
     def __changeBreakPoints(self, startIndex, endIndex):
         """
         Private slot to set changed breakpoints.
         
-        @param startIndex starting index of the change breakpoins (QModelIndex)
-        @param endIndex ending index of the change breakpoins (QModelIndex)
+        @param startIndex starting index of the change breakpoins
+        @type QModelIndex
+        @param endIndex ending index of the change breakpoins
+        @type QModelIndex
         """
         if self.debugging:
             self.__addBreakPoints(
                 QModelIndex(), startIndex.row(), endIndex.row())
-
+    
     def __breakPointDataAboutToBeChanged(self, startIndex, endIndex):
         """
         Private slot to handle the dataAboutToBeChanged signal of the
         breakpoint 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.__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 = (
+                fn, lineno, cond, temp, enabled, ignorecount = (
                     self.breakpointModel.getBreakPointByIndex(index)[:6])
-                self.remoteBreakpoint(fn, line, True, cond, temp)
+                
+                if (fn, lineno) in self.__reportedBreakpointIssues:
+                    self.__reportedBreakpointIssues.remove((fn, lineno))
+                
+                self.remoteBreakpoint(debuggerId, fn, lineno, True, cond, temp)
                 if not enabled:
-                    self.__remoteBreakpointEnable(fn, line, False)
+                    self.__remoteBreakpointEnable(
+                        debuggerId, fn, lineno, False)
                 if ignorecount:
-                    self.__remoteBreakpointIgnore(fn, line, ignorecount)
-
+                    self.__remoteBreakpointIgnore(
+                        debuggerId, fn, lineno, ignorecount)
+    
     def __makeWatchCondition(self, cond, special):
         """
         Private method to construct the condition string.
         
-        @param cond condition (string)
-        @param special special condition (string)
-        @return condition string (string)
+        @param cond condition
+        @type str
+        @param special special condition
+        @type str
+        @return condition string
+        @rtype str
         """
         if special == "":
             _cond = cond
@@ -643,14 +694,15 @@
             elif special == self.watchSpecialChanged:
                 _cond = "{0} ??changed??".format(cond)
         return _cond
-        
+    
     def __splitWatchCondition(self, cond):
         """
         Private method to split a remote watch expression.
         
-        @param cond remote expression (string)
+        @param cond remote expression
+        @type str
         @return tuple of local expression (string) and special condition
-            (string)
+        @rtype str
         """
         if cond.endswith(" ??created??"):
             cond, special = cond.split()
@@ -663,25 +715,32 @@
             special = ""
         
         return cond, special
-        
+    
+    @pyqtSlot(str)
     def __clientClearWatchPoint(self, condition):
         """
         Private slot to handle the clientClearWatch signal.
         
-        @param condition expression of watch expression to clear (string)
+        @param condition expression of watch expression to clear
+        @type str
         """
         if self.debugging:
             cond, special = self.__splitWatchCondition(condition)
             index = self.watchpointModel.getWatchPointIndex(cond, special)
             self.watchpointModel.deleteWatchPointByIndex(index)
-        
+            if condition in self.__reportedWatchpointIssues:
+                self.__reportedWatchpointIssues.remove(condition)
+    
     def __deleteWatchPoints(self, parentIndex, start, end):
         """
         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 +748,37 @@
                 cond, special = (
                     self.watchpointModel.getWatchPointByIndex(index)[0:2])
                 cond = self.__makeWatchCondition(cond, special)
-                self.__remoteWatchpoint(cond, False)
-        
+                self.__remoteWatchpoint("", cond, False)
+                if cond in self.__reportedWatchpointIssues:
+                    self.__reportedWatchpointIssues.remove(cond)
+    
     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,40 +786,50 @@
                 cond, special, temp, enabled, ignorecount = (
                     self.watchpointModel.getWatchPointByIndex(index)[:5])
                 cond = self.__makeWatchCondition(cond, special)
-                self.__remoteWatchpoint(cond, True, temp)
+                
+                if cond in self.__reportedWatchpointIssues:
+                    self.__reportedWatchpointIssues.remove(cond)
+                
+                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):
         """
         Private slot to set changed watch expressions.
         
-        @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.__addWatchPoints(
                 QModelIndex(), startIndex.row(), endIndex.row())
-        
+    
     def getClientCapabilities(self, clientType):
         """
         Public method to retrieve the debug clients capabilities.
         
-        @param clientType debug client type (string)
-        @return debug client capabilities (integer)
+        @param clientType debug client type
+        @type str
+        @return debug client capabilities
+        @rtype int
         """
         try:
             return self.__debuggerInterfaceRegistry[clientType][0]
         except KeyError:
             return 0    # no capabilities
-        
+    
     def getClientInterpreter(self):
         """
         Public method to get the interpreter of the debug client.
         
-        @return interpreter of the debug client (string)
+        @return interpreter of the debug client
+        @rtype str
         """
         return self.clientInterpreter
     
@@ -800,20 +879,23 @@
             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):
         """
         Public method to cleanly shut down.
@@ -823,12 +905,13 @@
         """
         if self.debuggerInterface is not None:
             self.debuggerInterface.shutdown()
-
+    
     def remoteEnvironment(self, env):
         """
         Public method to set the environment for a program to debug, run, ...
         
-        @param env environment settings (string)
+        @param env environment settings
+        @type str
         """
         envlist = Utilities.parseEnvironmentString(env)
         envdict = {}
@@ -841,11 +924,12 @@
             except ValueError:
                 pass
         self.debuggerInterface.remoteEnvironment(envdict)
-        
+    
     def remoteLoad(self, venvName, fn, argv, wd, env, autoClearShell=True,
                    tracePython=False, autoContinue=True, forProject=False,
                    runInConsole=False, autoFork=False, forkChild=False,
-                   clientType="", enableCallTrace=False):
+                   clientType="", enableCallTrace=False,
+                   enableMultiprocess=False, multiprocessNoDebug=""):
         """
         Public method to load a new program to debug.
         
@@ -882,9 +966,15 @@
         @keyparam enableCallTrace flag indicating to enable the call trace
             function
         @type bool
+        @param enableMultiprocess flag indicating to perform multiprocess
+            debugging
+        @type bool
+        @param multiprocessNoDebug space separated list of programs not to be
+            debugged
+        @type str
         """
         self.__autoClearShell = autoClearShell
-        self.__autoContinue = autoContinue
+        self.__multiprocessNoDebugList = multiprocessNoDebug.split()
         
         if clientType not in self.getSupportedLanguages():
             # a not supported client language was requested
@@ -909,16 +999,19 @@
         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,
-                                          autoContinue, autoFork, forkChild)
+        self.debuggerInterface.remoteLoad(
+            fn, argv, wd, tracePython, autoContinue, autoFork, forkChild,
+            enableMultiprocess=enableMultiprocess
+        )
         self.debugging = True
         self.running = True
         self.__restoreBreakpoints()
         self.__restoreWatchpoints()
-
+        self.__restoreNoDebugList()
+    
     def remoteRun(self, venvName, fn, argv, wd, env, autoClearShell=True,
                   forProject=False, runInConsole=False, autoFork=False,
                   forkChild=False, clientType=""):
@@ -980,7 +1073,7 @@
         self.debuggerInterface.remoteRun(fn, argv, wd, autoFork, forkChild)
         self.debugging = False
         self.running = True
-
+    
     def remoteCoverage(self, venvName, fn, argv, wd, env,
                        autoClearShell=True, erase=False, forProject=False,
                        runInConsole=False, clientType=""):
@@ -1041,7 +1134,7 @@
         self.debuggerInterface.remoteCoverage(fn, argv, wd, erase)
         self.debugging = False
         self.running = True
-
+    
     def remoteProfile(self, venvName, fn, argv, wd, env,
                       autoClearShell=True, erase=False, forProject=False,
                       runInConsole=False, clientType=""):
@@ -1102,207 +1195,301 @@
         self.debuggerInterface.remoteProfile(fn, argv, wd, erase)
         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)
-
-    def remoteStep(self):
+        self.debuggerInterface.remoteStatement(debuggerId, stmt.rstrip())
+    
+    def remoteStep(self, debuggerId):
         """
         Public method to single step the debugged program.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.debuggerInterface.remoteStep()
-
-    def remoteStepOver(self):
+        self.debuggerInterface.remoteStep(debuggerId)
+    
+    def remoteStepOver(self, debuggerId):
         """
         Public method to step over the debugged program.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.debuggerInterface.remoteStepOver()
-
-    def remoteStepOut(self):
+        self.debuggerInterface.remoteStepOver(debuggerId)
+    
+    def remoteStepOut(self, debuggerId):
         """
         Public method to step out the debugged program.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.debuggerInterface.remoteStepOut()
-
-    def remoteStepQuit(self):
+        self.debuggerInterface.remoteStepOut(debuggerId)
+    
+    def remoteStepQuit(self, debuggerId):
         """
         Public method to stop the debugged program.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.debuggerInterface.remoteStepQuit()
-
-    def remoteContinue(self, special=False):
+        self.debuggerInterface.remoteStepQuit(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
         """
-        self.debuggerInterface.remoteContinue(special)
-
-    def remoteMoveIP(self, line):
+        self.debuggerInterface.remoteContinue(debuggerId, special)
+    
+    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)
-
-    def remoteBreakpoint(self, fn, line, setBreakpoint, cond=None, temp=False):
+        self.debuggerInterface.remoteMoveIP(debuggerId, line)
+    
+    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)
-        
-    def __remoteBreakpointEnable(self, fn, line, enable):
+        self.debuggerInterface.remoteBreakpoint(
+            debuggerId, fn, line, setBreakpoint, cond, temp)
+    
+    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)
-        
-    def __remoteBreakpointIgnore(self, fn, line, count):
+        self.debuggerInterface.remoteBreakpointEnable(
+            debuggerId, fn, line, enable)
+    
+    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)
-        
-    def __remoteWatchpoint(self, cond, setWatch, temp=False):
+        self.debuggerInterface.remoteBreakpointIgnore(
+            debuggerId, fn, line, count)
+    
+    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):
+    def remoteRawInput(self, debuggerId, inputString):
         """
         Public method to send the raw input to the debugged program.
         
-        @param s the raw input (string)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param inputString the raw input
+        @type str
         """
-        self.debuggerInterface.remoteRawInput(s)
-        self.clientRawInputSent.emit()
-        
-    def remoteThreadList(self):
+        self.debuggerInterface.remoteRawInput(debuggerId, inputString)
+        self.clientRawInputSent.emit(debuggerId)
+    
+    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()
-        
-    def remoteSetThread(self, tid):
+        self.debuggerInterface.remoteThreadList(debuggerId)
+    
+    def remoteSetThread(self, debuggerId, tid):
         """
         Public method to request to set the given thread as current thread.
         
-        @param tid id of the thread (integer)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param tid id of the thread
+        @type int
+        """
+        self.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)
-        
-    def remoteClientVariable(self, scope, filterList, var, framenr=0):
+            debuggerId, scope, filterList, framenr, self.__maxVariableSize)
+    
+    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)
-        
-    def remoteClientSetFilter(self, scope, filterStr):
+            debuggerId, scope, filterList, var, framenr,
+            self.__maxVariableSize)
+    
+    def remoteClientSetFilter(self, debuggerId, scope, filterStr):
         """
         Public method to set a variables filter list.
         
+        @param debuggerId ID of the debugger backend
+        @type str
         @param scope the scope of the variables (0 = local, 1 = global)
+        @type int
         @param filterStr regexp string for variable names to filter out
-            (string)
+        @type str
         """
-        self.debuggerInterface.remoteClientSetFilter(scope, filterStr)
-        
-    def setCallTraceEnabled(self, on):
+        self.debuggerInterface.remoteClientSetFilter(
+            debuggerId, scope, filterStr)
+    
+    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):
         """
         Public slot to get the banner info of the remote client.
         """
         self.debuggerInterface.remoteBanner()
-        
+    
     def remoteCapabilities(self):
         """
         Public slot to get the debug clients capabilities.
         """
         self.debuggerInterface.remoteCapabilities()
-        
-    def remoteCompletion(self, text):
+    
+    def remoteCompletion(self, debuggerId, text):
         """
         Public slot to get the a list of possible commandline completions
         from the remote client.
         
-        @param text the text to be completed (string)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param text the text to be completed
+        @type str
         """
         self.debuggerInterface.remoteCompletion(text)
     
@@ -1419,7 +1606,7 @@
         if debug:
             self.__restoreBreakpoints()
             self.__restoreWatchpoints()
-        
+    
     def remoteUTRun(self, debug=False, failfast=False):
         """
         Public method to start a unittest run.
@@ -1430,115 +1617,157 @@
         @type bool
         """
         self.debuggerInterface.remoteUTRun(debug, failfast)
-        
+    
     def remoteUTStop(self):
         """
         public method to stop a unittest run.
         """
         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)
-        
-    def signalClientLine(self, filename, lineno, forStack=False):
+        if debuggerId:
+            self.clientOutput.emit("{0}: {1}".format(debuggerId, line))
+        else:
+            self.clientOutput.emit(line)
+    
+    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)
-        
-    def signalClientStack(self, stack):
+        self.clientLine.emit(filename, lineno, debuggerId, forStack)
+    
+    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)
-        
-    def signalClientThreadList(self, currentId, threadList):
+        self.clientStack.emit(stack, debuggerId)
+    
+    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)
-        
-    def signalClientThreadSet(self):
+        self.clientThreadList.emit(currentId, threadList, debuggerId)
+    
+    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()
-        
-    def signalClientVariables(self, scope, variables):
+        self.clientThreadSet.emit(debuggerId)
+    
+    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)
-        
-    def signalClientVariable(self, scope, variables):
+        self.clientVariables.emit(scope, variables, debuggerId)
+    
+    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)
-        
-    def signalClientStatement(self, more):
+        self.clientVariable.emit(scope, variables, debuggerId)
+    
+    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)
-        
-    def signalClientSyntaxError(self, message, filename, lineNo, characterNo):
+                                      stackTrace, debuggerId)
+    
+    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,23 +1781,35 @@
         @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)
-        
-    def signalClientExit(self, status, message=""):
+                                   funcName, funcArgs, debuggerId)
+    
+    def signalClientExit(self, program, status, message, debuggerId):
         """
         Public method to process the client exit status.
         
+        @param program name of the exited program
+        @type str
         @param status exit code
         @type int
         @param message message sent with the exit
         @type str
+        @param debuggerId ID of the debugger backend
+        @type str
+        """
+        self.clientExit.emit(program, 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,53 +1817,75 @@
         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):
+    
+    def signalClientClearBreak(self, filename, lineno, debuggerId):
         """
         Public method to process the client clear breakpoint command.
         
-        @param filename filename of the breakpoint (string)
-        @param lineno line umber of the breakpoint (integer)
+        @param filename filename of the breakpoint
+        @type str
+        @param lineno line umber of the breakpoint
+        @type int
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.clientClearBreak.emit(filename, lineno)
-        
-    def signalClientBreakConditionError(self, filename, lineno):
+        self.clientClearBreak.emit(filename, lineno, debuggerId)
+    
+    def signalClientBreakConditionError(self, filename, lineno, debuggerId):
         """
         Public method to process the client breakpoint condition error info.
         
-        @param filename filename of the breakpoint (string)
-        @param lineno line umber of the breakpoint (integer)
+        @param filename filename of the breakpoint
+        @type str
+        @param lineno line umber of the breakpoint
+        @type int
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.clientBreakConditionError.emit(filename, lineno)
-        
-    def signalClientClearWatch(self, condition):
+        if (filename, lineno) not in self.__reportedBreakpointIssues:
+            self.__reportedBreakpointIssues.append((filename, lineno))
+            self.clientBreakConditionError.emit(filename, lineno, debuggerId)
+    
+    def signalClientClearWatch(self, condition, debuggerId):
         """
         Public slot to handle the clientClearWatch signal.
         
-        @param condition expression of watch expression to clear (string)
+        @param condition expression of watch expression to clear
+        @type str
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.clientClearWatch.emit(condition)
-        
-    def signalClientWatchConditionError(self, condition):
+        self.clientClearWatch.emit(condition, debuggerId)
+    
+    def signalClientWatchConditionError(self, condition, debuggerId):
         """
         Public method to process the client watch expression error info.
         
-        @param condition expression of watch expression to clear (string)
+        @param condition expression of watch expression to clear
+        @type str
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.clientWatchConditionError.emit(condition)
-        
-    def signalClientRawInput(self, prompt, echo):
+        if condition not in self.__reportedWatchpointIssues:
+            self.__reportedWatchpointIssues.append(condition)
+            self.clientWatchConditionError.emit(condition, debuggerId)
+    
+    def signalClientRawInput(self, prompt, echo, debuggerId):
         """
         Public method to process the client raw input command.
         
-        @param prompt the input prompt (string)
-        @param echo flag indicating an echoing of the input (boolean)
+        @param prompt the input prompt
+        @type str
+        @param echo flag indicating an echoing of the input
+        @type bool
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.clientRawInput.emit(prompt, echo)
-        
-    def signalClientBanner(self, version, platform, debugClient, venvName):
+        self.clientRawInput.emit(prompt, echo, debuggerId)
+    
+    def signalClientBanner(self, version, platform, venvName):
         """
         Public method to process the client banner info.
         
@@ -1630,12 +1893,10 @@
         @type str
         @param platform hostname of the client
         @type str
-        @param debugClient additional debugger type info
-        @type str
         @param venvName name of the virtual environment
         @type str
         """
-        self.clientBanner.emit(version, platform, debugClient, venvName)
+        self.clientBanner.emit(version, platform, venvName)
     
     def signalClientCapabilities(self, capabilities, clientType, venvName):
         """
@@ -1654,33 +1915,46 @@
         except KeyError:
             # ignore silently
             pass
-        
-    def signalClientCompletionList(self, completionList, text):
+    
+    def signalClientCompletionList(self, completionList, text, debuggerId):
         """
         Public method to process the client auto completion info.
         
-        @param completionList list of possible completions (list of strings)
-        @param text the text to be completed (string)
+        @param completionList list of possible completions
+        @type list of str
+        @param text the text to be completed
+        @type str
+        @param debuggerId ID of the debugger backend
+        @type str
         """
         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.
@@ -1693,81 +1967,100 @@
         @type str
         """
         self.utDiscovered.emit(testCases, exceptionType, exceptionValue)
-        
+    
     def clientUtPrepared(self, result, exceptionType, exceptionValue):
         """
         Public method to process the client unittest prepared info.
         
-        @param result number of test cases (0 = error) (integer)
-        @param exceptionType exception type (string)
-        @param exceptionValue exception message (string)
+        @param result number of test cases (0 = error)
+        @type int
+        @param exceptionType exception type
+        @type str
+        @param exceptionValue exception message
+        @type str
         """
         self.utPrepared.emit(result, exceptionType, exceptionValue)
-        
+    
     def clientUtStartTest(self, testname, doc):
         """
         Public method to process the client start test info.
         
-        @param testname name of the test (string)
-        @param doc short description of the test (string)
+        @param testname name of the test
+        @type str
+        @param doc short description of the test
+        @type str
         """
         self.utStartTest.emit(testname, doc)
-        
+    
     def clientUtStopTest(self):
         """
         Public method to process the client stop test info.
         """
         self.utStopTest.emit()
-        
+    
     def clientUtTestFailed(self, testname, traceback, testId):
         """
         Public method to process the client test failed info.
         
-        @param testname name of the test (string)
-        @param traceback lines of traceback info (list of strings)
-        @param testId id of the test (string)
+        @param testname name of the test
+        @type str
+        @param traceback lines of traceback info
+        @type list of str
+        @param testId id of the test
+        @type str
         """
         self.utTestFailed.emit(testname, traceback, testId)
-        
+    
     def clientUtTestErrored(self, testname, traceback, testId):
         """
         Public method to process the client test errored info.
         
-        @param testname name of the test (string)
-        @param traceback lines of traceback info (list of strings)
-        @param testId id of the test (string)
+        @param testname name of the test
+        @type str
+        @param traceback lines of traceback info
+        @type list of str
+        @param testId id of the test
+        @type str
         """
         self.utTestErrored.emit(testname, traceback, testId)
-        
+    
     def clientUtTestSkipped(self, testname, reason, testId):
         """
         Public method to process the client test skipped info.
         
-        @param testname name of the test (string)
-        @param reason reason for skipping the test (string)
-        @param testId id of the test (string)
+        @param testname name of the test
+        @type str
+        @param reason reason for skipping the test
+        @type str
+        @param testId id of the test
+        @type str
         """
         self.utTestSkipped.emit(testname, reason, testId)
-        
+    
     def clientUtTestFailedExpected(self, testname, traceback, testId):
         """
         Public method to process the client test failed expected info.
         
-        @param testname name of the test (string)
-        @param traceback lines of traceback info (list of strings)
-        @param testId id of the test (string)
+        @param testname name of the test
+        @type str
+        @param traceback lines of traceback info
+        @type list of str
+        @param testId id of the test
+        @type str
         """
         self.utTestFailedExpected.emit(testname, traceback, testId)
-        
+    
     def clientUtTestSucceededUnexpected(self, testname, testId):
         """
         Public method to process the client test succeeded unexpected info.
         
-        @param testname name of the test (string)
-        @param testId id of the test (string)
+        @param testname name of the test
+        @type str
+        @param testId id of the test
+        @type str
         """
         self.utTestSucceededUnexpected.emit(testname, testId)
-        
+    
     def clientUtFinished(self, status):
         """
         Public method to process the client unit test finished info.
@@ -1777,25 +2070,29 @@
         """
         self.utFinished.emit()
         
-        self.clientExit.emit(int(status), "", True)
+        self.clientExit.emit("", int(status), "", True, "")
         self.debugging = False
         self.running = False
-        
-    def passiveStartUp(self, fn, exc):
+    
+    def passiveStartUp(self, fn, exc, debuggerId):
         """
         Public method to handle a passive debug connection.
         
-        @param fn filename of the debugged script (string)
-        @param exc flag to enable exception reporting of the IDE (boolean)
+        @param fn filename of the debugged script
+        @type str
+        @param exc flag to enable exception reporting of the IDE
+        @type bool
+        @param debuggerId ID of the debugger backend
+        @type str
         """
         self.appendStdout.emit(self.tr("Passive debug connection received\n"))
         self.passiveClientExited = False
         self.debugging = True
         self.running = True
-        self.__restoreBreakpoints()
-        self.__restoreWatchpoints()
+        self.__restoreBreakpoints(debuggerId)
+        self.__restoreWatchpoints(debuggerId)
         self.passiveDebugStarted.emit(fn, exc)
-        
+    
     def __passiveShutDown(self):
         """
         Private method to shut down a passive debug connection.
@@ -1803,37 +2100,48 @@
         self.passiveClientExited = True
         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):
         """
         Public slot to get a reference to the breakpoint model object.
         
-        @return reference to the breakpoint model object (BreakPointModel)
+        @return reference to the breakpoint model object
+        @rtype BreakPointModel
         """
         return self.breakpointModel
-
+    
     def getWatchPointModel(self):
         """
         Public slot to get a reference to the watch expression model object.
         
         @return reference to the watch expression model object
-            (WatchPointModel)
+        @rtype WatchPointModel
         """
         return self.watchpointModel
     
@@ -1841,7 +2149,8 @@
         """
         Public method to test, if the debug server is connected to a backend.
         
-        @return flag indicating a connection (boolean)
+        @return flag indicating a connection
+        @rtype bool
         """
         return self.debuggerInterface and self.debuggerInterface.isConnected()
     
@@ -1862,3 +2171,49 @@
         @type bool
         """
         self.debugging = on
+    
+    def signalClientDebuggerId(self, debuggerId):
+        """
+        Public method to signal the receipt of a new debugger ID.
+        
+        This signal indicates, that a new debugger backend has connected.
+        
+        @param debuggerId ID of the newly connected debugger backend
+        @type str
+        """
+        self.clientDebuggerId.emit(debuggerId)
+    
+    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)
+        self.__restoreNoDebugList(debuggerId)
+    
+    def __restoreNoDebugList(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.debuggerInterface.remoteNoDebugList(
+                debuggerId, self.__multiprocessNoDebugList)
--- a/eric6/Debugger/DebugUI.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Debugger/DebugUI.py	Sat Feb 22 17:03:43 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()
@@ -107,6 +108,12 @@
                 'DebugInfo/ForkAutomatically', False))
         self.forkIntoChild = Preferences.toBool(
             Preferences.Prefs.settings.value('DebugInfo/ForkIntoChild', False))
+        self.enableMultiprocess = Preferences.toBool(
+            Preferences.Prefs.settings.value(
+                'DebugInfo/EnableMultiprocess', False))
+        self.multiprocessNoDebugHistory = Preferences.toList(
+            Preferences.Prefs.settings.value(
+                'DebugInfo/MultiprocessNoDebugHistory'))
         
         self.lastDebuggedFile = None
         self.lastStartAction = 0    # 0=None, 1=Script, 2=Project
@@ -125,6 +132,7 @@
         debugServer.clientGone.connect(self.__clientGone)
         debugServer.clientLine.connect(self.__clientLine)
         debugServer.clientExit.connect(self.__clientExit)
+        debugServer.lastClientExited.connect(self.__lastClientExited)
         debugServer.clientSyntaxError.connect(self.__clientSyntaxError)
         debugServer.clientException.connect(self.__clientException)
         debugServer.clientSignal.connect(self.__clientSignal)
@@ -137,8 +145,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)
@@ -915,6 +921,7 @@
         self.argvHistory = []
         self.wdHistory = []
         self.envHistory = []
+        self.multiprocessNoDebugHistory = []
         
         Preferences.Prefs.settings.setValue(
             'DebugInfo/ArgumentsHistory', self.argvHistory)
@@ -922,6 +929,9 @@
             'DebugInfo/WorkingDirectoryHistory', self.wdHistory)
         Preferences.Prefs.settings.setValue(
             'DebugInfo/EnvironmentHistory', self.envHistory)
+        Preferences.Prefs.settings.setValue(
+            'DebugInfo/MultiprocessNoDebugHistory',
+            self.multiprocessNoDebugHistory)
         
     def shutdown(self):
         """
@@ -956,6 +966,11 @@
             'DebugInfo/ForkAutomatically', self.forkAutomatically)
         Preferences.Prefs.settings.setValue(
             'DebugInfo/ForkIntoChild', self.forkIntoChild)
+        Preferences.Prefs.settings.setValue(
+            'DebugInfo/EnableMultiprocess', self.enableMultiprocess)
+        Preferences.Prefs.settings.setValue(
+            'DebugInfo/MultiprocessNoDebugHistory',
+            self.multiprocessNoDebugHistory)
         
     def shutdownServer(self):
         """
@@ -995,13 +1010,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()
@@ -1009,15 +1029,18 @@
             self.ui.setDebugProfile()
         self.viewmanager.setFileLine(fn, line)
         if not forStack:
-            self.__getThreadList()
-            self.__getClientVariables()
+            self.__getThreadList(debuggerId)
+            self.__getClientVariables(debuggerId)
 
         self.debugActGrp.setEnabled(True)
-        
-    def __clientExit(self, status, message, quiet):
+    
+    @pyqtSlot(str, int, str, bool)
+    def __clientExit(self, program, status, message, quiet):
         """
         Private method to handle the debugged program terminating.
         
+        @param program name of the exited program
+        @type str
         @param status exit code of the debugged program
         @type int
         @param message exit message of the debugged program
@@ -1025,11 +1048,9 @@
         @param quiet flag indicating to suppress exit info display
         @type bool
         """
-        self.viewmanager.exit()
-
-        self.__resetUI()
-        
         if not quiet:
+            if not program:
+                program = self.ui.currentProg
             if (
                 not Preferences.getDebugger("SuppressClientExit") or
                 status != 0
@@ -1039,18 +1060,19 @@
                         Utilities.html_uencode(message))
                 else:
                     info = ""
-                if self.ui.currentProg is None:
+                if program is None:
                     E5MessageBox.information(
-                        self.ui, Program,
-                        self.tr('<p>The program has terminated with an exit'
-                                ' status of {0}.</p>{1}').format(status, info))
+                        self.ui, Program, self.tr(
+                            '<p>The program has terminated with an exit'
+                            ' status of {0}.</p>{1}').format(status, info)
+                    )
                 else:
                     E5MessageBox.information(
-                        self.ui, Program,
-                        self.tr('<p><b>{0}</b> has terminated with an exit'
-                                ' status of {1}.</p>{2}')
-                            .format(Utilities.normabspath(self.ui.currentProg),
-                                    status, info))
+                        self.ui, Program, self.tr(
+                            '<p><b>{0}</b> has terminated with an exit'
+                            ' status of {1}.</p>{2}').format(
+                            Utilities.normabspath(program), status, info)
+                    )
             else:
                 if message:
                     info = self.tr("Message: {0}").format(
@@ -1058,7 +1080,7 @@
                 else:
                     info = ""
                 if self.ui.notificationsEnabled():
-                    if self.ui.currentProg is None:
+                    if program is None:
                         msg = self.tr(
                             'The program has terminated with an exit status of'
                             ' {0}.\n{1}').format(status, info)
@@ -1066,23 +1088,30 @@
                         msg = self.tr(
                             '"{0}" has terminated with an exit status of'
                             ' {1}.\n{2}').format(
-                            os.path.basename(self.ui.currentProg), status,
-                            info)
+                            os.path.basename(program), status, info)
                     self.ui.showNotification(
                         UI.PixmapCache.getPixmap("debug48.png"),
                         self.tr("Program terminated"), msg)
                 else:
-                    if self.ui.currentProg is None:
+                    if program is None:
                         self.appendStdout.emit(self.tr(
                             'The program has terminated with an exit status'
-                            ' of {0}.\n{1}\n').format(status, info))
+                            ' of {0}.\n{1}\n').format(status, info)
+                        )
                     else:
                         self.appendStdout.emit(self.tr(
                             '"{0}" has terminated with an exit status of'
                             ' {1}.\n{2}\n').format(
-                            Utilities.normabspath(self.ui.currentProg), status,
-                            info))
-
+                            Utilities.normabspath(program), status, info)
+                        )
+    
+    def __lastClientExited(self):
+        """
+        Private slot handling the exit of the last client.
+        """
+        self.viewmanager.exit()
+        self.__resetUI()
+    
     def __clientSyntaxError(self, message, filename, lineNo, characterNo):
         """
         Private method to handle a syntax error in the debugged program.
@@ -1125,13 +1154,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 +1250,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
@@ -1226,13 +1261,14 @@
         
         if self.lastAction != -1:
             if self.lastAction == 2:
-                self.__specialContinue()
+                self.__specialContinue(debuggerId)
             else:
-                self.debugActions[self.lastAction]()
+                self.debugActions[self.lastAction](debuggerId)
         else:
-            self.__continue()
+            self.__continue(debuggerId)
         
-    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 +1282,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,71 +1308,98 @@
                 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):
+    def __clientBreakConditionError(self, filename, lineno, debuggerId):
         """
         Private method to handle a condition error of a breakpoint.
         
-        @param filename filename of the breakpoint (string)
-        @param lineno linenumber of the breakpoint (integer)
+        @param filename filename of the breakpoint
+        @type str
+        @param lineno line umber of the breakpoint
+        @type int
+        @param debuggerId ID of the debugger backend
+        @type str
         """
         E5MessageBox.critical(
             self.ui,
@@ -1364,13 +1429,16 @@
             model.setBreakPointByIndex(index, fn, line,
                                        (cond, temp, enabled, count))
         
-    def __clientWatchConditionError(self, cond):
+    def __clientWatchConditionError(self, cond, debuggerId):
         """
         Private method to handle a expression error of a watch expression.
         
         Note: This can only happen for normal watch expressions
         
-        @param cond expression of the watch expression (string)
+        @param cond expression of the watch expression
+        @type str
+        @param debuggerId ID of the debugger backend
+        @type str
         """
         E5MessageBox.critical(
             self.ui,
@@ -1664,8 +1732,9 @@
             self.setArgvHistory("", clearHistories=True)
             self.setWdHistory("", clearHistories=True)
             self.setEnvHistory("", clearHistories=True)
+            self.setMultiprocessNoDebugHistory("", clearHistories=True)
         elif dlg.historiesModified():
-            argvHistory, wdHistory, envHistory = dlg.getHistories()
+            argvHistory, wdHistory, envHistory, _ = dlg.getHistories()
             self.setArgvHistory("", history=argvHistory)
             self.setWdHistory("", history=wdHistory)
             self.setEnvHistory("", history=envHistory)
@@ -1799,8 +1868,9 @@
             self.setArgvHistory("", clearHistories=True)
             self.setWdHistory("", clearHistories=True)
             self.setEnvHistory("", clearHistories=True)
+            self.setMultiprocessNoDebugHistory("", clearHistories=True)
         elif dlg.historiesModified():
-            argvHistory, wdHistory, envHistory = dlg.getHistories()
+            argvHistory, wdHistory, envHistory, _ = dlg.getHistories()
             self.setArgvHistory("", history=argvHistory)
             self.setWdHistory("", history=wdHistory)
             self.setEnvHistory("", history=envHistory)
@@ -1938,8 +2008,9 @@
             self.setArgvHistory("", clearHistories=True)
             self.setWdHistory("", clearHistories=True)
             self.setEnvHistory("", clearHistories=True)
+            self.setMultiprocessNoDebugHistory("", clearHistories=True)
         elif dlg.historiesModified():
-            argvHistory, wdHistory, envHistory = dlg.getHistories()
+            argvHistory, wdHistory, envHistory, _ = dlg.getHistories()
             self.setArgvHistory("", history=argvHistory)
             self.setWdHistory("", history=wdHistory)
             self.setEnvHistory("", history=envHistory)
@@ -1980,12 +2051,15 @@
             self.envHistory, self.exceptions, self.ui, 0,
             tracePython=self.tracePython, autoClearShell=self.autoClearShell,
             autoContinue=self.autoContinue, autoFork=self.forkAutomatically,
-            forkChild=self.forkIntoChild)
+            forkChild=self.forkIntoChild,
+            enableMultiprocess=self.enableMultiprocess,
+            multiprocessNoDebugHistory=self.multiprocessNoDebugHistory)
         if dlg.exec_() == QDialog.Accepted:
             (lastUsedVenvName, argv, wd, env, exceptions, clearShell,
              console) = dlg.getData()
-            tracePython, autoContinue, forkAutomatically, forkIntoChild = (
-                dlg.getDebugData())
+            (tracePython, autoContinue, forkAutomatically, forkIntoChild,
+             enableMultiprocess, multiprocessNoDebug,
+             ) = dlg.getDebugData()
             
             if debugProject:
                 fn = self.project.getMainScript(True)
@@ -2060,6 +2134,10 @@
             self.forkAutomatically = forkAutomatically
             self.forkIntoChild = forkIntoChild
             
+            # Save the multiprocess debugging data
+            self.enableMultiprocess = enableMultiprocess
+            self.setMultiprocessNoDebugHistory(multiprocessNoDebug)
+            
             # Hide all error highlights
             self.viewmanager.unhighlight()
             
@@ -2085,7 +2163,9 @@
                     autoContinue=autoContinue, forProject=debugProject,
                     runInConsole=console, autoFork=forkAutomatically,
                     forkChild=forkIntoChild, clientType=self.clientType,
-                    enableCallTrace=enableCallTrace)
+                    enableCallTrace=enableCallTrace,
+                    enableMultiprocess=enableMultiprocess,
+                    multiprocessNoDebug=multiprocessNoDebug)
                 
                 if (
                     self.debugServer.isClientProcessUp() and
@@ -2100,11 +2180,14 @@
             self.setArgvHistory("", clearHistories=True)
             self.setWdHistory("", clearHistories=True)
             self.setEnvHistory("", clearHistories=True)
+            self.setMultiprocessNoDebugHistory("", clearHistories=True)
         elif dlg.historiesModified():
-            argvHistory, wdHistory, envHistory = dlg.getHistories()
+            (argvHistory, wdHistory, envHistory,
+             noDebugHistory) = dlg.getHistories()
             self.setArgvHistory("", history=argvHistory)
             self.setWdHistory("", history=wdHistory)
             self.setEnvHistory("", history=envHistory)
+            self.setMultiprocessNoDebugHistory("", history=noDebugHistory)
     
     def __doRestart(self):
         """
@@ -2158,6 +2241,7 @@
                 enableCallTrace = self.debugViewer.isCallTraceEnabled()
                 self.debugViewer.clearCallTrace()
                 self.debugViewer.setCallTraceToProjectMode(forProject)
+                multiprocessNoDebug = self.multiprocessNoDebugHistory[0]
                 
                 # Ask the client to debug the new program.
                 self.debugServer.remoteLoad(
@@ -2170,7 +2254,9 @@
                     autoFork=self.forkAutomatically,
                     forkChild=self.forkIntoChild,
                     clientType=self.clientType,
-                    enableCallTrace=enableCallTrace)
+                    enableCallTrace=enableCallTrace,
+                    enableMultiprocess=self.enableMultiprocess,
+                    multiprocessNoDebug=multiprocessNoDebug)
                 
                 # Signal that we have started a debugging session
                 self.debuggingStarted.emit(fn)
@@ -2236,75 +2322,124 @@
         # Initialize the call stack viewer
         self.debugViewer.initCallStackViewer(False)
         
-    def __continue(self):
+    def __continue(self, debuggerId=""):
         """
         Private method to handle the Continue action.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
+        if not debuggerId:
+            debuggerId = self.getSelectedDebuggerId()
+        
         self.lastAction = 0
         self.__enterRemote()
-        self.debugServer.remoteContinue()
+        self.debugServer.remoteContinue(debuggerId)
 
-    def __specialContinue(self):
+    def __specialContinue(self, debuggerId=""):
         """
         Private method to handle the Special Continue action.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
+        if not debuggerId:
+            debuggerId = self.getSelectedDebuggerId()
+        
         self.lastAction = 2
         self.__enterRemote()
-        self.debugServer.remoteContinue(1)
+        self.debugServer.remoteContinue(debuggerId, 1)
 
-    def __step(self):
+    def __step(self, debuggerId=""):
         """
         Private method to handle the Step action.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
+        if not debuggerId:
+            debuggerId = self.getSelectedDebuggerId()
+        
         self.lastAction = 1
         self.__enterRemote()
-        self.debugServer.remoteStep()
+        self.debugServer.remoteStep(debuggerId)
 
-    def __stepOver(self):
+    def __stepOver(self, debuggerId=""):
         """
         Private method to handle the Step Over action.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
+        if not debuggerId:
+            debuggerId = self.getSelectedDebuggerId()
+        
         self.lastAction = 2
         self.__enterRemote()
-        self.debugServer.remoteStepOver()
+        self.debugServer.remoteStepOver(debuggerId)
 
-    def __stepOut(self):
+    def __stepOut(self, debuggerId=""):
         """
         Private method to handle the Step Out action.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
+        if not debuggerId:
+            debuggerId = self.getSelectedDebuggerId()
+        
         self.lastAction = 3
         self.__enterRemote()
-        self.debugServer.remoteStepOut()
+        self.debugServer.remoteStepOut(debuggerId)
 
-    def __stepQuit(self):
+    def __stepQuit(self, debuggerId=""):
         """
         Private method to handle the Step Quit action.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
+        if not debuggerId:
+            debuggerId = self.getSelectedDebuggerId()
+        
         self.lastAction = 4
         self.__enterRemote()
-        self.debugServer.remoteStepQuit()
+        self.debugServer.remoteStepQuit(debuggerId)
         self.__resetUI()
 
-    def __runToCursor(self):
+    def __runToCursor(self, debuggerId=""):
         """
         Private method to handle the Run to Cursor action.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
+        if not debuggerId:
+            debuggerId = self.getSelectedDebuggerId()
+        
         self.lastAction = 0
         aw = self.viewmanager.activeWindow()
         line = aw.getCursorPosition()[0] + 1
         self.__enterRemote()
         self.debugServer.remoteBreakpoint(
+            self.getSelectedDebuggerId(),
             aw.getFileName(), line, 1, None, 1)
-        self.debugServer.remoteContinue()
+        self.debugServer.remoteContinue(debuggerId)
 
-    def __moveInstructionPointer(self):
+    def __moveInstructionPointer(self, debuggerId=""):
         """
         Private method to move the instruction pointer to a different line.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
+        if not debuggerId:
+            debuggerId = self.getSelectedDebuggerId()
+        
         self.lastAction = 0
         aw = self.viewmanager.activeWindow()
         line = aw.getCursorPosition()[0] + 1
-        self.debugServer.remoteMoveIP(line)
+        self.debugServer.remoteMoveIP(debuggerId, line)
 
     def __enterRemote(self):
         """
@@ -2325,3 +2460,55 @@
         @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()
+    
+    def setDebugActionsEnabled(self, enable):
+        """
+        Public method to set the enabled state of the debug actions.
+        
+        @param enable enable state to be set
+        @type bool
+        """
+        self.debugActGrp.setEnabled(enable)
+
+    def setMultiprocessNoDebugHistory(self, noDebugList, clearHistories=False,
+                                      history=None):
+        """
+        Public slot to initialize the no debug list history.
+        
+        @param noDebugList whitespace separated list of progframs not to be
+            debugged
+        @type str
+        @param clearHistories flag indicating, that the list should be cleared
+        @type bool
+        @param history list of history entries to be set
+        @type list of str
+        """
+        if clearHistories:
+            del self.multiprocessNoDebugHistory[1:]
+        elif history is not None:
+            self.multiprocessNoDebugHistory = history[:]
+        else:
+            if noDebugList in self.multiprocessNoDebugHistory:
+                self.multiprocessNoDebugHistory.remove(noDebugList)
+            self.multiprocessNoDebugHistory.insert(0, noDebugList)
--- a/eric6/Debugger/DebugViewer.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Debugger/DebugViewer.py	Sat Feb 22 17:03:43 2020 +0100
@@ -8,25 +8,25 @@
 
 The views avaliable are:
 <ul>
-  <li>variables viewer for global variables</li>
-  <li>variables viewer for local variables</li>
+  <li>selector showing all connected debugger backends</li>
+  <li>variables viewer for global variables for the selected debug client</li>
+  <li>variables viewer for local variables for the selected debug client</li>
+  <li>call stack viewer for the selected debug client</li>
   <li>call trace viewer</li>
   <li>viewer for breakpoints</li>
   <li>viewer for watch expressions</li>
   <li>viewer for exceptions</li>
-  <li>viewer for threads</li>
-  <li>a file browser (optional)</li>
-  <li>an interpreter shell (optional)</li>
+  <li>viewer for threads for the selected debug client</li>
 </ul>
 """
 
 
 import os
 
-from PyQt5.QtCore import pyqtSignal
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, 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
@@ -50,6 +50,8 @@
     sourceFile = pyqtSignal(str, int)
     preferencesChanged = pyqtSignal()
     
+    ThreadIdRole = Qt.UserRole + 1
+    
     def __init__(self, debugServer, parent=None):
         """
         Constructor
@@ -62,12 +64,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.__mainLayout.setContentsMargins(0, 3, 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 +126,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 +180,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 +198,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 +207,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 +221,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 +232,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 +241,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())
         
@@ -230,8 +251,7 @@
         self.__mainLayout.addWidget(QLabel(self.tr("Threads:")))
         self.__threadList = QTreeWidget()
         self.__threadList.setHeaderLabels(
-            [self.tr("ID"), self.tr("Name"),
-             self.tr("State"), ""])
+            [self.tr("Name"), self.tr("State"), ""])
         self.__threadList.setSortingEnabled(True)
         self.__mainLayout.addWidget(self.__threadList)
         
@@ -245,11 +265,35 @@
         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.clientDebuggerId.connect(
+            self.__clientDebuggerId)
+        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.clientExit.connect(
+            self.__clientExit)
+        
+        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 +308,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 +331,7 @@
         self.__threadList.clear()
         self.__tabWidget.setCurrentWidget(self.glvWidget)
         self.breakpointViewer.handleResetUI()
+        self.__debuggersCombo.clear()
         
     def initCallStackViewer(self, projectMode):
         """
@@ -351,24 +402,107 @@
         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
+        """
+        self.__setDebuggerIconAndState(debuggerId, "break", "broken")
+        if debuggerId != self.getSelectedDebuggerId():
+            self.__debuggersCombo.setCurrentText(debuggerId)
+    
+    @pyqtSlot(str, int, str, bool, str)
+    def __clientExit(self, program, status, message, quiet, debuggerId):
+        """
+        Private method to handle the debugged program terminating.
         
+        @param program name of the exited program
+        @type str
+        @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 debuggerId == self.getSelectedDebuggerId():
+            # the current client has exited
+            self.globalsViewer.handleResetUI()
+            self.localsViewer.handleResetUI()
+            self.setGlobalsFilter()
+            self.setLocalsFilter()
+            self.sourceButton.setEnabled(False)
+            self.currentStack = None
+            self.stackComboBox.clear()
+            self.__threadList.clear()
+        index = self.__debuggersCombo.findText(debuggerId, Qt.MatchExactly)
+        self.__debuggersCombo.removeItem(index)
+    
+    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
+        """
+        self.__setDebuggerIconAndState(debuggerId, "syntaxError22",
+                                       "exception")
+    
+    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
+        """
+        self.__setDebuggerIconAndState(debuggerId, "exceptions", "exception")
+    
     def setVariablesFilter(self, globalsFilter, localsFilter):
         """
         Public slot to set the local variables filter.
@@ -396,12 +530,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):
         """
@@ -409,8 +545,10 @@
         """
         if self.debugServer.isDebugging():
             filterStr = self.globalsFilterEdit.text()
-            self.debugServer.remoteClientSetFilter(1, filterStr)
-            self.debugServer.remoteClientVariables(2, self.globalsFilter)
+            self.debugServer.remoteClientSetFilter(
+                self.getSelectedDebuggerId(), 1, filterStr)
+            self.debugServer.remoteClientVariables(
+                self.getSelectedDebuggerId(), 2, self.globalsFilter)
         
     def setLocalsFilter(self):
         """
@@ -418,10 +556,12 @@
         """
         if self.debugServer.isDebugging():
             filterStr = self.localsFilterEdit.text()
-            self.debugServer.remoteClientSetFilter(0, filterStr)
+            self.debugServer.remoteClientSetFilter(
+                self.getSelectedDebuggerId(), 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 +589,70 @@
         """
         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 = "break"
+                    if debugStatus < 1:
+                        debugStatus = 0
+                else:
+                    state = self.tr("running")
+                    icon = "mediaPlaybackStart"
+                itm = QTreeWidgetItem(self.__threadList,
+                                      [thread['name'], state])
+                itm.setData(0, self.ThreadIdRole, thread['id'])
+                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"
+            state = "running"
+        elif debugStatus == 0:
+            icon = "break"
+            state = "broken"
+        else:
+            icon = "exceptions"
+            state = "exception"
+        self.__setDebuggerIconAndState(debuggerId, icon, state)
+    
     def __threadSelected(self, current, previous):
         """
         Private slot to handle the selection of a thread in the thread list.
@@ -487,8 +662,8 @@
             (QTreeWidgetItem)
         """
         if current is not None and self.__doThreadListUpdate:
-            tid = int(current.text(0))
-            self.debugServer.remoteSetThread(tid)
+            tid = current.data(0, self.ThreadIdRole)
+            self.debugServer.remoteSetThread(self.getSelectedDebuggerId(), tid)
     
     def __callStackFrameSelected(self, frameNo):
         """
@@ -499,3 +674,71 @@
         """
         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)
+            self.debugUI.setDebugActionsEnabled(
+                self.getSelectedDebuggerState() != "running")
+            self.__showSource()
+    
+    def __clientDebuggerId(self, debuggerId):
+        """
+        Private slot to receive the ID of a newly connected debugger backend.
+        
+        @param debuggerId ID of a newly connected debugger backend
+        @type str
+        """
+        self.__debuggersCombo.addItem(debuggerId)
+    
+    def getSelectedDebuggerId(self):
+        """
+        Public method to get the currently selected debugger ID.
+        
+        @return selected debugger ID
+        @rtype str
+        """
+        return self.__debuggersCombo.currentText()
+    
+    def getSelectedDebuggerState(self):
+        """
+        Public method to get the currently selected debugger's state.
+        
+        @return selected debugger's state (broken, exception, running)
+        @rtype str
+        """
+        return self.__debuggersCombo.currentData()
+    
+    def __setDebuggerIconAndState(self, debuggerId, iconName, state):
+        """
+        Private method to set the icon for a specific debugger ID.
+        
+        @param debuggerId ID of the debugger backend (empty ID means the
+            currently selected one)
+        @type str
+        @param iconName name of the icon to be used
+        @type str
+        @param state state of the debugger (broken, exception, running)
+        @type str
+        """
+        if debuggerId:
+            index = self.__debuggersCombo.findText(debuggerId, Qt.MatchExactly)
+        else:
+            index = self.__debuggersCombo.currentIndex()
+        if index >= 0:
+            self.__debuggersCombo.setItemIcon(
+                index, UI.PixmapCache.getIcon(iconName))
+            self.__debuggersCombo.setItemData(index, state)
--- a/eric6/Debugger/DebuggerInterfaceNone.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Debugger/DebuggerInterfaceNone.py	Sat Feb 22 17:03:43 2020 +0100
@@ -16,6 +16,7 @@
 ClientTypeAssociations = []
 
 
+# TODO: synchronize debugger interface with Python one
 class DebuggerInterfaceNone(QObject):
     """
     Class implementing a dummy debugger interface for the debug server.
--- a/eric6/Debugger/DebuggerInterfacePython.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Debugger/DebuggerInterfacePython.py	Sat Feb 22 17:03:43 2020 +0100
@@ -51,6 +51,7 @@
         
         self.__isNetworked = True
         self.__autoContinue = False
+        self.__autoContinued = []
         
         self.debugServer = debugServer
         self.passive = passive
@@ -58,8 +59,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
@@ -85,26 +88,32 @@
         
         # attribute to remember the name of the executed script
         self.__scriptName = ""
-
+    
     def __identityTranslation(self, fn, remote2local=True):
         """
         Private method to perform the identity path translation.
         
-        @param fn filename to be translated (string)
+        @param fn filename to be translated
+        @type str
         @param remote2local flag indicating the direction of translation
             (False = local to remote, True = remote to local [default])
-        @return translated filename (string)
+        @type bool
+        @return translated filename
+        @rtype str
         """
         return fn
-        
+    
     def __remoteTranslation(self, fn, remote2local=True):
         """
         Private method to perform the path translation.
         
-        @param fn filename to be translated (string)
+        @param fn filename to be translated
+        @type str
         @param remote2local flag indicating the direction of translation
             (False = local to remote, True = remote to local [default])
-        @return translated filename (string)
+        @type bool
+        @return translated filename
+        @rtype str
         """
         if remote2local:
             path = fn.replace(self.translateRemote, self.translateLocal)
@@ -116,7 +125,7 @@
                 path = path.replace("\\", "/")
         
         return path
-        
+    
     def __startProcess(self, program, arguments, environment=None,
                        workingDir=None):
         """
@@ -147,7 +156,7 @@
             proc = None
         
         return proc
-        
+    
     def startRemote(self, port, runInConsole, venvName, originalPathString,
                     workingDir=None):
         """
@@ -217,6 +226,10 @@
             redirect = str(Preferences.getDebugger("Python3Redirect"))
             noencoding = (Preferences.getDebugger("Python3NoEncoding") and
                           '--no-encoding' or '')
+        multiprocessEnabled = (
+            '--multiprocess' if Preferences.getDebugger("MultiProcessEnabled")
+            else ''
+        )
         
         if Preferences.getDebugger("RemoteDbgEnabled"):
             ipaddr = self.debugServer.getHostAddress(False)
@@ -226,8 +239,12 @@
                 rhost = "localhost"
             if rexec:
                 args = Utilities.parseOptionString(rexec) + [
-                    rhost, interpreter, debugClient, noencoding, str(port),
-                    redirect, ipaddr]
+                    rhost, interpreter, debugClient]
+                if noencoding:
+                    args.append(noencoding)
+                if multiprocessEnabled:
+                    args.append(multiprocessEnabled)
+                args.extend([str(port), redirect, ipaddr])
                 if Utilities.isWindowsPlatform():
                     if not os.path.splitext(args[0])[1]:
                         for ext in [".exe", ".com", ".cmd", ".bat"]:
@@ -292,8 +309,12 @@
             ccmd = Preferences.getDebugger("ConsoleDbgCommand")
             if ccmd:
                 args = Utilities.parseOptionString(ccmd) + [
-                    interpreter, os.path.abspath(debugClient), noencoding,
-                    str(port), '0', ipaddr]
+                    interpreter, os.path.abspath(debugClient)]
+                if noencoding:
+                    args.append(noencoding)
+                if multiprocessEnabled:
+                    args.append(multiprocessEnabled)
+                args.extend([str(port), '0', ipaddr])
                 args[0] = Utilities.getExecutablePath(args[0])
                 process = self.__startProcess(args[0], args[1:], clientEnv,
                                               workingDir=workingDir)
@@ -306,11 +327,14 @@
                             """ started.</p>"""))
                 return process, self.__isNetworked, interpreter
         
-        process = self.__startProcess(
-            interpreter,
-            [debugClient, noencoding, str(port), redirect, ipaddr],
-            clientEnv,
-            workingDir=workingDir)
+        args = [debugClient]
+        if noencoding:
+            args.append(noencoding)
+        if multiprocessEnabled:
+            args.append(multiprocessEnabled)
+        args.extend([str(port), redirect, ipaddr])
+        process = self.__startProcess(interpreter, args, clientEnv,
+                                      workingDir=workingDir)
         if process is None:
             self.__startedVenv = ""
             E5MessageBox.critical(
@@ -322,7 +346,7 @@
             self.__startedVenv = venvName
         
         return process, self.__isNetworked, interpreter
-
+    
     def startRemoteForProject(self, port, runInConsole, venvName,
                               originalPathString, workingDir=None):
         """
@@ -361,7 +385,12 @@
         
         redirect = str(project.getDebugProperty("REDIRECT"))
         noencoding = (
-            project.getDebugProperty("NOENCODING") and '--no-encoding' or '')
+            '--no-encoding' if project.getDebugProperty("NOENCODING") else ''
+        )
+        multiprocessEnabled = (
+            '--multiprocess' if Preferences.getDebugger("MultiProcessEnabled")
+            else ''
+        )
         
         venvManager = e5App().getObject("VirtualEnvManager")
         interpreter = venvManager.getVirtualenvInterpreter(venvName)
@@ -387,8 +416,12 @@
                 rhost = "localhost"
             if rexec:
                 args = Utilities.parseOptionString(rexec) + [
-                    rhost, interpreter, debugClient, noencoding, str(port),
-                    redirect, ipaddr]
+                    rhost, interpreter, debugClient]
+                if noencoding:
+                    args.append(noencoding)
+                if multiprocessEnabled:
+                    args.append(multiprocessEnabled)
+                args.extend([str(port), redirect, ipaddr])
                 if Utilities.isWindowsPlatform():
                     if not os.path.splitext(args[0])[1]:
                         for ext in [".exe", ".com", ".cmd", ".bat"]:
@@ -421,7 +454,7 @@
             else:
                 # remote shell command is missing
                 return None, self.__isNetworked, ""
-    
+        
         # set translation function
         self.translate = self.__identityTranslation
         
@@ -455,8 +488,12 @@
                     Preferences.getDebugger("ConsoleDbgCommand"))
             if ccmd:
                 args = Utilities.parseOptionString(ccmd) + [
-                    interpreter, os.path.abspath(debugClient), noencoding,
-                    str(port), '0', ipaddr]
+                    interpreter, os.path.abspath(debugClient)]
+                if noencoding:
+                    args.append(noencoding)
+                if multiprocessEnabled:
+                    args.append(multiprocessEnabled)
+                args.extend([str(port), '0', ipaddr])
                 args[0] = Utilities.getExecutablePath(args[0])
                 process = self.__startProcess(args[0], args[1:], clientEnv,
                                               workingDir=workingDir)
@@ -469,11 +506,14 @@
                             """ started.</p>"""))
                 return process, self.__isNetworked, interpreter
         
-        process = self.__startProcess(
-            interpreter,
-            [debugClient, noencoding, str(port), redirect, ipaddr],
-            clientEnv,
-            workingDir=workingDir)
+        args = [debugClient]
+        if noencoding:
+            args.append(noencoding)
+        if multiprocessEnabled:
+            args.append(multiprocessEnabled)
+        args.extend([str(port), redirect, ipaddr])
+        process = self.__startProcess(interpreter, args, clientEnv,
+                                      workingDir=workingDir)
         if process is None:
             self.__startedVenv = ""
             E5MessageBox.critical(
@@ -485,12 +525,13 @@
             self.__startedVenv = venvName
         
         return process, self.__isNetworked, interpreter
-
+    
     def getClientCapabilities(self):
         """
         Public method to retrieve the debug clients capabilities.
         
-        @return debug client capabilities (integer)
+        @return debug client capabilities
+        @rtype int
         """
         return self.clientCapabilities
     
@@ -498,30 +539,92 @@
         """
         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(debuggerId)
+            
+            self.debugServer.signalClientDebuggerId(debuggerId)
+            
+            if debuggerId == self.__master:
+                self.__flush()
+                self.debugServer.masterClientConnected()
+            
+            self.debugServer.initializeClient(debuggerId)
+            
+            # perform auto-continue except for master
+            if debuggerId != self.__master:
+                QTimer.singleShot(
+                    0, lambda: self.remoteContinue(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]
+                if debuggerId == self.__master:
+                    self.__master = None
+                if debuggerId in self.__autoContinued:
+                    self.__autoContinued.remove(debuggerId)
+                break
+        else:
+            if sock in self.__pendingConnections:
+                self.__pendingConnections.remove(sock)
+        
+        if not self.__connections:
+            # no active connections anymore
+            self.debugServer.signalLastClientExited()
+            self.__autoContinued.clear()
+    
+    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 +632,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,23 +686,35 @@
         
         @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):
+                   autoContinue=True, autoFork=False, forkChild=False,
+                   enableMultiprocess=False):
         """
         Public method to load a new program to debug.
         
-        @param fn the filename to debug (string)
-        @param argv the commandline arguments to pass to the program (string)
-        @param wd the working directory for the program (string)
-        @keyparam traceInterpreter flag indicating if the interpreter library
-            should be traced as well (boolean)
-        @keyparam autoContinue flag indicating, that the debugger should not
-            stop at the first executable line (boolean)
-        @keyparam autoFork flag indicating the automatic fork mode (boolean)
-        @keyparam forkChild flag indicating to debug the child after forking
-            (boolean)
+        @param fn the filename to debug
+        @type str
+        @param argv the commandline arguments to pass to the program
+        @type str
+        @param wd the working directory for the program
+        @type str
+        @param traceInterpreter flag indicating if the interpreter library
+            should be traced as well
+        @type bool
+        @param autoContinue flag indicating, that the debugger should not
+            stop at the first executable line
+        @type bool
+        @param autoFork flag indicating the automatic fork mode
+        @type bool
+        @param forkChild flag indicating to debug the child after forking
+        @type bool
+        @param enableMultiprocess flag indicating to perform multiprocess
+            debugging
+        @type bool
         """
         self.__autoContinue = autoContinue
         self.__scriptName = os.path.abspath(fn)
@@ -592,18 +728,23 @@
             "traceInterpreter": traceInterpreter,
             "autofork": autoFork,
             "forkChild": forkChild,
-        })
+            "multiprocess": enableMultiprocess,
+        }, self.__master)
     
     def remoteRun(self, fn, argv, wd, autoFork=False, forkChild=False):
         """
         Public method to load a new program to run.
         
-        @param fn the filename to run (string)
-        @param argv the commandline arguments to pass to the program (string)
-        @param wd the working directory for the program (string)
-        @keyparam autoFork flag indicating the automatic fork mode (boolean)
-        @keyparam forkChild flag indicating to debug the child after forking
-            (boolean)
+        @param fn the filename to run
+        @type str
+        @param argv the commandline arguments to pass to the program
+        @type str
+        @param wd the working directory for the program
+        @type str
+        @param autoFork flag indicating the automatic fork mode
+        @type bool
+        @param forkChild flag indicating to debug the child after forking
+        @type bool
         """
         self.__scriptName = os.path.abspath(fn)
         
@@ -615,17 +756,21 @@
             "argv": Utilities.parseOptionString(argv),
             "autofork": autoFork,
             "forkChild": forkChild,
-        })
+        }, self.__master)
     
     def remoteCoverage(self, fn, argv, wd, erase=False):
         """
         Public method to load a new program to collect coverage data.
         
-        @param fn the filename to run (string)
-        @param argv the commandline arguments to pass to the program (string)
-        @param wd the working directory for the program (string)
-        @keyparam erase flag indicating that coverage info should be
-            cleared first (boolean)
+        @param fn the filename to run
+        @type str
+        @param argv the commandline arguments to pass to the program
+        @type str
+        @param wd the working directory for the program
+        @type str
+        @param erase flag indicating that coverage info should be
+            cleared first
+        @type bool
         """
         self.__scriptName = os.path.abspath(fn)
         
@@ -636,17 +781,21 @@
             "filename": fn,
             "argv": Utilities.parseOptionString(argv),
             "erase": erase,
-        })
-
+        }, self.__master)
+    
     def remoteProfile(self, fn, argv, wd, erase=False):
         """
         Public method to load a new program to collect profiling data.
         
-        @param fn the filename to run (string)
-        @param argv the commandline arguments to pass to the program (string)
-        @param wd the working directory for the program (string)
-        @keyparam erase flag indicating that timing info should be cleared
-            first (boolean)
+        @param fn the filename to run
+        @type str
+        @param argv the commandline arguments to pass to the program
+        @type str
+        @param wd the working directory for the program
+        @type str
+        @param erase flag indicating that timing info should be cleared
+            first
+        @type bool
         """
         self.__scriptName = os.path.abspath(fn)
         
@@ -657,185 +806,286 @@
             "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)
     
-    def remoteRawInput(self, s):
+    def remoteRawInput(self, debuggerId, inputString):
         """
         Public method to send the raw input to the debugged program.
         
-        @param s the raw input (string)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param inputString the raw input
+        @type str
         """
         self.__sendJsonCommand("RawInput", {
-            "input": s,
-        })
+            "input": inputString,
+        }, debuggerId)
     
-    def remoteThreadList(self):
+    def remoteThreadList(self, debuggerId):
         """
         Public method to request the list of threads from the client.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.__sendJsonCommand("RequestThreadList", {})
-        
-    def remoteSetThread(self, tid):
+        self.__sendJsonCommand("RequestThreadList", {}, debuggerId)
+    
+    def remoteSetThread(self, debuggerId, tid):
         """
         Public method to request to set the given thread as current thread.
         
-        @param tid id of the thread (integer)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param tid id of the thread
+        @type int
         """
         self.__sendJsonCommand("RequestThreadSet", {
             "threadID": tid,
-        })
+        }, debuggerId)
+    
+    def remoteClientStack(self, debuggerId):
+        """
+        Public method to request the stack of the main thread.
         
-    def remoteClientVariables(self, scope, filterList, framenr=0, maxSize=0):
+        @param debuggerId ID of the debugger backend
+        @type str
+        """
+        self.__sendJsonCommand("RequestStack", {}, debuggerId)
+    
+    def remoteClientVariables(self, debuggerId, scope, filterList, framenr=0,
+                              maxSize=0):
         """
         Public method to request the variables of the debugged program.
         
+        @param debuggerId ID of the debugger backend
+        @type str
         @param scope the scope of the variables (0 = local, 1 = global)
         @type int
         @param filterList list of variable types to filter out
@@ -852,13 +1102,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,30 +1130,52 @@
             "scope": scope,
             "filters": filterList,
             "maxSize": maxSize,
-        })
+        }, debuggerId)
     
-    def remoteClientSetFilter(self, scope, filterStr):
+    def remoteClientSetFilter(self, debuggerId, scope, filterStr):
         """
         Public method to set a variables filter list.
         
+        @param debuggerId ID of the debugger backend
+        @type str
         @param scope the scope of the variables (0 = local, 1 = global)
+        @type int
         @param filterStr regexp string for variable names to filter out
-            (string)
+        @type str
         """
         self.__sendJsonCommand("RequestSetFilter", {
             "scope": scope,
             "filter": filterStr,
-        })
+        }, debuggerId)
     
-    def setCallTraceEnabled(self, on):
+    def setCallTraceEnabled(self, debuggerId, on):
         """
         Public method to set the call trace state.
         
-        @param on flag indicating to enable the call trace function (boolean)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param on flag indicating to enable the call trace function
+        @type bool
         """
         self.__sendJsonCommand("RequestCallTrace", {
             "enable": on,
-        })
+        }, debuggerId)
+    
+    def remoteNoDebugList(self, debuggerId, noDebugList):
+        """
+        Public method to set a list of programs not to be debugged.
+        
+        The programs given in the list will not be run under the control
+        of the multi process debugger.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param noDebugList list of Python programs not to be debugged
+        @type list of str
+        """
+        self.__sendJsonCommand("RequestSetNoDebugList", {
+            "noDebug": noDebugList,
+        }, debuggerId)
     
     def remoteBanner(self):
         """
@@ -909,22 +1183,28 @@
         """
         self.__sendJsonCommand("RequestBanner", {})
     
-    def remoteCapabilities(self):
+    def remoteCapabilities(self, debuggerId):
         """
         Public slot to get the debug clients capabilities.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.__sendJsonCommand("RequestCapabilities", {})
+        self.__sendJsonCommand("RequestCapabilities", {}, debuggerId)
     
-    def remoteCompletion(self, text):
+    def remoteCompletion(self, debuggerId, text):
         """
         Public slot to get the a list of possible commandline completions
         from the remote client.
         
-        @param text the text to be completed (string)
+        @param debuggerId ID of the debugger backend
+        @type str
+        @param text the text to be completed
+        @type str
         """
         self.__sendJsonCommand("RequestCompletion", {
             "text": text,
-        })
+        }, debuggerId)
     
     def remoteUTDiscover(self, syspath, workdir, discoveryStart):
         """
@@ -1024,33 +1304,40 @@
         """
         self.__sendJsonCommand("RequestUTStop", {})
     
-    def __askForkTo(self):
+    def __askForkTo(self, debuggerId):
         """
         Private method to ask the user which branch of a fork to follow.
+        
+        @param debuggerId ID of the debugger backend
+        @type str
         """
         selections = [self.tr("Parent Process"),
                       self.tr("Child process")]
         res, ok = QInputDialog.getItem(
             None,
             self.tr("Client forking"),
-            self.tr("Select the fork branch to follow."),
+            self.tr("Select the fork branch to follow (Debugger: {0}).")
+            .format(debuggerId),
             selections,
             0, False)
         if not ok or res == selections[0]:
             self.__sendJsonCommand("ResponseForkTo", {
                 "target": "parent",
-            })
+            }, debuggerId)
         else:
             self.__sendJsonCommand("ResponseForkTo", {
                 "target": "child",
-            })
+            }, debuggerId)
     
-    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 +1346,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 +1356,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 +1381,35 @@
         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)
+            if (
+                self.__autoContinue and
+                params["debuggerId"] not in self.__autoContinued
+            ):
+                self.__autoContinued.append(params["debuggerId"])
+                QTimer.singleShot(
+                    0, lambda: self.remoteContinue(params["debuggerId"]))
             else:
                 self.debugServer.signalClientLine(
-                    cf[0], int(cf[1]),
+                    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 +1420,120 @@
                 fromInfo["filename"], str(fromInfo["linenumber"]),
                 fromInfo["codename"],
                 toInfo["filename"], str(toInfo["linenumber"]),
-                toInfo["codename"])
+                toInfo["codename"],
+                params["debuggerId"])
         
         elif method == "ResponseVariables":
             self.debugServer.signalClientVariables(
-                params["scope"], params["variables"])
+                params["scope"], params["variables"], params["debuggerId"])
         
         elif method == "ResponseVariable":
             self.debugServer.signalClientVariable(
-                params["scope"], [params["variable"]] + params["variables"])
+                params["scope"], [params["variable"]] + params["variables"],
+                params["debuggerId"])
         
         elif method == "ResponseThreadList":
             self.debugServer.signalClientThreadList(
-                params["currentID"], params["threadList"])
+                params["currentID"], params["threadList"],
+                params["debuggerId"])
         
         elif method == "ResponseThreadSet":
-            self.debugServer.signalClientThreadSet()
+            self.debugServer.signalClientThreadSet(params["debuggerId"])
         
         elif method == "ResponseCapabilities":
             self.clientCapabilities = params["capabilities"]
-            self.debugServer.signalClientCapabilities(
-                params["capabilities"],
-                params["clientType"],
-                self.__startedVenv,
-            )
+            if params["debuggerId"] == self.__master:
+                # signal only for the master connection
+                self.debugServer.signalClientCapabilities(
+                    params["capabilities"],
+                    params["clientType"],
+                    self.__startedVenv,
+                )
         
         elif method == "ResponseBanner":
-            self.debugServer.signalClientBanner(
-                params["version"],
-                params["platform"],
-                params["dbgclient"],
-                self.__startedVenv,
-            )
+            if params["debuggerId"] == self.__master:
+                # signal only for the master connection
+                self.debugServer.signalClientBanner(
+                    params["version"],
+                    params["platform"],
+                    self.__startedVenv,
+                )
         
         elif method == "ResponseOK":
-            self.debugServer.signalClientStatement(False)
+            self.debugServer.signalClientStatement(False, params["debuggerId"])
         
         elif method == "ResponseContinue":
-            self.debugServer.signalClientStatement(True)
+            self.debugServer.signalClientStatement(True, params["debuggerId"])
         
         elif method == "RequestRaw":
             self.debugServer.signalClientRawInput(
-                params["prompt"], params["echo"])
+                params["prompt"], params["echo"], params["debuggerId"])
         
         elif method == "ResponseBPConditionError":
             fn = self.translate(params["filename"], True)
             self.debugServer.signalClientBreakConditionError(
-                fn, params["line"])
+                fn, params["line"], params["debuggerId"])
         
         elif method == "ResponseClearBreakpoint":
             fn = self.translate(params["filename"], True)
-            self.debugServer.signalClientClearBreak(fn, params["line"])
+            self.debugServer.signalClientClearBreak(
+                fn, params["line"], params["debuggerId"])
         
         elif method == "ResponseWatchConditionError":
             self.debugServer.signalClientWatchConditionError(
-                params["condition"])
+                params["condition"], params["debuggerId"])
         
         elif method == "ResponseClearWatch":
-            self.debugServer.signalClientClearWatch(params["condition"])
+            self.debugServer.signalClientClearWatch(
+                params["condition"], params["debuggerId"])
         
         elif method == "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["program"], params["status"], params["message"],
+                params["debuggerId"])
         
         elif method == "PassiveStartup":
             self.debugServer.passiveStartUp(
-                self.translate(params["filename"], True), params["exceptions"])
+                self.translate(params["filename"], True), params["exceptions"],
+                params["debuggerId"])
         
         elif method == "ResponseCompletion":
             self.debugServer.signalClientCompletionList(
-                params["completions"], params["text"])
+                params["completions"], params["text"], params["debuggerId"])
+        
+        ###################################################################
+        ## Unit test related stuff is not done with multi processing
+        ###################################################################
         
         elif method == "ResponseUTDiscover":
             self.debugServer.clientUtDiscovered(
@@ -1267,9 +1575,9 @@
                 params["testname"], params["id"])
         
         elif method == "RequestForkTo":
-            self.__askForkTo()
+            self.__askForkTo(params["debuggerId"])
     
-    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 +1585,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 +1599,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	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Debugger/ExceptionLogger.py	Sat Feb 22 17:03:43 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/StartDebugDialog.ui	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Debugger/StartDebugDialog.ui	Sat Feb 22 17:03:43 2020 +0100
@@ -7,7 +7,7 @@
     <x>0</x>
     <y>0</y>
     <width>550</width>
-    <height>333</height>
+    <height>332</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -157,57 +157,7 @@
     </layout>
    </item>
    <item>
-    <layout class="QGridLayout" name="gridLayout_2">
-     <item row="0" column="0">
-      <widget class="QCheckBox" name="exceptionCheckBox">
-       <property name="toolTip">
-        <string>Uncheck to disable exception reporting</string>
-       </property>
-       <property name="whatsThis">
-        <string>&lt;b&gt;Report exceptions&lt;/b&gt;
-&lt;p&gt;Uncheck this in order to disable exception reporting.&lt;/p&gt;</string>
-       </property>
-       <property name="text">
-        <string>Report exceptions</string>
-       </property>
-       <property name="shortcut">
-        <string>Alt+E</string>
-       </property>
-       <property name="checked">
-        <bool>true</bool>
-       </property>
-      </widget>
-     </item>
-     <item row="0" column="1">
-      <widget class="QCheckBox" name="clearShellCheckBox">
-       <property name="toolTip">
-        <string>Select to clear the display of the interpreter window</string>
-       </property>
-       <property name="whatsThis">
-        <string>&lt;b&gt;Clear interpreter window&lt;/b&gt;&lt;p&gt;This clears the display of the interpreter window before starting the debug client.&lt;/p&gt;</string>
-       </property>
-       <property name="text">
-        <string>Clear interpreter window</string>
-       </property>
-       <property name="checked">
-        <bool>true</bool>
-       </property>
-      </widget>
-     </item>
-     <item row="1" column="0">
-      <widget class="QCheckBox" name="consoleCheckBox">
-       <property name="toolTip">
-        <string>Select to start the debugger in a console window</string>
-       </property>
-       <property name="whatsThis">
-        <string>&lt;b&gt;Start in console&lt;/b&gt;
-&lt;p&gt;Select to start the debugger in a console window. The console command has to be configured on the Debugger-&amp;gt;General page&lt;/p&gt;</string>
-       </property>
-       <property name="text">
-        <string>Start in console</string>
-       </property>
-      </widget>
-     </item>
+    <layout class="QGridLayout" name="gridLayout">
      <item row="2" column="0">
       <widget class="QCheckBox" name="tracePythonCheckBox">
        <property name="toolTip">
@@ -237,9 +187,115 @@
        </property>
       </widget>
      </item>
+     <item row="0" column="1">
+      <widget class="QCheckBox" name="clearShellCheckBox">
+       <property name="toolTip">
+        <string>Select to clear the display of the interpreter window</string>
+       </property>
+       <property name="whatsThis">
+        <string>&lt;b&gt;Clear interpreter window&lt;/b&gt;&lt;p&gt;This clears the display of the interpreter window before starting the debug client.&lt;/p&gt;</string>
+       </property>
+       <property name="text">
+        <string>Clear interpreter window</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="0">
+      <widget class="QCheckBox" name="exceptionCheckBox">
+       <property name="toolTip">
+        <string>Uncheck to disable exception reporting</string>
+       </property>
+       <property name="whatsThis">
+        <string>&lt;b&gt;Report exceptions&lt;/b&gt;
+&lt;p&gt;Uncheck this in order to disable exception reporting.&lt;/p&gt;</string>
+       </property>
+       <property name="text">
+        <string>Report exceptions</string>
+       </property>
+       <property name="shortcut">
+        <string>Alt+E</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="0">
+      <widget class="QCheckBox" name="consoleCheckBox">
+       <property name="toolTip">
+        <string>Select to start the debugger in a console window</string>
+       </property>
+       <property name="whatsThis">
+        <string>&lt;b&gt;Start in console&lt;/b&gt;
+&lt;p&gt;Select to start the debugger in a console window. The console command has to be configured on the Debugger-&amp;gt;General page&lt;/p&gt;</string>
+       </property>
+       <property name="text">
+        <string>Start in console</string>
+       </property>
+      </widget>
+     </item>
     </layout>
    </item>
    <item>
+    <widget class="QGroupBox" name="multiprocessGroup">
+     <property name="toolTip">
+      <string>Select this to enable multi process debugging.</string>
+     </property>
+     <property name="whatsThis">
+      <string>&lt;b&gt;Multi Process Debugging&lt;/b&gt;&lt;p&gt;This enables debugging capability for multi process programs. Each started Python program is started within a new debugger.&lt;/p&gt;</string>
+     </property>
+     <property name="title">
+      <string>Multi Process Debugging</string>
+     </property>
+     <property name="checkable">
+      <bool>true</bool>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout_2">
+      <item>
+       <widget class="QLabel" name="TextLabel1_2">
+        <property name="text">
+         <string>Don't Debug:</string>
+        </property>
+        <property name="buddy">
+         <cstring>cmdlineCombo</cstring>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QComboBox" name="multiprocessNoDebugCombo">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="toolTip">
+         <string>Enter the list of programs not to be debugged separated by space</string>
+        </property>
+        <property name="whatsThis">
+         <string/>
+        </property>
+        <property name="editable">
+         <bool>true</bool>
+        </property>
+        <property name="insertPolicy">
+         <enum>QComboBox::InsertAtTop</enum>
+        </property>
+        <property name="sizeAdjustPolicy">
+         <enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
+        </property>
+        <property name="duplicatesEnabled">
+         <bool>false</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
     <widget class="QGroupBox" name="groupBox">
      <property name="title">
       <string>Forking</string>
@@ -310,6 +366,8 @@
   <tabstop>consoleCheckBox</tabstop>
   <tabstop>tracePythonCheckBox</tabstop>
   <tabstop>autoContinueCheckBox</tabstop>
+  <tabstop>multiprocessGroup</tabstop>
+  <tabstop>multiprocessNoDebugCombo</tabstop>
   <tabstop>forkModeCheckBox</tabstop>
   <tabstop>forkChildCheckBox</tabstop>
  </tabstops>
--- a/eric6/Debugger/StartDialog.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Debugger/StartDialog.py	Sat Feb 22 17:03:43 2020 +0100
@@ -29,7 +29,8 @@
                  exceptions,
                  parent=None, dialogType=0, modfuncList=None,
                  tracePython=False, autoClearShell=True, autoContinue=True,
-                 autoFork=False, forkChild=False):
+                 autoFork=False, forkChild=False, enableMultiprocess=False,
+                 multiprocessNoDebugHistory=None):
         """
         Constructor
         
@@ -56,21 +57,27 @@
                 <li>3 = start profile dialog</li>
                 </ul>
         @type int (0 to 3)
-        @keyparam modfuncList history list of module functions
+        @param modfuncList history list of module functions
         @type list of str
-        @keyparam tracePython flag indicating if the Python library should
+        @param tracePython flag indicating if the Python library should
             be traced as well
         @type bool
-        @keyparam autoClearShell flag indicating, that the interpreter window
+        @param autoClearShell flag indicating, that the interpreter window
             should be cleared automatically
         @type bool
-        @keyparam autoContinue flag indicating, that the debugger should not
+        @param autoContinue flag indicating, that the debugger should not
             stop at the first executable line
         @type bool
-        @keyparam autoFork flag indicating the automatic fork mode
+        @param autoFork flag indicating the automatic fork mode
+        @type bool
+        @param forkChild flag indicating to debug the child after forking
         @type bool
-        @keyparam forkChild flag indicating to debug the child after forking
+        @param enableMultiprocess flag indicating the support for multi process
+            debugging
         @type bool
+        @param multiprocessNoDebugHistory list of lists with programs not to be
+            debugged
+        @type list of str
         """
         super(StartDialog, self).__init__(parent)
         self.setModal(True)
@@ -127,11 +134,21 @@
         self.ui.venvComboBox.setCurrentIndex(venvIndex)
         
         if dialogType == 0:        # start debug dialog
+            enableMultiprocessGlobal = Preferences.getDebugger(
+                "MultiProcessEnabled")
             self.ui.tracePythonCheckBox.setChecked(tracePython)
             self.ui.tracePythonCheckBox.show()
             self.ui.autoContinueCheckBox.setChecked(autoContinue)
             self.ui.forkModeCheckBox.setChecked(autoFork)
             self.ui.forkChildCheckBox.setChecked(forkChild)
+            self.ui.multiprocessGroup.setEnabled(enableMultiprocessGlobal)
+            self.ui.multiprocessGroup.setChecked(
+                enableMultiprocess & enableMultiprocessGlobal)
+            self.ui.multiprocessNoDebugCombo.clear()
+            if multiprocessNoDebugHistory:
+                self.ui.multiprocessNoDebugCombo.addItems(
+                    multiprocessNoDebugHistory)
+                self.ui.multiprocessNoDebugCombo.setCurrentIndex(0)
         
         if dialogType == 1:       # start run dialog
             self.ui.forkModeCheckBox.setChecked(autoFork)
@@ -182,18 +199,22 @@
         
         @return a tuple of a flag indicating, if the Python library should be
             traced as well, a flag indicating, that the debugger should not
-            stop at the first executable line (boolean), a flag indicating,
-            that the debugger should fork automatically (boolean) and a flag
-            indicating, that the debugger should debug the child process after
-            forking automatically (boolean)
+            stop at the first executable line, a flag indicating, that the
+            debugger should fork automatically, a flag indicating, that the
+            debugger should debug the child process after forking
+            automatically, a flag indicating to support multi process debugging
+            and a space separated list of programs not to be debugged
+        @rtype tuple of (bool, bool, bool, bool, bool, str)
         """
         if self.dialogType == 0:
             return (self.ui.tracePythonCheckBox.isChecked(),
                     self.ui.autoContinueCheckBox.isChecked(),
                     self.ui.forkModeCheckBox.isChecked(),
-                    self.ui.forkChildCheckBox.isChecked())
+                    self.ui.forkChildCheckBox.isChecked(),
+                    self.ui.multiprocessGroup.isChecked(),
+                    self.ui.multiprocessNoDebugCombo.currentText())
         else:
-            return (False, False, False, False)
+            return (False, False, False, False, False, [])
         
     def getRunData(self):
         """
@@ -254,6 +275,11 @@
         self.ui.cmdlineCombo.addItem(cmdLine)
         self.ui.workdirPicker.addItem(workdir)
         self.ui.environmentCombo.addItem(environment)
+        
+        if self.dialogType == 0:
+            noDebugList = self.ui.multiprocessNoDebugCombo.currentText()
+            self.ui.multiprocessNoDebugCombo.clear()
+            self.ui.multiprocessNoDebugCombo.addItem(noDebugList)
     
     def __editHistory(self):
         """
@@ -265,6 +291,15 @@
             self.tr("Working Directory"),
             self.tr("Environment"),
         ]
+        combos = [
+            None,
+            self.ui.cmdlineCombo,
+            self.ui.workdirPicker,
+            self.ui.environmentCombo,
+        ]
+        if self.dialogType == 0:
+            histories.append(self.tr("No Debug Programs"))
+            combos.append(self.ui.multiprocessNoDebugCombo)
         historyKind, ok = QInputDialog.getItem(
             self,
             self.tr("Edit History"),
@@ -272,32 +307,27 @@
             histories,
             0, False)
         if ok and historyKind:
+            history = []
             historiesIndex = histories.index(historyKind)
             if historiesIndex == 2:
                 history = self.ui.workdirPicker.getPathItems()
             else:
-                history = []
-                if historiesIndex == 1:
-                    combo = self.ui.cmdlineCombo
-                else:
-                    combo = self.ui.environmentCombo
-                for index in range(combo.count()):
-                    history.append(combo.itemText(index))
+                combo = combos[historiesIndex]
+                if combo:
+                    for index in range(combo.count()):
+                        history.append(combo.itemText(index))
             
-            from .StartHistoryEditDialog import StartHistoryEditDialog
-            dlg = StartHistoryEditDialog(history, self)
-            if dlg.exec_() == QDialog.Accepted:
-                history = dlg.getHistory()
-                if historiesIndex == 1:
-                    combo = self.ui.cmdlineCombo
-                elif historiesIndex == 2:
-                    combo = self.ui.workdirPicker
-                else:
-                    combo = self.ui.environmentCombo
-                combo.clear()
-                combo.addItems(history)
-                
-                self.__historiesModified = True
+            if history:
+                from .StartHistoryEditDialog import StartHistoryEditDialog
+                dlg = StartHistoryEditDialog(history, self)
+                if dlg.exec_() == QDialog.Accepted:
+                    history = dlg.getHistory()
+                    combo = combos[historiesIndex]
+                    if combo:
+                        combo.clear()
+                        combo.addItems(history)
+                        
+                        self.__historiesModified = True
     
     def historiesModified(self):
         """
@@ -322,15 +352,24 @@
         Public method to get the lists of histories.
         
         @return tuple containing the histories of command line arguments,
-            working directories, environment settings and interpreters
+            working directories, environment settings and no debug programs
+            lists
         @rtype tuple of four list of str
         """
+        if self.dialogType == 0:
+            noDebugHistory = [
+                self.ui.multiprocessNoDebugCombo.itemText(index)
+                for index in range(self.ui.multiprocessNoDebugCombo.count())
+            ]
+        else:
+            noDebugHistory = None
         return (
             [self.ui.cmdlineCombo.itemText(index) for index in range(
                 self.ui.cmdlineCombo.count())],
             self.ui.workdirPicker.getPathItems(),
             [self.ui.environmentCombo.itemText(index) for index in range(
                 self.ui.environmentCombo.count())],
+            noDebugHistory,
         )
     
     def on_buttonBox_clicked(self, button):
--- a/eric6/Debugger/VariablesViewer.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Debugger/VariablesViewer.py	Sat Feb 22 17:03:43 2020 +0100
@@ -793,6 +793,7 @@
         variablesFilter = e5App().getObject("DebugUI").variablesFilter(
             self.__globalScope)
         e5App().getObject("DebugServer").remoteClientVariable(
+            e5App().getObject("DebugUI").getSelectedDebuggerId(),
             self.__globalScope, variablesFilter, pathlist, self.framenr)
     
     def setExpanded(self, index, state):
--- a/eric6/Preferences/ConfigurationPages/DebuggerGeneralPage.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Preferences/ConfigurationPages/DebuggerGeneralPage.py	Sat Feb 22 17:03:43 2020 +0100
@@ -137,6 +137,8 @@
             Preferences.getDebugger("PathTranslationRemote"))
         self.dbgTranslationLocalEdit.setText(
             Preferences.getDebugger("PathTranslationLocal"))
+        self.multiprocessCheckBox.setChecked(
+            Preferences.getDebugger("MultiProcessEnabled"))
         self.debugThreeStateBreakPoint.setChecked(
             Preferences.getDebugger("ThreeStateBreakPoints"))
         self.recentFilesSpinBox.setValue(
@@ -230,6 +232,9 @@
             "PathTranslationLocal",
             self.dbgTranslationLocalEdit.text())
         Preferences.setDebugger(
+            "MultiProcessEnabled",
+            self.multiprocessCheckBox.isChecked())
+        Preferences.setDebugger(
             "ThreeStateBreakPoints",
             self.debugThreeStateBreakPoint.isChecked())
         Preferences.setDebugger(
--- a/eric6/Preferences/ConfigurationPages/DebuggerGeneralPage.ui	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Preferences/ConfigurationPages/DebuggerGeneralPage.ui	Sat Feb 22 17:03:43 2020 +0100
@@ -479,6 +479,25 @@
     </widget>
    </item>
    <item>
+    <widget class="QGroupBox" name="groupBox_6">
+     <property name="title">
+      <string>Multi Process Debugging</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_8">
+      <item>
+       <widget class="QCheckBox" name="multiprocessCheckBox">
+        <property name="toolTip">
+         <string>Select to enable multiprocess debugging support globally</string>
+        </property>
+        <property name="text">
+         <string>Enable Multi Process Debugging Support</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
     <widget class="QGroupBox" name="groupBox_8">
      <property name="title">
       <string>Breakpoints</string>
@@ -804,6 +823,7 @@
   <tabstop>debugAutoSaveScriptsCheckBox</tabstop>
   <tabstop>automaticResetCheckBox</tabstop>
   <tabstop>dontShowClientExitCheckBox</tabstop>
+  <tabstop>multiprocessCheckBox</tabstop>
   <tabstop>debugThreeStateBreakPoint</tabstop>
   <tabstop>recentFilesSpinBox</tabstop>
   <tabstop>exceptionBreakCheckBox</tabstop>
--- a/eric6/Preferences/__init__.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Preferences/__init__.py	Sat Feb 22 17:03:43 2020 +0100
@@ -111,7 +111,9 @@
         # space separated list of Python2 extensions
         "PythonExtensions": ".py2 .pyw2 .ptl",
         # space separated list of Python3 extensions
-        "Python3Extensions": ".py .pyw .py3 .pyw3"
+        "Python3Extensions": ".py .pyw .py3 .pyw3",
+        # Global Multiprocess Debugging Support
+        "MultiProcessEnabled": True,
     }
     
     # defaults for the UI settings
@@ -1723,6 +1725,7 @@
                "Autosave", "ThreeStateBreakPoints",
                "SuppressClientExit", "BreakAlways",
                "AutoViewSourceCode", "ShowExceptionInShell",
+               "MultiProcessEnabled",
                ]:
         return toBool(prefClass.settings.value(
             "Debugger/" + key, prefClass.debuggerDefaults[key]))
--- a/eric6/QScintilla/Shell.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/QScintilla/Shell.py	Sat Feb 22 17:03:43 2020 +0100
@@ -16,7 +16,7 @@
 except ImportError:
     from ThirdParty.enum import Enum
 
-from PyQt5.QtCore import pyqtSignal, QFileInfo, Qt, QEvent
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, QFileInfo, Qt, QEvent
 from PyQt5.QtGui import QClipboard, QPalette, QFont
 from PyQt5.QtWidgets import (
     QDialog, QInputDialog, QApplication, QMenu, QWidget, QHBoxLayout,
@@ -244,6 +244,8 @@
         dbs.clientSignal.connect(self.__clientSignal)
         self.dbs = dbs
         
+        self.__debugUI = None
+        
         # Initialize instance variables.
         self.__initialise()
         self.prline = 0
@@ -603,6 +605,7 @@
         @param ui reference to the debugger UI object (DebugUI)
         """
         ui.exceptionInterrupt.connect(self.__writePrompt)
+        self.__debugUI = ui
         
     def __initialise(self):
         """
@@ -610,8 +613,10 @@
         """
         self.buff = ""
         self.inContinue = False
-        self.inRawMode = False
-        self.echoInput = True
+        self.__inRawMode = False
+        self.__echoInput = True
+        self.__rawModeDebuggerId = None
+        self.__rawModeQueue = []
         self.clientCapabilities = 0
         self.inCommandExecution = False
         self.interruptCommandExecution = False
@@ -817,7 +822,7 @@
         else:
             self.dbs.remoteBanner()
         
-    def __writeBanner(self, version, platform, dbgclient, venvName):
+    def __writeBanner(self, version, platform, venvName):
         """
         Private method to write a banner with info from the debug client.
         
@@ -825,8 +830,6 @@
         @type str
         @param platform platform of the remote interpreter
         @type str
-        @param dbgclient debug client variant used
-        @type str
         @param venvName name of the virtual environment
         @type str
         """
@@ -837,10 +840,8 @@
         else:
             self.__currentVenv = venvName
             version = version.replace("#", self.tr("No."))
-            if platform != "" and dbgclient != "":
-                self.__write(
-                    self.tr('{0} on {1}, {2}')
-                        .format(version, platform, dbgclient))
+            if platform != "":
+                self.__write(self.tr('{0} on {1}').format(version, platform))
             else:
                 self.__write(version)
             if venvName:
@@ -862,9 +863,10 @@
         """
         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:
+        if not self.__inRawMode:
             self.inContinue = more
             self.__writePrompt()
         self.inCommandExecution = False
@@ -1037,34 +1039,41 @@
         """
         self.__write(self.tr("StdErr: {0}").format(s))
         
-    def __raw_input(self, prompt, echo):
+    def __raw_input(self, prompt, echo, debuggerId):
         """
         Private method to handle raw input.
         
-        @param prompt prompt to be displayed
+        @param prompt the input prompt
         @type str
-        @param echo Flag indicating echoing of the input
+        @param echo flag indicating an echoing of the input
         @type bool
+        @param debuggerId ID of the debugger backend
+        @type str
         """
-        self.setFocus()
-        self.inRawMode = True
-        self.echoInput = echo
-        
-        # Get all text which is still waiting for output
-        QApplication.processEvents()
-        self.__flushQueuedText()
-        
-        self.__write(prompt)
-        line, col = self.__getEndPos()
-        self.setCursorPosition(line, col)
-        buf = self.text(line)
-        if buf.startswith(sys.ps1):
-            buf = buf.replace(sys.ps1, "")
-        if buf.startswith(sys.ps2):
-            buf = buf.replace(sys.ps2, "")
-        self.prompt = buf
-        # move cursor to end of line
-        self.moveCursorToEOL()
+        if self.__inRawMode:
+            # we are processing another raw input event already
+            self.__rawModeQueue.append((debuggerId, prompt, echo))
+        else:
+            self.setFocus()
+            self.__inRawMode = True
+            self.__echoInput = echo
+            self.__rawModeDebuggerId = debuggerId
+            
+            # Get all text which is still waiting for output
+            QApplication.processEvents()
+            self.__flushQueuedText()
+            
+            self.__write(self.tr("<{0}> {1}").format(debuggerId, prompt))
+            line, col = self.__getEndPos()
+            self.setCursorPosition(line, col)
+            buf = self.text(line)
+            if buf.startswith(sys.ps1):
+                buf = buf.replace(sys.ps1, "")
+            if buf.startswith(sys.ps2):
+                buf = buf.replace(sys.ps2, "")
+            self.prompt = buf
+            # move cursor to end of line
+            self.moveCursorToEOL()
         
     def paste(self):
         """
@@ -1320,12 +1329,15 @@
                 line, col = self.__getEndPos()
                 self.setCursorPosition(line, col)
                 self.prline, self.prcol = self.getCursorPosition()
-            if self.echoInput:
+            if self.__echoInput:
                 ac = self.isListActive()
                 super(Shell, self).keyPressEvent(ev)
                 self.incrementalSearchActive = True
                 if ac and self.racEnabled:
-                    self.dbs.remoteCompletion(self.completionText + txt)
+                    self.dbs.remoteCompletion(
+                        self.__debugUI.getSelectedDebuggerId(),
+                        self.completionText + txt
+                    )
             else:
                 self.__insertTextNoEcho(txt)
         else:
@@ -1357,7 +1369,10 @@
             if self.inContinue and not buf[:index - len(sys.ps2)].strip():
                 self.SendScintilla(cmd)
             elif self.racEnabled:
-                self.dbs.remoteCompletion(buf)
+                self.dbs.remoteCompletion(
+                    self.__debugUI.getSelectedDebuggerId(),
+                    buf
+                )
         
     def __QScintillaLeftDeleteCommand(self, method):
         """
@@ -1385,7 +1400,10 @@
                 db = 1
             if db and ac and self.racEnabled and self.completionText:
                 delta = len(self.text(line)) - oldLength
-                self.dbs.remoteCompletion(self.completionText[:delta])
+                self.dbs.remoteCompletion(
+                    self.__debugUI.getSelectedDebuggerId(),
+                    self.completionText[:delta]
+                )
         
     def __QScintillaDeleteBack(self):
         """
@@ -1762,7 +1780,7 @@
         @param historyIndex history index to be set
         @type int
         """
-        if not self.inRawMode:
+        if not self.__inRawMode:
             self.inCommandExecution = True
             self.interruptCommandExecution = False
             if not cmd:
@@ -1852,22 +1870,27 @@
                 self.vm.quit()
                 return
             
-            self.dbs.remoteStatement(cmd)
+            self.dbs.remoteStatement(self.__debugUI.getSelectedDebuggerId(),
+                                     cmd)
             while self.inCommandExecution:
                 try:
                     QApplication.processEvents()
                 except KeyboardInterrupt:
                     pass
         else:
-            if not self.echoInput:
+            if not self.__echoInput:
                 cmd = self.buff
                 self.buff = ""
             elif cmd:
                 cmd = cmd[len(self.prompt):]
-            self.inRawMode = False
-            self.echoInput = True
+            self.__inRawMode = False
+            self.__echoInput = True
             
-            self.dbs.remoteRawInput(cmd)
+            self.dbs.remoteRawInput(self.__rawModeDebuggerId, cmd)
+            
+            if self.__rawModeQueue:
+                debuggerId, prompt, echo = self.__rawModeQueue.pop(0)
+                self.__raw_input(prompt, echo, debuggerId)
     
     def __showVenvName(self):
         """
@@ -2064,7 +2087,8 @@
                 self.dbs.clientProcessStdout.disconnect(self.__writeStdOut)
                 self.dbs.clientProcessStderr.disconnect(self.__writeStdErr)
             self.__showStdOutErr = showStdOutErr
-        
+    
+    @pyqtSlot(list, str)
     def __showCompletions(self, completions, text):
         """
         Private method to display the possible completions.
--- a/eric6/UI/UserInterface.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/UI/UserInterface.py	Sat Feb 22 17:03:43 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)
--- a/eric6/Utilities/BackgroundClient.py	Sat Feb 22 14:27:42 2020 +0100
+++ b/eric6/Utilities/BackgroundClient.py	Sat Feb 22 17:03:43 2020 +0100
@@ -11,7 +11,7 @@
 
 from __future__ import unicode_literals
 try:
-    bytes = unicode
+    bytes = unicode         # __IGNORE_EXCEPTION__
     import StringIO as io   # __IGNORE_EXCEPTION__
 except NameError:
     import io       # __IGNORE_WARNING__

eric ide

mercurial