Implemented an eric-ide Server Shell. server

Fri, 09 Feb 2024 19:54:15 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 09 Feb 2024 19:54:15 +0100
branch
server
changeset 10555
08e853c0c77b
parent 10551
d80184d38152
child 10559
64db35c6e335

Implemented an eric-ide Server Shell.

eric7.epj file | annotate | diff | comparison | revisions
src/eric7/Debugger/DebugServer.py file | annotate | diff | comparison | revisions
src/eric7/Debugger/DebuggerInterfacePython.py file | annotate | diff | comparison | revisions
src/eric7/Preferences/ConfigurationPages/EricServerPage.py file | annotate | diff | comparison | revisions
src/eric7/Preferences/ConfigurationPages/EricServerPage.ui file | annotate | diff | comparison | revisions
src/eric7/Preferences/__init__.py file | annotate | diff | comparison | revisions
src/eric7/QScintilla/Shell.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServer/EricServer.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServer/EricServerFileSystemRequestHandler.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/EricServerDebuggerInterface.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/EricServerInterface.py file | annotate | diff | comparison | revisions
src/eric7/UI/UserInterface.py file | annotate | diff | comparison | revisions
src/eric7/VirtualEnv/VirtualenvManager.py file | annotate | diff | comparison | revisions
--- a/eric7.epj	Wed Feb 07 15:28:08 2024 +0100
+++ b/eric7.epj	Fri Feb 09 19:54:15 2024 +0100
@@ -2126,9 +2126,11 @@
       "src/eric7/QtHelpInterface/__init__.py",
       "src/eric7/RemoteServer/EricRequestCategory.py",
       "src/eric7/RemoteServer/EricServer.py",
+      "src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py",
       "src/eric7/RemoteServer/EricServerFileSystemRequestHandler.py",
       "src/eric7/RemoteServer/__init__.py",
       "src/eric7/RemoteServerInterface/EricServerConnectionDialog.py",
+      "src/eric7/RemoteServerInterface/EricServerDebuggerInterface.py",
       "src/eric7/RemoteServerInterface/EricServerFileDialog.py",
       "src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py",
       "src/eric7/RemoteServerInterface/EricServerInterface.py",
--- a/src/eric7/Debugger/DebugServer.py	Wed Feb 07 15:28:08 2024 +0100
+++ b/src/eric7/Debugger/DebugServer.py	Fri Feb 09 19:54:15 2024 +0100
@@ -492,28 +492,32 @@
         forProject=False,
         runInConsole=False,
         venvName="",
-        workingDir=None,
+        workingDir="",
         configOverride=None,
+        startRemote=False
     ):
         """
         Public method to start a debug client.
 
-        @param unplanned flag indicating that the client has died
-        @type bool
-        @param clType type of client to be started
-        @type str
-        @param forProject flag indicating a project related action
-        @type bool
+        @param unplanned flag indicating that the client has died (defaults to True)
+        @type bool (optional)
+        @param clType type of client to be started (defaults to None)
+        @type str (optional)
+        @param forProject flag indicating a project related action (defaults to False)
+        @type bool (optional)
         @param runInConsole flag indicating to start the debugger in a
-            console window
-        @type bool
-        @param venvName name of the virtual environment to be used
-        @type str
-        @param workingDir directory to start the debugger client in
-        @type str
-        @param configOverride dictionary containing the global config override
-            data
-        @type dict
+            console window (defaults to False)
+        @type bool (optional)
+        @param venvName name of the virtual environment to be used (defaults to "")
+        @type str (optional)
+        @param workingDir directory to start the debugger client in (defaults to "")
+        @type str (optional)
+        @param configOverride dictionary containing the global config override data
+            (defaults to None)
+        @type dict (optional)
+        @param startRemote flag indicating to start the client via an eric-ide server
+            (defaults to False)
+        @type bool (optional)
         """
         self.running = False
 
@@ -560,6 +564,7 @@
                         self.__originalPathString,
                         workingDir=workingDir,
                         configOverride=configOverride,
+                        startRemote=startRemote,
                     )
                 else:
                     (
@@ -573,6 +578,7 @@
                         self.__originalPathString,
                         workingDir=workingDir,
                         configOverride=configOverride,
+                        # TODO: add 'startRemote' parameter
                     )
             else:
                 (
@@ -586,6 +592,7 @@
                     self.__originalPathString,
                     workingDir=workingDir,
                     configOverride=configOverride,
+                    startRemote=startRemote,
                 )
 
             if self.clientProcess:
@@ -603,6 +610,8 @@
                 elif self.__autoClearShell:
                     self.__autoClearShell = False
                     self.remoteBanner()
+            elif startRemote:
+                self.remoteBanner()
             else:
                 if clType and self.lastClientType:
                     self.__setClientType(self.lastClientType)
@@ -2223,3 +2232,11 @@
         except KeyError:
             # The project object is not present
             return ""
+
+    def getEricServerEnvironmentString(self):
+        """
+        Public method to get the string for an eric-ide server environment.
+        """
+        # TODO: make this more elaborate once server environments definitions
+        #       are supported
+        return "eric-ide Server"
--- a/src/eric7/Debugger/DebuggerInterfacePython.py	Wed Feb 07 15:28:08 2024 +0100
+++ b/src/eric7/Debugger/DebuggerInterfacePython.py	Fri Feb 09 19:54:15 2024 +0100
@@ -50,6 +50,17 @@
         self.__autoContinued = []
         self.__isStepCommand = False
 
+        self.__ericServerDebugging = False  # are we debugging via the eric-ide server?
+        try:
+            self.__ericServerDebuggerInterface = ericApp().getObject(
+                "EricServer"
+            ).getServiceInterface("Debugger")
+            self.__ericServerDebuggerInterface.debugClientResponse.connect(
+                lambda jsonStr: self.handleJsonCommand(jsonStr, None)
+            )
+        except KeyError:
+            self.__ericServerDebuggerInterface = None
+
         self.debugServer = debugServer
         self.passive = passive
         self.process = None
@@ -88,8 +99,8 @@
         @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])
-        @type bool
+            (False = local to remote, True = remote to local) (defaults to True)
+        @type bool (optional)
         @return translated filename
         @rtype str
         """
@@ -102,8 +113,8 @@
         @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])
-        @type bool
+            (False = local to remote, True = remote to local) (defaults to True)
+        @type bool (optional)
         @return translated filename
         @rtype str
         """
@@ -118,7 +129,24 @@
 
         return path
 
-    def __startProcess(self, program, arguments, environment=None, workingDir=None):
+    def __ericServerTranslation(self, fn, remote2local=True):
+        """
+        Private method to perform the eric-ide server path translation.
+
+        @param fn filename to be translated
+        @type str
+        @param remote2local flag indicating the direction of translation
+            (False = local to remote, True = remote to local) (defaults to True)
+        @type bool (optional)
+        @return translated filename
+        @rtype str
+        """
+        if remote2local:
+            return FileSystemUtilities.remoteFileName(fn)
+        else:
+            return FileSystemUtilities.plainFileName(fn)
+
+    def __startProcess(self, program, arguments, environment=None, workingDir=""):
         """
         Private method to start the debugger client process.
 
@@ -154,8 +182,9 @@
         runInConsole,
         venvName,
         originalPathString,
-        workingDir=None,
+        workingDir="",
         configOverride=None,
+        startRemote=False,
     ):
         """
         Public method to start a remote Python interpreter.
@@ -169,41 +198,54 @@
         @type str
         @param originalPathString original PATH environment variable
         @type str
-        @param workingDir directory to start the debugger client in
-        @type str
-        @param configOverride dictionary containing the global config override
-            data
-        @type dict
+        @param workingDir directory to start the debugger client in (defaults to "")
+        @type str (optional)
+        @param configOverride dictionary containing the global config override data
+            (defaults to None)
+        @type dict (optional)
+        @param startRemote flag indicating to start the client via an eric-ide server
+            (defaults to False)
+        @type bool (optional)
         @return client process object, a flag to indicate a network connection
             and the name of the interpreter in case of a local execution
         @rtype tuple of (QProcess, bool, str)
         """
         global origPathEnv
 
-        if not venvName:
-            venvName = Preferences.getDebugger("Python3VirtualEnv")
-        if venvName == self.debugServer.getProjectEnvironmentString():
-            project = ericApp().getObject("Project")
-            venvName = project.getProjectVenv()
-            execPath = project.getProjectExecPath()
-            interpreter = project.getProjectInterpreter()
+        if startRemote or venvName == self.debugServer.getEricServerEnvironmentString():
+            # TODO change this once server environment definitions are supported
+            startRemote = True
+            venvName = self.debugServer.getEricServerEnvironmentString()
+            interpreter = ""
         else:
-            venvManager = ericApp().getObject("VirtualEnvManager")
-            interpreter = venvManager.getVirtualenvInterpreter(venvName)
-            execPath = venvManager.getVirtualenvExecPath(venvName)
-        if interpreter == "":
-            # use the interpreter used to run eric for identical variants
-            interpreter = PythonUtilities.getPythonExecutable()
-        if interpreter == "":
-            EricMessageBox.critical(
-                None,
-                self.tr("Start Debugger"),
-                self.tr("""<p>No suitable Python3 environment configured.</p>"""),
-            )
-            return None, False, ""
+            if not venvName:
+                venvName = Preferences.getDebugger("Python3VirtualEnv")
+            if venvName == self.debugServer.getProjectEnvironmentString():
+                project = ericApp().getObject("Project")
+                venvName = project.getProjectVenv()
+                execPath = project.getProjectExecPath()
+                interpreter = project.getProjectInterpreter()
+            else:
+                venvManager = ericApp().getObject("VirtualEnvManager")
+                interpreter = venvManager.getVirtualenvInterpreter(venvName)
+                execPath = venvManager.getVirtualenvExecPath(venvName)
+            if interpreter == "":
+                # use the interpreter used to run eric for identical variants
+                interpreter = PythonUtilities.getPythonExecutable()
+            if interpreter == "":
+                EricMessageBox.critical(
+                    None,
+                    self.tr("Start Debugger"),
+                    self.tr("""<p>No suitable Python3 environment configured.</p>"""),
+                )
+                return None, False, ""
 
         self.__inShutdown = False
 
+        self.__ericServerDebugging = False
+        self.__ericServerDebuggerInterface.stopClient()
+        self.__mainDebugger = None
+
         redirect = (
             str(configOverride["redirect"])
             if configOverride and configOverride["enable"]
@@ -290,6 +332,28 @@
                 )
                 return None, False, ""
 
+        elif startRemote and self.__ericServerDebuggerInterface is not None:
+            # debugging via an eric-ide server
+            ##self.__ericServerDebuggerInterface.stopClient()
+            ##self.__mainDebugger = None
+##
+            self.translate = self.__ericServerTranslation
+            self.__ericServerDebugging = True
+
+            args = []
+            if noencoding:
+                args.append(noencoding)
+            if multiprocessEnabled:
+                args.append(multiprocessEnabled)
+            if callTraceOptimization:
+                args.append(callTraceOptimization)
+            self.__ericServerDebuggerInterface.startClient(
+                interpreter, originalPathString, args, workingDir=workingDir,
+            )
+            self.__startedVenv = venvName
+
+            return None, self.__isNetworked, ""
+
         else:
             # local debugging code below
             debugClient = self.__determineDebugClient()
@@ -398,6 +462,7 @@
         originalPathString,
         workingDir=None,
         configOverride=None,
+        startRemote=False,
     ):
         """
         Public method to start a remote Python interpreter for a project.
@@ -416,6 +481,9 @@
         @param configOverride dictionary containing the global config override
             data
         @type dict
+        @param startRemote flag indicating to start the client via an eric-ide server
+            (defaults to False)
+        @type bool (optional)
         @return client process object, a flag to indicate a network connection
             and the name of the interpreter in case of a local execution
         @rtype tuple of (QProcess, bool, str)
@@ -460,6 +528,10 @@
 
         self.__inShutdown = False
 
+        self.__ericServerDebugging = False
+        self.__ericServerDebuggerInterface.stopClient()
+        self.__mainDebugger = None
+
         if project.getDebugProperty("REMOTEDEBUGGER"):
             # remote debugging code
             ipaddr = self.debugServer.getHostAddress(False)
@@ -517,6 +589,8 @@
                 # remote shell command is missing
                 return None, self.__isNetworked, ""
 
+        # TODO: add server debugging for projects
+
         else:
             # local debugging code below
             debugClient = project.getDebugProperty("DEBUGCLIENT")
@@ -619,7 +693,7 @@
         """
         self.__pendingConnections.append(sock)
 
-        sock.readyRead.connect(lambda: self.__parseClientLine(sock))
+        sock.readyRead.connect(lambda: self.__receiveJson(sock))
         sock.disconnected.connect(lambda: self.__socketDisconnected(sock))
 
         return True
@@ -634,30 +708,30 @@
         @param debuggerId id of the connected debug client
         @type str
         """
-        if sock in self.__pendingConnections:
+        if sock and sock in self.__pendingConnections:
             self.__connections[debuggerId] = sock
             self.__pendingConnections.remove(sock)
 
-            if self.__mainDebugger is None:
-                self.__mainDebugger = debuggerId
-                # Get the remote clients capabilities
-                self.remoteCapabilities(debuggerId)
+        if self.__mainDebugger is None:
+            self.__mainDebugger = debuggerId
+            # Get the remote clients capabilities
+            self.remoteCapabilities(debuggerId)
 
-            self.debugServer.signalClientDebuggerId(debuggerId)
+        self.debugServer.signalClientDebuggerId(debuggerId)
 
-            if debuggerId == self.__mainDebugger:
-                self.__flush()
-                self.debugServer.mainClientConnected()
+        if debuggerId == self.__mainDebugger:
+            self.__flush()
+            self.debugServer.mainClientConnected()
 
-            self.debugServer.initializeClient(debuggerId)
+        self.debugServer.initializeClient(debuggerId)
 
-            # perform auto-continue except for main
-            if (
-                debuggerId != self.__mainDebugger
-                and self.__autoContinue
-                and not self.__isStepCommand
-            ):
-                QTimer.singleShot(0, lambda: self.remoteContinue(debuggerId))
+        # perform auto-continue except for main
+        if (
+            debuggerId != self.__mainDebugger
+            and self.__autoContinue
+            and not self.__isStepCommand
+        ):
+            QTimer.singleShot(0, lambda: self.remoteContinue(debuggerId))
 
     def __socketDisconnected(self, sock):
         """
@@ -707,9 +781,15 @@
         """
         if self.__mainDebugger:
             # Send commands that were waiting for the connection.
-            conn = self.__connections[self.__mainDebugger]
-            for jsonStr in self.__commandQueue:
-                self.__writeJsonCommandToSocket(jsonStr, conn)
+            if self.__ericServerDebugging:
+                for jsonStr in self.__commandQueue:
+                    self.__ericServerDebuggerInterface.sendClientCommand(
+                        self.__mainDebugger, jsonStr
+                    )
+            else:
+                conn = self.__connections[self.__mainDebugger]
+                for jsonStr in self.__commandQueue:
+                    self.__writeJsonCommandToSocket(jsonStr, conn)
 
         self.__commandQueue.clear()
 
@@ -1396,7 +1476,7 @@
             debuggerId,
         )
 
-    def __parseClientLine(self, sock):
+    def __receiveJson(self, sock):
         """
         Private method to handle data from the client.
 
@@ -1423,11 +1503,11 @@
             logging.debug("<Debug-Server> %s", jsonStr)
             ##print("Server: ", jsonStr)    ## debug       # __IGNORE_WARNING_M891__
 
-            self.__handleJsonCommand(jsonStr, sock)
+            self.handleJsonCommand(jsonStr, sock)
 
-    def __handleJsonCommand(self, jsonStr, sock):
+    def handleJsonCommand(self, jsonStr, sock):
         """
-        Private method to handle a command or response serialized as a
+        Public method to handle a command or response serialized as a
         JSON string.
 
         @param jsonStr string containing the command or response received
@@ -1663,14 +1743,25 @@
         }
         jsonStr = json.dumps(commandDict)
 
-        if debuggerId and debuggerId in self.__connections:
-            sock = self.__connections[debuggerId]
-        elif sock is None and self.__mainDebugger is not None:
-            sock = self.__connections[self.__mainDebugger]
-        if sock is not None:
-            self.__writeJsonCommandToSocket(jsonStr, sock)
+        if self.__ericServerDebugging:
+            # Debugging via the eric-ide server -> pass the command on to it
+            if self.__mainDebugger is None:
+                # debugger has not connected yet -> queue the command
+                self.__commandQueue.append(jsonStr)
+            else:
+                self.__ericServerDebuggerInterface.sendClientCommand(
+                    debuggerId, jsonStr
+                )
         else:
-            self.__commandQueue.append(jsonStr)
+            # Local debugging -> send the command to the client
+            if debuggerId and debuggerId in self.__connections:
+                sock = self.__connections[debuggerId]
+            elif sock is None and self.__mainDebugger is not None:
+                sock = self.__connections[self.__mainDebugger]
+            if sock is not None:
+                self.__writeJsonCommandToSocket(jsonStr, sock)
+            else:
+                self.__commandQueue.append(jsonStr)
 
     def __writeJsonCommandToSocket(self, jsonCommand, sock):
         """
--- a/src/eric7/Preferences/ConfigurationPages/EricServerPage.py	Wed Feb 07 15:28:08 2024 +0100
+++ b/src/eric7/Preferences/ConfigurationPages/EricServerPage.py	Fri Feb 09 19:54:15 2024 +0100
@@ -28,12 +28,14 @@
 
         # set initial values
         self.timeoutSpinBox.setValue(Preferences.getEricServer("ConnectionTimeout"))
+        self.startShellCheckBox.setChecked(Preferences.getEricServer("AutostartShell"))
 
     def save(self):
         """
         Public slot to save the Cooperation configuration.
         """
         Preferences.setEricServer("ConnectionTimeout", self.timeoutSpinBox.value())
+        Preferences.setEricServer("AutostartShell", self.startShellCheckBox.isChecked())
 
 
 def create(dlg):  # noqa: U100
--- a/src/eric7/Preferences/ConfigurationPages/EricServerPage.ui	Wed Feb 07 15:28:08 2024 +0100
+++ b/src/eric7/Preferences/ConfigurationPages/EricServerPage.ui	Fri Feb 09 19:54:15 2024 +0100
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>496</width>
-    <height>300</height>
+    <width>510</width>
+    <height>452</height>
    </rect>
   </property>
   <layout class="QVBoxLayout" name="verticalLayout_2">
@@ -32,6 +32,16 @@
     </widget>
    </item>
    <item>
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>&lt;b&gt;Note:&lt;/b&gt; The eric-ide server is configured via command line parameters. The parameters of this page configure the interface to the eric-ide server.</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
     <widget class="QGroupBox" name="groupBox">
      <property name="title">
       <string>Server Connection</string>
@@ -79,16 +89,16 @@
    <item>
     <widget class="QGroupBox" name="groupBox_2">
      <property name="title">
-      <string>Notes</string>
+      <string>Shell</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout">
       <item>
-       <widget class="QLabel" name="label_2">
+       <widget class="QCheckBox" name="startShellCheckBox">
+        <property name="toolTip">
+         <string>Select this to start an eric-ide Server Shell when a connection to an eric-ide Server is established.</string>
+        </property>
         <property name="text">
-         <string>&lt;ul&gt;&lt;li&gt;The eric-ide server is configured via command line parameters. The parameters of this page configure the interface to the eric-ide server.&lt;/li&gt;&lt;/ul&gt;</string>
-        </property>
-        <property name="wordWrap">
-         <bool>true</bool>
+         <string>Start server Shell when server is conncted</string>
         </property>
        </widget>
       </item>
@@ -110,6 +120,10 @@
    </item>
   </layout>
  </widget>
+ <tabstops>
+  <tabstop>timeoutSpinBox</tabstop>
+  <tabstop>startShellCheckBox</tabstop>
+ </tabstops>
  <resources/>
  <connections/>
 </ui>
--- a/src/eric7/Preferences/__init__.py	Wed Feb 07 15:28:08 2024 +0100
+++ b/src/eric7/Preferences/__init__.py	Fri Feb 09 19:54:15 2024 +0100
@@ -1726,6 +1726,7 @@
     ericServerDefaults = {
         "ConnectionProfiles": "{}",  # JSON encoded dictionary
         "ConnectionTimeout": 10,  # timeout in seconds
+        "AutostartShell": True,
     }
 
 
@@ -4206,6 +4207,10 @@
         return int(
             Prefs.settings.value(f"{prefix}{key}", Prefs.ericServerDefaults[key])
         )
+    elif key in ("AutostartShell",):
+        return toBool(
+            Prefs.settings.value(f"{prefix}{key}", Prefs.ericServerDefaults[key])
+        )
     elif key in ("ConnectionProfiles",):
         jsonStr = Prefs.settings.value(f"{prefix}{key}", Prefs.ericServerDefaults[key])
         if jsonStr:
--- a/src/eric7/QScintilla/Shell.py	Wed Feb 07 15:28:08 2024 +0100
+++ b/src/eric7/QScintilla/Shell.py	Fri Feb 09 19:54:15 2024 +0100
@@ -12,6 +12,7 @@
 import pathlib
 import re
 import sys
+import time
 
 from PyQt6.Qsci import QsciScintilla
 from PyQt6.QtCore import QEvent, QPoint, Qt, pyqtSignal, pyqtSlot
@@ -388,10 +389,16 @@
         self.lmenu.clear()
         venvManager = ericApp().getObject("VirtualEnvManager")
         for venvName in sorted(venvManager.getVirtualenvNames()):
-            self.lmenu.addAction(venvName)
+            act = self.lmenu.addAction(venvName)
+            act.setData(venvName)
         if self.__project.isOpen():
             self.lmenu.addSeparator()
-            self.lmenu.addAction(self.tr("Project"))
+            act = self.lmenu.addAction(self.tr("Project"))
+            act.setData("<<project>>")
+        if ericApp().getObject("EricServer").isServerConnected():
+            self.lmenu.addSeparator()
+            act = self.lmenu.addAction(self.tr("eric-ide Server"))
+            act.setData("<<eric-server>>")
 
     def __resizeLinenoMargin(self):
         """
@@ -1925,11 +1932,7 @@
                     # Display the banner.
                     self.__getBanner()
                 elif cmd in ["%reset", "%restart"]:
-                    self.dbs.startClient(
-                        False,
-                        venvName=self.__currentVenv,
-                        workingDir=self.__currentWorkingDirectory,
-                    )
+                    self.doRestart()
                 elif cmd in ["%envs", "%environments"]:
                     venvs = (
                         ericApp().getObject("VirtualEnvManager").getVirtualenvNames()
@@ -1981,9 +1984,12 @@
                 self.setFocus(Qt.FocusReason.OtherFocusReason)
             else:
                 self.dbs.remoteStatement(self.__getSelectedDebuggerId(), cmd)
-                while self.inCommandExecution:
+                now = time.monotonic()
+                while self.inCommandExecution and time.monotonic() - now < 10:
+                    # 10 seconds timeout
                     with contextlib.suppress(KeyboardInterrupt):
                         QApplication.processEvents()
+                self.inCommandExecution = False
         else:
             if not self.__echoInput:
                 cmd = self.buff
@@ -2138,13 +2144,15 @@
         @param action context menu action that was triggered
         @type QAction
         """
-        venvName = action.text()
-        if venvName == self.tr("Project"):
+        venvName = action.data()
+        if venvName == "<<project>>":
             if self.__project.isOpen():
                 self.__currentWorkingDirectory = self.__project.getProjectPath()
             self.dbs.startClient(
                 False, forProject=True, workingDir=self.__currentWorkingDirectory
             )
+        elif venvName == "<<eric-server>>":
+            self.dbs.startClient(False, startRemote=True)
         else:
             self.dbs.startClient(False, venvName=venvName)
         self.__getBanner()
@@ -2616,6 +2624,26 @@
             self.dbs.startClient(False)
             self.__getBanner()
 
+    #################################################################
+    ## eric-ide Server Support
+    #################################################################
+
+    @pyqtSlot(bool)
+    def remoteConnectionChanged(self, connected):
+        """
+        Public slot handling a change of the connection state to an eric-ide server.
+
+        @param connected flag indicating the connection state
+        @type bool
+        """
+        if connected:
+            if Preferences.getEricServer("AutostartShell"):
+                self.dbs.startClient(False, startRemote=True)
+        else:
+            if self.__currentVenv == self.dbs.getEricServerEnvironmentString():
+                # start default backend
+                self.dbs.startClient(False)
+                self.__getBanner()
 
 #
 # eflag: noqa = M601
--- a/src/eric7/RemoteServer/EricServer.py	Wed Feb 07 15:28:08 2024 +0100
+++ b/src/eric7/RemoteServer/EricServer.py	Fri Feb 09 19:54:15 2024 +0100
@@ -9,17 +9,20 @@
 
 import io
 import json
-import select
+import selectors
 import socket
 import struct
 import sys
 import traceback
+import types
 import zlib
 
 from eric7.UI.Info import Version
 # TODO: remove dependency on 'eric7.UI.Info'
 
 from .EricRequestCategory import EricRequestCategory
+from .EricServerDebuggerRequestHandler import EricServerDebuggerRequestHandler
+from .EricServerFileSystemRequestHandler import EricServerFileSystemRequestHandler
 
 
 class EricServer:
@@ -45,15 +48,38 @@
 
         self.__connection = None
 
+        self.__selector = selectors.DefaultSelector()
+
+        # create and register the 'Debugger' request handler
+        self.__debuggerRequestHandler = EricServerDebuggerRequestHandler(self)
+        self.registerRequestHandler(
+            EricRequestCategory.Debugger,
+            self.__debuggerRequestHandler.handleRequest,
+        )
+
+        # create and register the 'File System' request handler
+        self.__fileSystemRequestHandler = EricServerFileSystemRequestHandler(self)
+        self.registerRequestHandler(
+            EricRequestCategory.FileSystem,
+            self.__fileSystemRequestHandler.handleRequest,
+        )
+
+        # TODO: 'Project' handler not implemented yet
+        # TODO: implement an 'EditorConfig' handler (?)
+
         address = ("", port)
         if socket.has_dualstack_ipv6() and useIPv6:
             self.__socket = socket.create_server(
                 address, family=socket.AF_INET6, dualstack_ipv6=True
             )
         else:
-            self.__socket = socket.create_server(
-                address, family=socket.AF_INET
-            )
+            self.__socket = socket.create_server(address, family=socket.AF_INET)
+
+    def getSelector(self):
+        """
+        Public method to get a reference to the selector object.
+        """
+        return self.__selector
 
     #######################################################################
     ## Methods for receiving requests and sending the results.
@@ -73,35 +99,109 @@
             (defaults to "", i.e. no UUID received)
         @type str
         """
-        commandDict = {
-            "jsonrpc": "2.0",
-            "category": category,
-            "reply": reply,
-            "params": params,
-            "uuid": reqestUuid,
-        }
-        data = json.dumps(commandDict).encode("utf8", "backslashreplace")
+        if self.__connection is not None:
+            commandDict = {
+                "jsonrpc": "2.0",
+                "category": category,
+                "reply": reply,
+                "params": params,
+                "uuid": reqestUuid,
+            }
+            self.sendJsonCommand(commandDict, self.__connection)
+
+    def sendJsonCommand(self, jsonCommand, sock):
+        """
+        Public method to send a JSON encoded command/response via a given socket.
+
+        @param jsonCommand dictionary containing the command data or a JSON encoded
+            command string
+        @type dict or str
+        @param sock reference to the socket to send the data to
+        @type socket.socket
+        """
+        if isinstance(jsonCommand, dict):
+            jsonCommand = json.dumps(jsonCommand)
+        data = jsonCommand.encode("utf8", "backslashreplace")
         header = struct.pack(b"!II", len(data), zlib.adler32(data) & 0xFFFFFFFF)
-        self.__connection.sendall(header)
-        self.__connection.sendall(data)
+        sock.sendall(header)
+        sock.sendall(data)
 
-    def __receiveBytes(self, length):
+    def __receiveBytes(self, length, sock):
         """
         Private method to receive the given length of bytes.
 
         @param length bytes to receive
         @type int
+        @param sock reference to the socket to receive the data from
+        @type socket.socket
         @return received bytes or None if connection closed
         @rtype bytes
         """
         data = bytearray()
         while len(data) < length:
-            newData = self.__connection.recv(length - len(data))
-            if not newData:
-                return None
+            try:
+                newData = sock.recv(length - len(data))
+                if not newData:
+                    print(str(newData))
+                    return None
+
+                data += newData
+            except OSError as err:
+                if err.errno != 11:
+                    data = None  # in case some data was received already
+                    break
+        return data
+
+    def receiveJsonCommand(self, sock):
+        """
+        Public method to receive a JSON encoded command and data.
+
+        @param sock reference to the socket to receive the data from
+        @type socket.socket
+        @return dictionary containing the JSON command data or None to signal
+            an issue while receiving data
+        @rtype dict
+        """
+        if self.isSocketClosed(sock):
+            return None
+
+        header = self.__receiveBytes(struct.calcsize(b"!II"), sock)
+        if not header:
+            return {}
+
+        length, datahash = struct.unpack(b"!II", header)
 
-            data += newData
-        return data
+        length = int(length)
+        data = self.__receiveBytes(length, sock)
+        if data is None:
+            return None
+
+        if not data or zlib.adler32(data) & 0xFFFFFFFF != datahash:
+            self.sendJson(
+                category=EricRequestCategory.Error,
+                reply="EricServerChecksumException",
+                params={
+                    "ExceptionType": "ProtocolChecksumError",
+                    "ExceptionValue": "The checksum of the data does not match.",
+                    "ProtocolData": data.decode("utf8", "backslashreplace"),
+                },
+            )
+            return {}
+
+        jsonStr = data.decode("utf8", "backslashreplace")
+        try:
+            return json.loads(jsonStr.strip())
+        except (TypeError, ValueError) as err:
+            self.sendJson(
+                category=EricRequestCategory.Error,
+                reply="EricServerException",
+                params={
+                    "ExceptionType": "ProtocolError",
+                    "ExceptionValue": str(err),
+                    "ProtocolData": jsonStr.strip(),
+                },
+            )
+            return {}
 
     def __receiveJson(self):
         """
@@ -113,41 +213,9 @@
             request
         @rtype tuple of (int, str, dict, str)
         """
-        # step 1: receive the data
-        header = self.__receiveBytes(struct.calcsize(b"!II"))
-        if not header:
-            return EricRequestCategory.Error, None, None, None
-
-        length, datahash = struct.unpack(b"!II", header)
+        requestDict = self.receiveJsonCommand(self.__connection)
 
-        length = int(length)
-        data = self.__receiveBytes(length)
-        if not data or zlib.adler32(data) & 0xFFFFFFFF != datahash:
-            self.sendJson(
-                category=EricRequestCategory.Error,
-                reply="ClientChecksumException",
-                params={
-                    "ExceptionType": "ProtocolChecksumError",
-                    "ExceptionValue": "The checksum of the data does not match.",
-                    "ProtocolData": data.decode("utf8", "backslashreplace"),
-                },
-            )
-            return EricRequestCategory.Error, None, None, None
-
-        # step 2: decode and convert the data
-        jsonString = data.decode("utf8", "backslashreplace")
-        try:
-            requestDict = json.loads(jsonString.strip())
-        except (TypeError, ValueError) as err:
-            self.sendJson(
-                category=EricRequestCategory.Error,
-                reply="ClientException",
-                params={
-                    "ExceptionType": "ProtocolError",
-                    "ExceptionValue": str(err),
-                    "ProtocolData": jsonString.strip(),
-                },
-            )
+        if not requestDict:
             return EricRequestCategory.Error, None, None, None
 
         category = requestDict["category"]
@@ -157,6 +225,28 @@
 
         return category, request, params, reqestUuid
 
+    def isSocketClosed(self, sock):
+        """
+        Public method to check, if a given socket is closed.
+
+        @param sock reference to the socket to be checked
+        @type socket.socket
+        @return flag indicating a closed state
+        @rtype bool
+        """
+        try:
+            # this will try to read bytes without removing them from buffer (peek only)
+            data = sock.recv(16, socket.MSG_PEEK)
+            if len(data) == 0:
+                return True
+        except BlockingIOError:
+            return False  # socket is open and reading from it would block
+        except ConnectionError:
+            return True  # socket was closed for some other reason
+        except Exception:
+            return False
+        return False
+
     #######################################################################
     ## Methods for the server main loop.
     #######################################################################
@@ -165,9 +255,66 @@
         """
         Private method to shut down the server.
         """
+        self.__closeIdeConnection()
+
+        print("Stop listening for 'eric-ide' connections.")
         self.__socket.shutdown(socket.SHUT_RDWR)
         self.__socket.close()
 
+        self.__selector.close()
+
+    def __acceptIdeConnection(self, sock):
+        """
+        Private method to accept the connection on the listening IDE server socket.
+
+        @param sock reference to the listening socket
+        @type socket.socket
+        """
+        self.__connection, address = sock.accept()  # Should be ready to read
+        print(f"'eric-ide' connection from {address[0]},  port {address[1]}")
+        self.__connection.setblocking(False)
+        data = types.SimpleNamespace(
+            name="eric-ide", address=address, handler=self.__serviceIdeConnection
+        )
+        events = selectors.EVENT_READ
+        self.__selector.register(self.__connection, events, data=data)
+
+    def __closeIdeConnection(self):
+        """
+        Private method to close the connection to an eric-ide.
+        """
+        if self.__connection is not None:
+            print(
+                f"Closing 'eric-ide' connection to {self.__connection.getpeername()}."
+            )
+            self.__selector.unregister(self.__connection)
+            self.__connection.shutdown(socket.SHUT_RDWR)
+            self.__connection.close()
+            self.__connection = None
+
+            self.__debuggerRequestHandler.shutdownClients()
+
+    def __serviceIdeConnection(self, key):
+        """
+        Private method to service the eric-ide connection.
+        
+        @param key reference to the SelectorKey object associated with the connection
+            to be serviced
+        @type selectors.SelectorKey
+        """
+        if key.data.name == "eric-ide":
+            category, request, params, reqestUuid = self.__receiveJson()
+            if category == EricRequestCategory.Error or request is None:
+                self.__closeIdeConnection()
+                return
+
+            if category == EricRequestCategory.Server:
+                if request.lower() == "shutdown":
+                    self.__shouldStop = True
+                    return
+
+            self.__handleRequest(category, request, params, reqestUuid)
+
     def run(self):
         """
         Public method implementing the remote server main loop.
@@ -182,51 +329,33 @@
         @return flag indicating a clean shutdown
         @rtype bool
         """
-        shutdown = False
         cleanExit = True
+        self.__shouldStop = False
 
         # listen on the server socket for new connections
         self.__socket.listen(1)
+        self.__socket.setblocking(False)
+        print(f"Listening for 'eric-ide' connections on {self.__socket.getsockname()}")
+        data = types.SimpleNamespace(
+            name="server", acceptHandler=self.__acceptIdeConnection
+        )
+        self.__selector.register(self.__socket, selectors.EVENT_READ, data=data)
+
+        self.__debuggerRequestHandler.initServerSocket()
 
         while True:
             try:
-                # accept the next pending connection
-                print("Waiting for connection...")
-                self.__connection, address = self.__socket.accept()
-                print(f"Connection from {address[0]},  port {address[1]}")
-
-                selectErrors = 0
-                while selectErrors <= 10:  # selected arbitrarily
-                    try:
-                        rrdy, wrdy, xrdy = select.select([self.__connection], [], [])
-
-                        # Just waiting for self.__connection. Therefore no check
-                        # needed.
-                        category, request, params, reqestUuid = self.__receiveJson()
-                        if category == EricRequestCategory.Error or request is None:
-                            selectErrors += 1
-                        elif category == EricRequestCategory.Server:
-                            if request.lower() == "exit":
-                                break
-                            elif request.lower() == "shutdown":
-                                shutdown = True
-                                break
-                            else:
-                                self.__handleRequest(
-                                    category, request, params, reqestUuid
-                                )
-                        else:
-                            self.__handleRequest(category, request, params, reqestUuid)
-
-                            # reset select errors
-                            selectErrors = 0
-
-                    except (select.error, socket.error):
-                        selectErrors += 1
+                events = self.__selector.select(timeout=None)
+                for key, mask in events:
+                    if key.data.name == "server":
+                        # it is an event for a server socket
+                        key.data.acceptHandler(key.fileobj)
+                    else:
+                        key.data.handler(key)
 
             except KeyboardInterrupt:
                 # intercept user pressing Ctrl+C
-                shutdown = True
+                self.__shouldStop = True
 
             except Exception:
                 exctype, excval, exctb = sys.exc_info()
@@ -237,15 +366,10 @@
                 
                 print(f"{str(exctype)} / {str(excval)} / {tbinfo}")
 
-                shutdown = True
+                self.__shouldStop = True
                 cleanExit = False
 
-            if self.__connection is not None:
-                self.__connection.shutdown(socket.SHUT_RDWR)
-                self.__connection.close()
-                self.__connection = None
-
-            if shutdown:
+            if self.__shouldStop:
                 # exit the outer loop and shut down the server
                 self.__shutdown()
                 break
@@ -324,36 +448,8 @@
         """
         try:
             handler = self.__requestCategoryHandlerRegistry[category]
+            handler(request=request, params=params, reqestUuid=reqestUuid)
         except KeyError:
-            if category < EricRequestCategory.UserCategory:
-                # it is an internally supported category
-                if category == EricRequestCategory.FileSystem:
-                    from .EricServerFileSystemRequestHandler import (
-                        EricServerFileSystemRequestHandler,
-                    )
-                    handler = EricServerFileSystemRequestHandler(self).handleRequest
-
-                elif category == EricRequestCategory.Project:
-                    # TODO: 'Project' handler not implemented yet
-                    handler = None
-
-                elif category == EricRequestCategory.Debugger:
-                    # TODO: 'Debugger' handler not implemented yet
-                    handler = None
-
-                # TODO: implement an 'EditorConfig' handler (?)
-
-                else:
-                    # That internal category does not exist.
-                    handler = None
-
-                self.registerRequestHandler(category, handler)
-            else:
-                handler = None
-
-        if handler is not None:
-            handler(request=request, params=params, reqestUuid=reqestUuid)
-        else:
             self.sendJson(
                 category=EricRequestCategory.Error,
                 reply="UnsupportedServiceCategory",
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py	Fri Feb 09 19:54:15 2024 +0100
@@ -0,0 +1,353 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the debugger request handler of the eric-ide server.
+"""
+
+import json
+import os
+import selectors
+import socket
+import subprocess
+import sys
+import types
+
+from .EricRequestCategory import EricRequestCategory
+
+class EricServerDebuggerRequestHandler:
+    """
+    Class implementing the debugger request handler of the eric-ide server.
+    """
+
+    def __init__(self, server):
+        """
+        Constructor
+
+        @param server reference to the eric-ide server object
+        @type EricServer
+        """
+        self.__server = server
+        
+        self.__requestMethodMapping = {
+            "StartClient": self.__startClient,
+            "StopClient": self.__stopClient,
+            "DebugClientCommand": self.__relayDebugClientCommand
+        }
+
+        self.__mainClientId = None
+        self.__client = None
+        self.__pendingConnections = []
+        self.__connections = {}
+
+        address = ("127.0.0.1", 0)
+        self.__socket = socket.create_server(address, family=socket.AF_INET)
+
+    def initServerSocket(self):
+        """
+        Public method to initialize the server socket listening for debug client
+        connections.
+        """
+        # listen on the debug server socket
+        self.__socket.listen()
+        self.__socket.setblocking(False)
+        print(
+            f"Listening for 'debug client' connections on"
+            f" {self.__socket.getsockname()}"
+        )
+        data = types.SimpleNamespace(
+            name="server", acceptHandler=self.__acceptDbgClientConnection
+        )
+        self.__server.getSelector().register(
+            self.__socket, selectors.EVENT_READ, data=data
+        )
+
+    def handleRequest(self, request, params, reqestUuid):
+        """
+        Public method handling the received debugger requests.
+
+        @param request request name
+        @type str
+        @param params dictionary containing the request parameters
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+        @type str
+        """
+        try:
+            result = self.__requestMethodMapping[request](params)
+            if result:
+                self.__server.sendJson(
+                    category=EricRequestCategory.Debugger,
+                    reply=request,
+                    params=result,
+                    reqestUuid=reqestUuid,
+                )
+
+        except KeyError:
+            self.__server.sendJson(
+                category=EricRequestCategory.Debugger,
+                reply="DebuggerRequestError",
+                params={"Error": f"Request type '{request}' is not supported."},
+            )
+
+    #######################################################################
+    ## DebugServer like methods.
+    #######################################################################
+
+    def __acceptDbgClientConnection(self, sock):
+        """
+        Private method to accept the connection on the listening debug server socket.
+
+        @param sock reference to the listening socket
+        @type socket.socket
+        """
+        connection, address = sock.accept()  # Should be ready to read
+        print(f"'Debug client' connection from {address[0]},  port {address[1]}")
+        connection.setblocking(False)
+        self.__pendingConnections.append(connection)
+
+        data = types.SimpleNamespace(
+            name="debug_client",
+            address=address,
+            handler=self.__serviceDbgClientConnection,
+        )
+        events = selectors.EVENT_READ
+        self.__server.getSelector().register(connection, events, data=data)
+
+    def __serviceDbgClientConnection(self, key):
+        """
+        Private method to service the debug client connection.
+        
+        @param key reference to the SelectorKey object associated with the connection
+            to be serviced
+        @type selectors.SelectorKey
+        """
+        sock = key.fileobj
+        data = self.__server.receiveJsonCommand(sock)
+
+        if data is None:
+            # socket was closed by debug client
+            self.__clientSocketDisconnected(sock)
+        elif data:
+            method = data["method"]
+            if method == "DebuggerId" and sock in self.__pendingConnections:
+                debuggerId = data['params']['debuggerId']
+                self.__connections[debuggerId] = sock
+                self.__pendingConnections.remove(sock)
+                if self.__mainClientId is None:
+                    self.__mainClientId = debuggerId
+
+            elif method == "ResponseBanner":
+                # add an indicator for the eric-ide server
+                data["params"]["platform"] += " (eric-ide Server)"
+
+            # pass on the data to the eric-ide
+            jsonStr = json.dumps(data)
+            print("Client Response:", jsonStr)
+            self.__server.sendJson(
+                category=EricRequestCategory.Debugger,
+                reply="DebugClientResponse",
+                params={"response": jsonStr},
+            )
+
+    def __clientSocketDisconnected(self, sock):
+        """
+        Private slot handling a socket disconnecting.
+
+        @param sock reference to the disconnected socket
+        @type QTcpSocket
+        """
+        self.__server.getSelector().unregister(sock)
+
+        for debuggerId in list(self.__connections):
+            if self.__connections[debuggerId] is sock:
+                del self.__connections[debuggerId]
+                self.__server.sendJson(
+                    category=EricRequestCategory.Debugger,
+                    reply="DebugClientDisconnected",
+                    params={"debugger_id": debuggerId},
+                )
+
+                if debuggerId == self.__mainClientId:
+                    self.__mainClientId = None
+
+                break
+        else:
+            if sock in self.__pendingConnections:
+                self.__pendingConnections.remove(sock)
+
+        sock.shutdown(socket.SHUT_RDWR)
+        sock.close()
+
+        if not self.__connections:
+            # no active connections anymore
+            self.__server.sendJson(
+                category=EricRequestCategory.Debugger,
+                reply="LastDebugClientExited",
+                params={},
+            )
+
+    def __serviceDbgClientStdoutStderr(self, key):
+        """
+        Private method to service the debug client stdout and stderr channels.
+        
+        @param key reference to the SelectorKey object associated with the connection
+            to be serviced
+        @type selectors.SelectorKey
+        """
+        data = key.fileobj.read()
+        if key.data.name == "debug_client_stdout":
+            # TODO: stdout handling not implemented yet
+            print("stdout:", data)
+        elif key.data.name == "debug_client_stderr":
+            # TODO: stderr handling not implemented yet
+            print("stderr:", data)
+
+    def shutdownClients(self):
+        """
+        Public method to shut down all connected clients.
+        """
+        if not self.__client:
+            # no client started yet
+            return
+
+        while self.__pendingConnections:
+            sock = self.__pendingConnections.pop()
+            commandDict = self.__prepareClientCommand("RequestShutdown", {})
+            self.__server.sendJsonCommand(commandDict, sock)
+            self.__shutdownSocket("", sock)
+
+        while self.__connections:
+            debuggerId, sock = self.__connections.popitem()
+            commandDict = self.__prepareClientCommand("RequestShutdown", {})
+            self.__server.sendJsonCommand(commandDict, sock)
+            self.__shutdownSocket(debuggerId, sock)
+
+        # reinitialize
+        self.__mainClientId = None
+        self.__client = None
+
+        # no active connections anymore
+        self.__server.sendJson(
+            category=EricRequestCategory.Debugger,
+            reply="LastDebugClientExited",
+            params={},
+        )
+
+    def __shutdownSocket(self, debuggerId, sock):
+        """
+        Private slot to shut down a socket.
+
+        @param debuggerId ID of the debugger the socket belongs to
+        @type str
+        @param sock reference to the socket
+        @type socket.socket
+        """
+        self.__server.getSelector().unregister(sock)
+        sock.shutdown(socket.SHUT_RDWR)
+        sock.close()
+
+        if debuggerId:
+            self.__server.sendJson(
+                category=EricRequestCategory.Debugger,
+                reply="DebugClientDisconnected",
+                params={"debugger_id": debuggerId},
+            )
+
+    def __prepareClientCommand(self, command, params):
+        """
+        Private method to prepare a command dictionary for the debug client.
+
+        @param command command to be sent
+        @type str
+        @param params dictionary containing the command parameters
+        @type dict
+        @return completed command dictionary to be sent to the debug client
+        @rtype dict
+        """
+        return {
+            "jsonrpc": "2.0",
+            "method": command,
+            "params": params,
+        }
+
+    #######################################################################
+    ## Individual request handler methods.
+    #######################################################################
+
+    def __startClient(self, params):
+        """
+        Private method to start a debug client process.
+
+        @param params dictionary containing the request data
+        @type dict
+        """
+        # 1. stop an already started debug client
+        if self.__client is not None:
+            self.__client.terminate()
+            self.__client = None
+
+        # 2. start a debug client
+        debugClient = os.path.abspath(
+            os.path.join(
+                os.path.dirname(__file__),
+                "..",
+                "DebugClients",
+                "Python",
+                "DebugClient.py",
+            )
+        )
+        ipaddr, port = self.__socket.getsockname()
+        args = [sys.executable, debugClient]
+        args.extend(params["arguments"])
+        args.extend([str(port), "True", ipaddr])
+
+        self.__client = subprocess.Popen(
+            args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+        )
+        # TODO: register stdin & stderr with selector
+
+    def __stopClient(self, params):
+        """
+        Private method to stop the current debug client process.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        self.shutdownClients()
+
+        return {"ok": True}
+
+    def __relayDebugClientCommand(self, params):
+        """
+        Private method to relay a debug client command to the client.
+
+        @param params dictionary containing the request data
+        @type dict
+        """
+        debuggerId = params["debugger_id"]
+        jsonStr = params["command"]
+        print(debuggerId, "->", jsonStr)
+
+        if not debuggerId and self.__mainClientId:
+            debuggerId = self.__mainClientId
+
+        try:
+            sock = self.__connections[debuggerId]
+        except KeyError:
+            print(f"Command for unknown debugger ID '{debuggerId}' received.")
+            # tell the eric-ide again, that this debugger ID is gone
+            self.__server.sendJson(
+                category=EricRequestCategory.Debugger,
+                reply="DebugClientDisconnected",
+                params={"debugger_id": debuggerId},
+            )
+            sock = (
+                self.__connections[self.__mainClientId] if self.__mainClientId else None
+            )
+        if sock:
+            self.__server.sendJsonCommand(jsonStr, sock)
--- a/src/eric7/RemoteServer/EricServerFileSystemRequestHandler.py	Wed Feb 07 15:28:08 2024 +0100
+++ b/src/eric7/RemoteServer/EricServerFileSystemRequestHandler.py	Fri Feb 09 19:54:15 2024 +0100
@@ -28,7 +28,7 @@
         @type EricServer
         """
         self.__server = server
-        
+
         self.__requestMethodMapping = {
             "Chdir": self.__chdir,
             "Getcwd": self.__getcwd,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerDebuggerInterface.py	Fri Feb 09 19:54:15 2024 +0100
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the file system interface to the eric-ide server.
+"""
+
+from PyQt6.QtCore import QEventLoop, QObject, pyqtSignal, pyqtSlot
+
+from eric7.EricWidgets import EricMessageBox
+from eric7.EricWidgets.EricApplication import ericApp
+from eric7.RemoteServer.EricRequestCategory import EricRequestCategory
+from eric7.SystemUtilities import FileSystemUtilities
+
+
+# TODO: sanitize all file names with FileSystemUtilities.plainFileName()
+class EricServerDebuggerInterface(QObject):
+    """
+    Class implementing the file system interface to the eric-ide server.
+    """
+
+    debugClientResponse = pyqtSignal(str)
+
+    def __init__(self, serverInterface):
+        """
+        Constructor
+
+        @param serverInterface reference to the eric-ide server interface
+        @type EricServerInterface
+        """
+        super().__init__(parent=serverInterface)
+
+        self.__serverInterface = serverInterface
+
+        self.__replyMethodMapping = {
+            "DebuggerRequestError": self.__handleDbgRequestError,
+            "DebugClientResponse": self.__handleDbgClientResponse,
+            "DebugClientDisconnected": self.__handleDbgClientDisconnected,
+            "LastDebugClientExited": self.__handleLastDbgClientExited,
+        }
+
+        # connect some signals
+        self.__serverInterface.remoteDebuggerReply.connect(self.__handleDebuggerReply)
+
+    def sendClientCommand(self, debuggerId, jsonCommand):
+        """
+        Public method to rely a debug client command via the eric-ide server.
+
+        @param debuggerId id of the debug client to send the command to
+        @type str
+        @param jsonCommand JSON encoded command dictionary to be relayed
+        @type str
+        """
+        self.__serverInterface.sendJson(
+            category=EricRequestCategory.Debugger,
+            request="DebugClientCommand",
+            params={"debugger_id": debuggerId, "command": jsonCommand},
+        )
+
+    @pyqtSlot(str, dict)
+    def __handleDebuggerReply(self, reply, params):
+        """
+        Private slot to handle a debugger reply from the eric-ide server.
+
+        @param reply name of the server reply
+        @type str
+        @param params dictionary containing the reply data
+        @type dict
+        """
+        try:
+            self.__replyMethodMapping[reply](params)
+        except KeyError:
+            EricMessageBox.critical(
+                None,
+                self.tr("Unknown Server Reply"),
+                self.tr(
+                    "<p>The eric-ide server debugger interface sent the unknown reply"
+                    " <b>{0}</b>.</p>"
+                ).format(reply),
+            )
+
+    #######################################################################
+    ## Methods for handling of debug client replies.
+    #######################################################################
+
+    def __handleDbgRequestError(self, params):
+        """
+        Private method to handle an error reported by the debugger interface of
+        the eric-ide server.
+
+        @param params dictionary containing the reply data
+        @type dict
+        """
+        EricMessageBox.warning(
+            None,
+            self.tr("Debug Client Command"),
+            self.tr(
+                "<p>The IDE received an error message.</p><p>Error: {0}</p>"
+            ).format(params["Error"]),
+        )
+
+    def __handleDbgClientResponse(self, params):
+        """
+        Private method to handle a response from a debug client connected to the
+        eric-ide server.
+
+        @param params dictionary containing the reply data
+        @type dict
+        """
+        self.debugClientResponse.emit(params["response"])
+
+    def __handleDbgClientDisconnected(self, params):
+        """
+        Private method to handle a debug client disconnect report of the
+        eric-ide server.
+
+        @param params dictionary containing the reply data
+        @type dict
+        """
+        ericApp().getObject("DebugServer").signalClientDisconnected(
+            params["debugger_id"]
+        )
+
+    def __handleLastDbgClientExited(self, params):
+        """
+        Private method to handle a report of the eric-ide server, that the last
+        debug client has disconnected.
+
+        @param params dictionary containing the reply data
+        @type dict
+        """
+        ericApp().getObject("DebugServer").signalLastClientExited()
+
+    #######################################################################
+    ## Methods for sending debug server commands to the eric-ide server.
+    #######################################################################
+
+    def startClient(self, interpreter, originalPathString, args, workingDir=""):
+        """
+        Public method to send a command to start a debug client.
+
+        @param interpreter path of the remote interpreter to be used
+        @type str
+        @param originalPathString original PATH environment variable
+        @type str
+        @param args list of command line parameters for the debug client
+        @type list of str
+        @param workingDir directory to start the debugger client in (defaults to "")
+        @type str (optional)
+        """
+        self.__serverInterface.sendJson(
+            category=EricRequestCategory.Debugger,
+            request="StartClient",
+            params={
+                "interpreter": FileSystemUtilities.plainFileName(interpreter),
+                "path": originalPathString,
+                "arguments": args,
+                "working_dir": FileSystemUtilities.plainFileName(workingDir),
+            },
+        )
+
+    def stopClient(self):
+        """
+        Public method to stop the debug client synchronously.
+        """
+        if self.__serverInterface.isServerConnected():
+            loop = QEventLoop()
+
+            def callback(reply, params):
+                """
+                Function to handle the server reply
+
+                @param reply name of the server reply
+                @type str
+                @param params dictionary containing the reply data
+                @type dict
+                """
+                if reply == "StopClient":
+                    loop.quit()
+
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.Debugger,
+                request="StopClient",
+                params={},
+                callback=callback,
+            )
+
+            loop.exec()
--- a/src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py	Wed Feb 07 15:28:08 2024 +0100
+++ b/src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py	Fri Feb 09 19:54:15 2024 +0100
@@ -15,7 +15,7 @@
 from eric7.RemoteServer.EricRequestCategory import EricRequestCategory
 
 
-# TODO: sanitize all file name with FileSystemUtilities.plainFileName()
+# TODO: sanitize all file names with FileSystemUtilities.plainFileName()
 class EricServerFileSystemInterface(QObject):
     """
     Class implementing the file system interface to the eric-ide server.
--- a/src/eric7/RemoteServerInterface/EricServerInterface.py	Wed Feb 07 15:28:08 2024 +0100
+++ b/src/eric7/RemoteServerInterface/EricServerInterface.py	Fri Feb 09 19:54:15 2024 +0100
@@ -107,8 +107,8 @@
                 from .EricServerFileSystemInterface import EricServerFileSystemInterface
                 self.__serviceInterfaces[lname] = EricServerFileSystemInterface(self)
             elif lname == "debugger":
-                # TODO: 'Debugger Interface' not implemented yet
-                pass
+                from .EricServerDebuggerInterface import EricServerDebuggerInterface
+                self.__serviceInterfaces[lname] = EricServerDebuggerInterface(self)
             elif lname == "project":
                 # TODO: 'Project Interface' not implemented yet
                 pass
--- a/src/eric7/UI/UserInterface.py	Wed Feb 07 15:28:08 2024 +0100
+++ b/src/eric7/UI/UserInterface.py	Fri Feb 09 19:54:15 2024 +0100
@@ -597,6 +597,9 @@
         self.__ericServerInterface.connectionStateChanged.connect(
             self.viewmanager.remoteConnectionChanged
         )
+        self.__ericServerInterface.connectionStateChanged.connect(
+            self.shell.remoteConnectionChanged
+        )
         self.__ericServerInterface.aboutToDisconnect.connect(
             self.viewmanager.closeRemoteEditors
         )
--- a/src/eric7/VirtualEnv/VirtualenvManager.py	Wed Feb 07 15:28:08 2024 +0100
+++ b/src/eric7/VirtualEnv/VirtualenvManager.py	Fri Feb 09 19:54:15 2024 +0100
@@ -26,6 +26,7 @@
 from .VirtualenvMeta import VirtualenvMetaData
 
 
+# TODO: introduce 'eric-ide Server' environment definitions
 class VirtualenvManager(QObject):
     """
     Class implementing an object to manage Python virtual environments.

eric ide

mercurial