Started implementing an eric-ide server for remote development (e.g. on a different host or in a Docker container). server

Mon, 29 Jan 2024 19:50:44 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 29 Jan 2024 19:50:44 +0100
branch
server
changeset 10531
3308e8349e4c
parent 10530
684f491a3bfc
child 10539
4274f189ff78

Started implementing an eric-ide server for remote development (e.g. on a different host or in a Docker container).

eric7.epj file | annotate | diff | comparison | revisions
src/eric7/Preferences/ConfigurationDialog.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/RemoteServer/EricRequestCategory.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServer/EricServer.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServer/__init__.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/EricServerConnectionDialog.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/EricServerConnectionDialog.ui file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/EricServerInterface.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/EricServerProfilesDialog.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/EricServerProfilesDialog.ui file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/__init__.py file | annotate | diff | comparison | revisions
src/eric7/UI/UserInterface.py file | annotate | diff | comparison | revisions
src/eric7/eric7_server.py file | annotate | diff | comparison | revisions
src/eric7/icons/breeze-dark/preferences-eric-server.svg file | annotate | diff | comparison | revisions
src/eric7/icons/breeze-light/preferences-eric-server.svg file | annotate | diff | comparison | revisions
src/eric7/icons/oxygen/preferences-eric-server.png file | annotate | diff | comparison | revisions
--- a/eric7.epj	Fri Jan 26 16:17:05 2024 +0100
+++ b/eric7.epj	Mon Jan 29 19:50:44 2024 +0100
@@ -630,6 +630,7 @@
       "src/eric7/Preferences/ConfigurationPages/EditorSyntaxPage.ui",
       "src/eric7/Preferences/ConfigurationPages/EditorTypingPage.ui",
       "src/eric7/Preferences/ConfigurationPages/EmailPage.ui",
+      "src/eric7/Preferences/ConfigurationPages/EricServerPage.ui",
       "src/eric7/Preferences/ConfigurationPages/GraphicsPage.ui",
       "src/eric7/Preferences/ConfigurationPages/HelpDocumentationPage.ui",
       "src/eric7/Preferences/ConfigurationPages/HelpViewersPage.ui",
@@ -709,6 +710,8 @@
       "src/eric7/QtHelpInterface/QtHelpDocumentationConfigurationDialog.ui",
       "src/eric7/QtHelpInterface/QtHelpDocumentationSelectionDialog.ui",
       "src/eric7/QtHelpInterface/QtHelpDocumentationSettingsWidget.ui",
+      "src/eric7/RemoteServerInterface/EricServerConnectionDialog.ui",
+      "src/eric7/RemoteServerInterface/EricServerProfilesDialog.ui",
       "src/eric7/Snapshot/SnapWidget.ui",
       "src/eric7/SqlBrowser/SqlBrowserWidget.ui",
       "src/eric7/SqlBrowser/SqlConnectionDialog.ui",
@@ -1928,6 +1931,7 @@
       "src/eric7/Preferences/ConfigurationPages/EditorSyntaxPage.py",
       "src/eric7/Preferences/ConfigurationPages/EditorTypingPage.py",
       "src/eric7/Preferences/ConfigurationPages/EmailPage.py",
+      "src/eric7/Preferences/ConfigurationPages/EricServerPage.py",
       "src/eric7/Preferences/ConfigurationPages/GraphicsPage.py",
       "src/eric7/Preferences/ConfigurationPages/HelpDocumentationPage.py",
       "src/eric7/Preferences/ConfigurationPages/HelpViewersPage.py",
@@ -2118,6 +2122,13 @@
       "src/eric7/QtHelpInterface/QtHelpDocumentationSettingsWidget.py",
       "src/eric7/QtHelpInterface/QtHelpSchemeHandler.py",
       "src/eric7/QtHelpInterface/__init__.py",
+      "src/eric7/RemoteServer/EricRequestCategory.py",
+      "src/eric7/RemoteServer/EricServer.py",
+      "src/eric7/RemoteServer/__init__.py",
+      "src/eric7/RemoteServerInterface/EricServerConnectionDialog.py",
+      "src/eric7/RemoteServerInterface/EricServerInterface.py",
+      "src/eric7/RemoteServerInterface/EricServerProfilesDialog.py",
+      "src/eric7/RemoteServerInterface/__init__.py",
       "src/eric7/Sessions/SessionFile.py",
       "src/eric7/Sessions/__init__.py",
       "src/eric7/Snapshot/SnapWidget.py",
@@ -2503,6 +2514,7 @@
       "src/eric7/eric7_qregularexpression.pyw",
       "src/eric7/eric7_re.py",
       "src/eric7/eric7_re.pyw",
+      "src/eric7/eric7_server.py",
       "src/eric7/eric7_shell.py",
       "src/eric7/eric7_shell.pyw",
       "src/eric7/eric7_snap.py",
--- a/src/eric7/Preferences/ConfigurationDialog.py	Fri Jan 26 16:17:05 2024 +0100
+++ b/src/eric7/Preferences/ConfigurationDialog.py	Mon Jan 29 19:50:44 2024 +0100
@@ -210,6 +210,13 @@
                     None,
                     None,
                 ],
+                "ericServerPage": [
+                    self.tr("eric-ide Server"),
+                    "preferences-eric-server",
+                    "EricServerPage",
+                    None,
+                    None,
+                ],
                 "graphicsPage": [
                     self.tr("Graphics"),
                     "preferences-graphics",
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/Preferences/ConfigurationPages/EricServerPage.py	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the eric-ide server related settings.
+"""
+
+from eric7 import Preferences
+
+from .ConfigurationPageBase import ConfigurationPageBase
+from .Ui_EricServerPage import Ui_EricServerPage
+
+
+class EricServerPage(ConfigurationPageBase, Ui_EricServerPage):
+    """
+    Class implementing the eric-ide server related settings.
+    """
+
+    def __init__(self):
+        """
+        Constructor
+        """
+        super().__init__()
+        self.setupUi(self)
+        self.setObjectName("EricServerPage")
+
+        # set initial values
+        self.timeoutSpinBox.setValue(Preferences.getEricServer("ConnectionTimeout"))
+
+    def save(self):
+        """
+        Public slot to save the Cooperation configuration.
+        """
+        Preferences.setEricServer("ConnectionTimeout", self.timeoutSpinBox.value())
+
+
+def create(dlg):  # noqa: U100
+    """
+    Module function to create the configuration page.
+
+    @param dlg reference to the configuration dialog
+    @type ConfigurationDialog
+    @return reference to the instantiated page
+    @rtype ConfigurationPageBase
+    """
+    page = EricServerPage()
+    return page
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/Preferences/ConfigurationPages/EricServerPage.ui	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EricServerPage</class>
+ <widget class="QWidget" name="EricServerPage">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>496</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <item>
+    <widget class="QLabel" name="headerLabel">
+     <property name="text">
+      <string>&lt;b&gt;Configure eric-ide Server Settings&lt;/b&gt;</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="Line" name="line11">
+     <property name="frameShape">
+      <enum>QFrame::HLine</enum>
+     </property>
+     <property name="frameShadow">
+      <enum>QFrame::Sunken</enum>
+     </property>
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>Server Connection</string>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout">
+      <item>
+       <widget class="QLabel" name="label">
+        <property name="text">
+         <string>Default Timeout:</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QSpinBox" name="timeoutSpinBox">
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+        <property name="suffix">
+         <string> s</string>
+        </property>
+        <property name="minimum">
+         <number>5</number>
+        </property>
+        <property name="maximum">
+         <number>60</number>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>294</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox_2">
+     <property name="title">
+      <string>Notes</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout">
+      <item>
+       <widget class="QLabel" name="label_2">
+        <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>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>87</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
--- a/src/eric7/Preferences/__init__.py	Fri Jan 26 16:17:05 2024 +0100
+++ b/src/eric7/Preferences/__init__.py	Mon Jan 29 19:50:44 2024 +0100
@@ -1708,7 +1708,7 @@
     else:
         jediDefaults["MouseClickGotoModifiers"] = Qt.KeyboardModifier.ControlModifier
 
-    # defaults for Hex Editor
+    # defaults for PDF viewer
     pdfViewerDefaults = {
         "PdfViewerState": QByteArray(),
         "PdfViewerSplitterState": QByteArray(),
@@ -1722,6 +1722,12 @@
         "PdfSearchHighlightAll": True,
     }
 
+    # defaults for the eric-ide server interface
+    ericServerDefaults = {
+        "ConnectionProfiles": "{}",  # JSON encoded dictionary
+        "ConnectionTimeout": 10,  # timeout in seconds
+    }
+
 
 def readToolGroups():
     """
@@ -4140,7 +4146,7 @@
 
 def getPdfViewer(key):
     """
-    Module function to retrieve the Pdf Viewer related settings.
+    Module function to retrieve the PDF Viewer related settings.
 
     @param key the key of the value to get
     @type str
@@ -4173,7 +4179,7 @@
 
 def setPdfViewer(key, value):
     """
-    Module function to store the Pdf Viewer related settings.
+    Module function to store the PDF Viewer related settings.
 
     @param key the key of the setting to be set
     @type str
@@ -4185,6 +4191,48 @@
     Prefs.settings.setValue("PdfViewer/" + key, value)
 
 
+def getEricServer(key):
+    """
+    Module function to retrieve the eric-ide server interface related settings.
+
+    @param key the key of the value to get
+    @type str
+    @return the requested user setting
+    @rtype Any
+    """
+    prefix = "EricServer/"
+
+    if key in ("ConnectionTimeout",):
+        return int(
+            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:
+            return json.loads(jsonStr)
+        else:
+            return None
+    else:
+        return Prefs.settings.value(f"{prefix}{key}", Prefs.ericServerDefaults[key])
+
+
+def setEricServer(key, value):
+    """
+    Module function to store the eric-ide server interface related settings.
+
+    @param key the key of the setting to be set
+    @type str
+    @param value the value to be set
+    @type Any
+    """
+    prefix = "EricServer/"
+
+    if key in ("ConnectionProfiles",):
+        Prefs.settings.setValue(f"{prefix}{key}", json.dumps(value))
+    else:
+        Prefs.settings.setValue(f"{prefix}{key}", value)
+
+
 def getGeometry(key):
     """
     Module function to retrieve the display geometry.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServer/EricRequestCategory.py	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing an enum for the various service categories.
+"""
+
+import enum
+
+class EricRequestCategory(enum.IntEnum):
+    """
+    Class defining the service categories of the eric remote server.
+    """
+    FileSystem = 0
+    Project = 1
+    Debugger = 2
+
+    Echo = 253  # only used for testing
+    Error = 254
+    Server = 255  # used by the remote server internally
+
+    # user/plugins may define own categories starting with this value
+    UserCategory = 1024
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServer/EricServer.py	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,332 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the eric remote server.
+"""
+
+import io
+import json
+import select
+import socket
+import struct
+import sys
+import traceback
+import zlib
+
+from eric7.UI.Info import Version
+
+from .EricRequestCategory import EricRequestCategory
+
+
+class EricServer:
+    """
+    Class implementing the eric remote server.
+    """
+
+    def __init__(self, port=42024, useIPv6=False):
+        """
+        Constructor
+
+        @param port port to listen on (defaults to 42024)
+        @type int (optional)
+        @param useIPv6 flag indicating to use IPv6 protocol (defaults to False)
+        @type bool (optional)
+        """
+        self.__requestCategoryHandlerRegistry = {
+            # Dictionary containing the defined and registered request category
+            # handlers. The key is the request category and the value is the respective
+            # handler method. This method must have the signature:
+            #     handler(request:str, params:dict, reqestUuid:str) -> None
+            EricRequestCategory.Debugger: None,  # TODO: not implemented yet
+            EricRequestCategory.Echo: self.__handleEchoRequest,
+            EricRequestCategory.FileSystem: None,  # TODO: not implemented yet
+            EricRequestCategory.Project: None,  # TODO: not implemented yet
+            EricRequestCategory.Server: self.__handleServerRequest
+        }
+
+        self.__connection = None
+
+        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
+            )
+
+    #######################################################################
+    ## Methods for receiving requests and sending the results.
+    #######################################################################
+
+    def sendJson(self, category, reply, params, reqestUuid=""):
+        """
+        Public method to send a single refactoring command to the server.
+
+        @param category service category
+        @type EricRequestCategory
+        @param reply reply name to be sent
+        @type str
+        @param params dictionary of named parameters for the request
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+            (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")
+        header = struct.pack(b"!II", len(data), zlib.adler32(data) & 0xFFFFFFFF)
+        self.__connection.sendall(header)
+        self.__connection.sendall(data)
+
+    def __receiveBytes(self, length):
+        """
+        Private method to receive the given length of bytes.
+
+        @param length bytes to receive
+        @type int
+        @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
+
+            data += newData
+        return data
+
+    def __receiveJson(self):
+        """
+        Private method to receive a JSON encoded command and data from the
+        server.
+
+        @return tuple containing the received service category, the command,
+            a dictionary containing the associated data and the UUID of the
+            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)
+
+        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(),
+                },
+            )
+            return EricRequestCategory.Error, None, None, None
+
+        category = requestDict["category"]
+        request = requestDict["request"]
+        params = requestDict["params"]
+        reqestUuid = requestDict["uuid"]
+
+        return category, request, params, reqestUuid
+
+    #######################################################################
+    ## Methods for the server main loop.
+    #######################################################################
+
+    def __shutdown(self):
+        """
+        Private method to shut down the server.
+        """
+        self.__socket.shutdown(socket.SHUT_RDWR)
+        self.__socket.close()
+
+    def run(self):
+        """
+        Public method implementing the remote server main loop.
+
+        Exiting the inner loop, that receives and dispatches the requests, will
+        cause the server to stop and exit. The main loop handles these requests.
+        <ul>
+        <li>exit - exit the handler loop and wait for the next connection</li>
+        <li>shutdown - exit the handler loop and perform a clean shutdown</li>
+        </ul>
+
+        @return flag indicating a clean shutdown
+        @rtype bool
+        """
+        shutdown = False
+        cleanExit = True
+
+        # listen on the server socket for new connections
+        self.__socket.listen(1)
+
+        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
+
+            except KeyboardInterrupt:
+                # intercept user pressing Ctrl+C
+                shutdown = True
+
+            except Exception:
+                exctype, excval, exctb = sys.exc_info()
+                tbinfofile = io.StringIO()
+                traceback.print_tb(exctb, None, tbinfofile)
+                tbinfofile.seek(0)
+                tbinfo = tbinfofile.read()
+                
+                print(f"{str(exctype)} / {str(excval)} / {tbinfo}")
+
+                shutdown = True
+                cleanExit = False
+
+            if self.__connection is not None:
+                self.__connection.shutdown(socket.SHUT_RDWR)
+                self.__connection.close()
+                self.__connection = None
+
+            if shutdown:
+                # exit the outer loop and shut down the server
+                self.__shutdown()
+                break
+
+        return cleanExit
+
+    #######################################################################
+    ## Request handler methods.
+    #######################################################################
+
+    def __handleRequest(self, category, request, params, reqestUuid):
+        """
+        Private method handling or dispatching the received requests.
+
+        @param category category of the request
+        @type EricRequestCategory
+        @param request request name
+        @type str
+        @param params request parameters
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+            (defaults to "", i.e. no UUID received)
+        @type str
+        """
+        try:
+            handler = self.__requestCategoryHandlerRegistry[category]
+            if handler is None:
+                raise ValueError("invalid handler function")
+            handler(request=request, params=params, reqestUuid=reqestUuid)
+        except (KeyError, ValueError):
+            self.sendJson(
+                category=EricRequestCategory.Error,
+                reply="UnsupportedServiceCategory",
+                params={"Category": category},
+            )
+
+    def __handleEchoRequest(self, request, params, reqestUuid):
+        """
+        Private method to handle an 'Echo' request.
+
+        @param request request name
+        @type str
+        @param params request parameters
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+            (defaults to "", i.e. no UUID received)
+        @type str
+        """
+        self.sendJson(
+            category=EricRequestCategory.Echo,
+            reply="Echo",
+            params=params,
+            reqestUuid=reqestUuid,
+        )
+
+    def __handleServerRequest(self, request, params, reqestUuid):
+        """
+        Private method to handle a 'Server' request.
+
+        @param request request name
+        @type str
+        @param params request parameters
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+            (defaults to "", i.e. no UUID received)
+        @type str
+        """
+        # 'Exit' and 'Shutdown' are handled in the 'run()' method.
+
+        if request.lower() == "versions":
+            self.sendJson(
+                category=EricRequestCategory.Server,
+                reply="Versions",
+                params={
+                    "python": sys.version.split()[0],
+                    "py_bitsize": "64-Bit" if sys.maxsize > 2**32 else "32-Bit",
+                    "version": Version,
+                },
+                reqestUuid=reqestUuid,
+            )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServer/__init__.py	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Package implementing the components of the eric-ide remote server.
+"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerConnectionDialog.py	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+
+"""
+Module implementing a dialog to enter the parameters for a connection to an eric-ide
+server.
+"""
+
+import ipaddress
+
+from PyQt6.QtCore import pyqtSlot
+from PyQt6.QtWidgets import QDialog, QDialogButtonBox
+
+from eric7 import Preferences
+
+from .Ui_EricServerConnectionDialog import Ui_EricServerConnectionDialog
+
+
+class EricServerConnectionDialog(QDialog, Ui_EricServerConnectionDialog):
+    """
+    Class implementing a dialog to enter the parameters for a connection to an eric-ide
+    server.
+    """
+
+    def __init__(self, profileNames=None, parent=None):
+        """
+        Constructor
+
+        @param profileNames list of defined connection profile names (defaults to None)
+        @type list of str (optional)
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.timeoutSpinBox.setToolTip(
+            self.tr("Enter the timeout for the connection attempt (default: {0} s.")
+            .format(Preferences.getEricServer("ConnectionTimeout"))
+        )
+
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False)
+
+        if profileNames is None:
+            self.nameLabel.setVisible(False)
+            self.nameEdit.setVisible(False)
+            self.nameEdit.setEnabled(False)
+
+        self.__profileNames = profileNames[:] if bool(profileNames) else []
+        self.__originalName = ""
+
+        self.nameEdit.textChanged.connect(self.__updateOK)
+        self.hostnameEdit.textChanged.connect(self.__updateOK)
+
+        msh = self.minimumSizeHint()
+        self.resize(max(self.width(), msh.width()), msh.height())
+
+    @pyqtSlot(str)
+    def on_hostnameEdit_textChanged(self, hostname):
+        """
+        Private slot handling a change of the hostname.
+
+        @param hostname text of the host name field
+        @type str
+        """
+    @pyqtSlot()
+    def __updateOK(self):
+        """
+        Private slot to update the enabled state of the OK button.
+        """
+        hostname = self.hostnameEdit.text()
+
+        if hostname and hostname[0] in "0123456789" and ":" not in hostname:
+            # possibly an IPv4 address
+            try:
+                ipaddress.IPv4Address(hostname)
+                valid = True
+            except ipaddress.AddressValueError:
+                # leading zeros are not allowed
+                valid = False
+        elif ":" in hostname:
+            # possibly an IPv6 address
+            try:
+                ipaddress.IPv6Address(hostname)
+                valid = True
+            except ipaddress.AddressValueError:
+                # leading zeros are not allowed
+                valid = False
+        elif ":" not in hostname:
+            valid = bool(hostname)
+        else:
+            valid = False
+
+        if self.nameEdit.isEnabled():
+            # connection profile mode
+            name = self.nameEdit.text()
+            valid &=  name == self.__originalName or name not in self.__profileNames
+
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(valid)
+
+    def getData(self):
+        """
+        Public method to get the entered data.
+
+        @return tuple containing the entered host name or IP address, the port number
+            and the timeout (in seconds)
+        @rtype tuple of (str, int, int)
+        """
+        port = self.portSpinBox.value()
+        if port == self.portSpinBox.minimum():
+            port = None
+
+        timeout = self.timeoutSpinBox.value()
+        if timeout == self.timeoutSpinBox.minimum():
+            timeout = None
+
+        return self.hostnameEdit.text(), port, timeout
+
+    def getProfileData(self):
+        """
+        Public method to get the entered data for connection profile mode.
+
+        @return tuple containing the profile name, host name or IP address,
+            the port number and the timeout (in seconds)
+        @rtype tuple of (str, str, int, int)
+        """
+        port = self.portSpinBox.value()
+        if port == self.portSpinBox.minimum():
+            port = 0
+
+        timeout = self.timeoutSpinBox.value()
+        if timeout == self.timeoutSpinBox.minimum():
+            timeout = 0
+
+        return self.nameEdit.text(), self.hostnameEdit.text(), port, timeout
+
+    def setProfileData(self, name, hostname, port, timeout):
+        """
+        Public method to set the connection profile data to be edited.
+
+        @param name profile name
+        @type str
+        @param hostname host name or IP address
+        @type str
+        @param port port number
+        @type int
+        @param timeout timeout value in seconds
+        @type int
+        """
+        # adjust some values
+        if not bool(port):
+            port = self.portSpinBox.minimum()
+        if not bool(timeout):
+            timeout = self.timeoutSpinBox.minimum()
+
+        self.__originalName = name
+
+        self.nameEdit.setText(name)
+        self.hostnameEdit.setText(hostname)
+        self.portSpinBox.setValue(port)
+        self.timeoutSpinBox.setValue(timeout)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerConnectionDialog.ui	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EricServerConnectionDialog</class>
+ <widget class="QDialog" name="EricServerConnectionDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>169</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>eric-ide Server Connection</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QLabel" name="nameLabel">
+     <property name="text">
+      <string>Name:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1" colspan="2">
+    <widget class="QLineEdit" name="nameEdit">
+     <property name="toolTip">
+      <string>Enter the name for the eric-ide server connection profile.</string>
+     </property>
+     <property name="clearButtonEnabled">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Hostname:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1" colspan="2">
+    <widget class="QLineEdit" name="hostnameEdit">
+     <property name="toolTip">
+      <string>Enter the hostname or IP address of the eric-ide server to connect to.</string>
+     </property>
+     <property name="clearButtonEnabled">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>Port:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <widget class="QSpinBox" name="portSpinBox">
+     <property name="toolTip">
+      <string>Enter the port number the eric-ide server listens on (default: 42024).</string>
+     </property>
+     <property name="wrapping">
+      <bool>true</bool>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+     </property>
+     <property name="specialValueText">
+      <string>default</string>
+     </property>
+     <property name="accelerated">
+      <bool>true</bool>
+     </property>
+     <property name="showGroupSeparator" stdset="0">
+      <bool>true</bool>
+     </property>
+     <property name="minimum">
+      <number>1024</number>
+     </property>
+     <property name="maximum">
+      <number>65535</number>
+     </property>
+     <property name="value">
+      <number>1024</number>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="2">
+    <spacer name="horizontalSpacer">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>240</width>
+       <height>20</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item row="3" column="0">
+    <widget class="QLabel" name="label_3">
+     <property name="text">
+      <string>Timeout:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="1">
+    <widget class="QSpinBox" name="timeoutSpinBox">
+     <property name="alignment">
+      <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+     </property>
+     <property name="specialValueText">
+      <string>default</string>
+     </property>
+     <property name="suffix">
+      <string> s</string>
+     </property>
+     <property name="maximum">
+      <number>60</number>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="0" colspan="3">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>nameEdit</tabstop>
+  <tabstop>hostnameEdit</tabstop>
+  <tabstop>portSpinBox</tabstop>
+  <tabstop>timeoutSpinBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>EricServerConnectionDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>EricServerConnectionDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerInterface.py	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,680 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the interface to the eric remote server.
+"""
+
+import json
+import struct
+import uuid
+import zlib
+
+from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
+from PyQt6.QtGui import QAction, QKeySequence
+from PyQt6.QtNetwork import QAbstractSocket, QTcpSocket
+from PyQt6.QtWidgets import QDialog, QMenu, QToolBar, QToolButton
+
+from eric7 import Preferences, Utilities
+from eric7.EricGui import EricPixmapCache
+from eric7.EricGui.EricAction import EricAction
+from eric7.EricWidgets import EricMessageBox
+from eric7.RemoteServer.EricRequestCategory import EricRequestCategory
+
+
+class EricServerInterface(QObject):
+    """
+    Class implementing the interface to the eric remote server.
+
+    @signal showMenu(name:str, menu:QMenu) emitted when a menu is about to be shown.
+        The name of the menu and a reference to the menu are given.
+
+    @signal connectionStateChanged(state:bool) emitted to indicate a change of the
+        connection state
+    @signal remoteReply(category:int, request:str, params:dict) emitted to deliver the
+        reply of an unknown category
+    @signal remoteDebuggerReply(request:str, params:dict) emitted to deliver the reply
+        of a remote server debugger request
+    @signal remoteEchoReply(request:str, params:dict) emitted to deliver the reply of
+        a remote server echo request
+    @signal remoteFileSystemReply(request:str, params:dict) emitted to deliver the
+        reply of a remote server file system request
+    @signal remoteProjectReply(request:str, params:dict) emitted to deliver the reply
+        of a remote server project related request
+    @signal remoteServerReply(request:str, params:dict) emitted to deliver the reply
+        of a remote server control request
+    """
+
+    showMenu = pyqtSignal(str, QMenu)
+
+    connectionStateChanged = pyqtSignal(bool)
+
+    remoteReply = pyqtSignal(int, str, dict)
+
+    remoteDebuggerReply = pyqtSignal(str, dict)
+    remoteEchoReply = pyqtSignal(str, dict)
+    remoteFileSystemReply = pyqtSignal(str, dict)
+    remoteProjectReply = pyqtSignal(str, dict)
+    remoteServerReply = pyqtSignal(str, dict)
+
+    def __init__(self, parent=None):
+        """
+        Constructor
+
+        @param parent reference to the parent object (defaults to None)
+        @type QObject (optional)
+        """
+        super().__init__(parent=parent)
+
+        self.__ui = parent
+
+        self.__categorySignalMapping = {
+            EricRequestCategory.Debugger: self.remoteDebuggerReply,
+            EricRequestCategory.Echo: self.remoteEchoReply,
+            EricRequestCategory.FileSystem: self.remoteFileSystemReply,
+            EricRequestCategory.Project: self.remoteProjectReply,
+            EricRequestCategory.Server: self.remoteServerReply,
+        }
+
+        self.__connection = None
+        self.__callbacks = {}  # callback references indexed by UUID
+
+        self.connectionStateChanged.connect(self.__connectionStateChanged)
+
+    #######################################################################
+    ## Methods for handling the server connection.
+    #######################################################################
+
+    def connectToServer(self, host, port=None, timeout=None):
+        """
+        Public method to connect to the given host and port
+
+        @param host host name or IP address of the eric remote server
+        @type str
+        @param port port number to connect to (defaults to None)
+        @type int (optional)
+        @param timeout timeout im seconds for the connection attempt
+            (defaults to None)
+        @type int (optional)
+        """
+        if not bool(port):  # None or 0
+            # use default port
+            port = 42024
+
+        if not bool(timeout):  # None or 0
+            # use configured default timeout
+            timeout = Preferences.getEricServer("ConnectionTimeout")
+        timeout = timeout * 1000  # convert to milliseconds
+
+        if self.__connection is not None:
+            self.disconnectFromServer()
+
+        self.__connection = QTcpSocket(self)
+        self.__connection.connectToHost(host, port)
+        if not self.__connection.waitForConnected(timeout):
+            EricMessageBox.critical(
+                None,
+                self.tr("Connect to eric-ide Server"),
+                self.tr(
+                    "<p>The connection to the eric-ide server {0}:{1} could not be"
+                    " established.</p><p>Reason: {2}</p>"
+                ).format(
+                    host if ":" not in host else f"[{host}]",
+                    port,
+                    self.__connection.errorString(),
+                ),
+            )
+            
+            self.__connection = None
+            return False
+
+        self.__connection.readyRead.connect(self.__receiveJson)
+        self.__connection.disconnected.connect(self.__handleDisconnect)
+
+        self.connectionStateChanged.emit(True)
+
+        return True
+
+    @pyqtSlot()
+    def disconnectFromServer(self):
+        """
+        Public method to disconnect from the eric remote server.
+        """
+        if self.__connection is not None and self.__connection.isValid():
+            self.__connection.disconnectFromHost()
+            if self.__connection is not None:
+                # may have disconnected already
+                self.__connection.waitForDisconnected(
+                    Preferences.getEricServer("ConnectionTimeout") * 1000
+                )
+
+                self.connectionStateChanged.emit(False)
+                self.__connection = None
+                self.__callbacks.clear()
+
+    def isServerConnected(self):
+        """
+        Public method to check, if a connection to an eric-ide server has been
+        established.
+
+        @return flag indicating the interface connection state
+        @rtype bool
+        """
+        return (
+            self.__connection is not None
+            and self.__connection.state() == QAbstractSocket.SocketState.ConnectedState
+        )
+
+    @pyqtSlot()
+    def __handleDisconnect(self):
+        """
+        Private slot handling a disconnect of the client.
+        """
+        if self.__connection is not None:
+            self.__connection.close()
+
+        self.connectionStateChanged.emit(False)
+        self.__connection = None
+        self.__callbacks.clear()
+
+    #######################################################################
+    ## Methods for sending requests and receiving the replies.
+    #######################################################################
+
+    @pyqtSlot()
+    def __receiveJson(self):
+        """
+        Private slot handling received data from the eric remote server.
+
+        @param idString id of the connection
+        @type str
+        """
+        while self.__connection and self.__connection.bytesAvailable():
+            header = self.__connection.read(struct.calcsize(b"!II"))
+            length, datahash = struct.unpack(b"!II", header)
+
+            data = bytearray()
+            while len(data) < length:
+                maxSize = length - len(data)
+                if self.__connection.bytesAvailable() < maxSize:
+                    self.__connection.waitForReadyRead(50)
+                data += self.__connection.read(maxSize)
+
+            if zlib.adler32(data) & 0xFFFFFFFF != datahash:
+                # corrupted data -> discard and continue
+                continue
+
+            jsonString = data.decode("utf-8", "backslashreplace")
+
+            # - print("Remote Server Interface: {0}".format(jsonString))
+            # - this is for debugging only
+
+            try:
+                serverDataDict = json.loads(jsonString.strip())
+            except (TypeError, ValueError) as err:
+                EricMessageBox.critical(
+                    None,
+                    self.tr("JSON Protocol Error"),
+                    self.tr(
+                        """<p>The response received from the remote server"""
+                        """ could not be decoded. Please report"""
+                        """ this issue with the received data to the"""
+                        """ eric bugs email address.</p>"""
+                        """<p>Error: {0}</p>"""
+                        """<p>Data:<br/>{1}</p>"""
+                    ).format(str(err), Utilities.html_encode(jsonString.strip())),
+                    EricMessageBox.Ok,
+                )
+                return
+
+            reqUuid = serverDataDict["uuid"]
+            try:
+                self.__callbacks[reqUuid](
+                    serverDataDict["reply"], serverDataDict["params"]
+                )
+                del self.__callbacks[reqUuid]
+            except KeyError:
+                # no callback for this UUID exists, send a signal
+                try:
+                    self.__categorySignalMapping[serverDataDict["category"]].emit(
+                        serverDataDict["reply"], serverDataDict["params"]
+                    )
+                except KeyError:
+                    if serverDataDict["category"] == EricRequestCategory.Error:
+                        # handle server errors in here
+                        self.__handleServerError(
+                            serverDataDict["reply"], serverDataDict["params"]
+                        )
+                    else:
+                        self.remoteReply.emit(
+                            serverDataDict["category"],
+                            serverDataDict["reply"],
+                            serverDataDict["params"],
+                        )
+
+    def sendJson(self, category, request, params, callback=None, flush=False):
+        """
+        Public method to send a single command to a client.
+
+        @param category service category
+        @type EricRequestCategory
+        @param request request name to be sent
+        @type str
+        @param params dictionary of named parameters for the request
+        @type dict
+        @param callback callback function for the reply from the eric remote server
+            (defaults to None)
+        @type function (optional)
+        @param flush flag indicating to flush the data to the socket
+            (defaults to False)
+        @type bool (optional)
+        """
+        reqUuid = str(uuid.uuid4())
+        if callback:
+            self.__callbacks[reqUuid] = callback
+
+        serviceDict = {
+            "jsonrpc": "2.0",
+            "category": category,
+            "request": request,
+            "params": params,
+            "uuid": reqUuid,
+        }
+        jsonString = json.dumps(serviceDict) + "\n"
+
+        if self.__connection is not None:
+            data = jsonString.encode("utf8", "backslashreplace")
+            header = struct.pack(b"!II", len(data), zlib.adler32(data) & 0xFFFFFFFF)
+            self.__connection.write(header)
+            self.__connection.write(data)
+            if flush:
+                self.__connection.flush()
+
+    def shutdownServer(self):
+        """
+        Public method shutdown the currebtly connected eric-ide remote server.
+        """
+        if self.__connection:
+            self.sendJson(
+                category=EricRequestCategory.Server,
+                request="Shutdown",
+                params={},
+            )
+
+    @pyqtSlot()
+    def serverVersions(self):
+        """
+        Public slot to request the eric-ide version of the server.
+        """
+        if self.__connection:
+            self.sendJson(
+                category=EricRequestCategory.Server,
+                request="Versions",
+                params={},
+                callback=self.__handleServerVersionReply,
+            )
+
+    #######################################################################
+    ## Callback methods
+    #######################################################################
+
+    def __handleServerVersionReply(self, reply, params):
+        """
+        Private method to handle the reply of a 'Version' request.
+
+        @param reply name of the eric-ide server reply
+        @type str
+        @param params dictionary containing the reply data
+        @type dict
+        @exception ValueError raised in case of an unsupported reply
+        """
+        if reply == "Versions":
+            versionText = self.tr("""<h2>Server Version Numbers</h2><table>""")
+    
+            # Python version
+            versionText += (
+                """<tr><td><b>Python</b></td><td>{0}, {1}</td></tr>"""
+            ).format(params["python"], params["py_bitsize"])
+
+            # eric7 version
+            versionText += (
+                """<tr><td><b>eric7_server</b></td><td>{0}</td></tr>"""
+            ).format(params["version"])
+
+            versionText += self.tr("""</table>""")
+
+            EricMessageBox.about(
+                None,
+                self.tr("eric-ide Server Versions"),
+                versionText,
+            )
+
+        else:
+            raise ValueError(f"unsupported reply received ({reply})")
+
+    #######################################################################
+    ## Reply handler methods
+    #######################################################################
+
+    def __handleServerError(self, reply, params):
+        """
+        Public method handling server error replies.
+
+        @param reply name of the error reply
+        @type str
+        @param params dictionary containing the specific reply data
+        @type dict
+        """
+        if reply == "ClientChecksumException":
+            self.__ui.appendToStderr(
+                self.tr("eric-ide Server Checksum Error\nError: {0}\nData:\n{1}\n")
+                .format(params["ExceptionValue"], params["ProtocolData"])
+            )
+
+        elif reply == "ClientException":
+            self.__ui.appendToStderr(
+                self.tr("eric-ide Server Data Error\nError: {0}\nData:\n{1}\n")
+                .format(params["ExceptionValue"], params["ProtocolData"])
+            )
+
+        elif reply == "UnsupportedServiceCategory":
+            self.__ui.appendToStderr(
+                self.tr(
+                    "eric-ide Server Unsupported Category\n"
+                    "Error: The server received the unsupported request category '{0}'."
+                ).format(params["Category"])
+            )
+
+    #######################################################################
+    ## User interface related methods
+    #######################################################################
+
+    def initActions(self):
+        """
+        Public slot to initialize the eric-ide server actions.
+        """
+        self.actions = []
+
+        self.connectServerAct = EricAction(
+            self.tr("Connect"),
+            EricPixmapCache.getIcon("linkConnect"),
+            self.tr("Connect..."),
+            QKeySequence(self.tr("Meta+Shift+C")),
+            0,
+            self,
+            "remote_server_connect",
+        )
+        self.connectServerAct.setStatusTip(
+            self.tr("Show a dialog to connect to an 'eric-ide' server")
+        )
+        self.connectServerAct.setWhatsThis(
+            self.tr(
+                """<b>Connect...</b>"""
+                """<p>This opens a dialog to enter the connection parameters to"""
+                """ connect to a remote 'eric-ide' server.</p>"""
+            )
+        )
+        self.connectServerAct.triggered.connect(self.__connectToServer)
+        self.actions.append(self.connectServerAct)
+
+        self.disconnectServerAct = EricAction(
+            self.tr("Disconnect"),
+            EricPixmapCache.getIcon("linkDisconnect"),
+            self.tr("Disconnect"),
+            QKeySequence(self.tr("Meta+Shift+D")),
+            0,
+            self,
+            "remote_server_disconnect",
+        )
+        self.disconnectServerAct.setStatusTip(
+            self.tr("Disconnect from the currently connected 'eric-ide' server")
+        )
+        self.disconnectServerAct.setWhatsThis(
+            self.tr(
+                """<b>Disconnect</b>"""
+                """<p>This disconnects from the currently connected 'eric-ide'"""
+                """ server.</p>"""
+            )
+        )
+        self.disconnectServerAct.triggered.connect(self.disconnectFromServer)
+        self.actions.append(self.disconnectServerAct)
+
+        self.stopServerAct = EricAction(
+            self.tr("Stop Server"),
+            EricPixmapCache.getIcon("stopScript"),
+            self.tr("Stop Server"),
+            QKeySequence(self.tr("Meta+Shift+S")),
+            0,
+            self,
+            "remote_server_shutdown",
+        )
+        self.stopServerAct.setStatusTip(
+            self.tr("Stop the currently connected 'eric-ide' server")
+        )
+        self.stopServerAct.setWhatsThis(
+            self.tr(
+                """<b>Stop Server</b>"""
+                """<p>This stops the currently connected 'eric-ide server.</p>"""
+            )
+        )
+        self.stopServerAct.triggered.connect(self.__shutdownServer)
+        self.actions.append(self.stopServerAct)
+
+        self.serverVersionsAct = EricAction(
+            self.tr("Show Server Versions"),
+            EricPixmapCache.getIcon("helpAbout"),
+            self.tr("Show Server Versions"),
+            0,
+            0,
+            self,
+            "remote_server_versions",
+        )
+        self.serverVersionsAct.setStatusTip(
+            self.tr("Show the eric-ide server versions")
+        )
+        self.serverVersionsAct.setWhatsThis(
+            self.tr(
+                """<b>Show Server Versions</b>"""
+                """<p>This opens a dialog to show the eric-ide server versions.</p>"""
+            )
+        )
+        self.serverVersionsAct.triggered.connect(self.serverVersions)
+        self.actions.append(self.serverVersionsAct)
+
+        self.disconnectServerAct.setEnabled(False)
+        self.stopServerAct.setEnabled(False)
+        self.serverVersionsAct.setEnabled(False)
+
+    def initMenu(self):
+        """
+        Public slot to initialize the eric-ide server menu.
+
+        @return the menu generated
+        @rtype QMenu
+        """
+        self.__serverProfilesMenu = QMenu(self.tr("Connect to"))##, self.__ui)
+        self.__serverProfilesMenu.aboutToShow.connect(self.__showServerProfilesMenu)
+        self.__serverProfilesMenu.triggered.connect(self.__serverProfileTriggered)
+
+        menu = QMenu(self.tr("eric-ide Server"), self.__ui)
+        menu.setTearOffEnabled(True)
+        menu.aboutToShow.connect(self.__showEricServerMenu)
+        menu.addAction(self.connectServerAct)
+        menu.addMenu(self.__serverProfilesMenu)
+        # TODO: add a 'Recent Connections' submenu
+        menu.addSeparator()
+        menu.addAction(self.disconnectServerAct)
+        menu.addSeparator()
+        menu.addAction(self.stopServerAct)
+        menu.addSeparator()
+        menu.addAction(self.serverVersionsAct)
+
+        self.__menus = {
+            "Main": menu,
+            ##"Recent": self.recentMenu,
+        }
+
+
+        return menu
+
+    def initToolbar(self, toolbarManager):
+        """
+        Public slot to initialize the eric-ide server toolbar.
+
+        @param toolbarManager reference to a toolbar manager object
+        @type EricToolBarManager
+        @return the toolbar generated
+        @rtype QToolBar
+        """
+        self.__connectButton = QToolButton()
+        self.__connectButton.setIcon(self.connectServerAct.icon())
+        self.__connectButton.setToolTip(self.connectServerAct.toolTip())
+        self.__connectButton.setWhatsThis(self.connectServerAct.whatsThis())
+        self.__connectButton.setPopupMode(
+            QToolButton.ToolButtonPopupMode.MenuButtonPopup
+        )
+        self.__connectButton.setMenu(self.__serverProfilesMenu)
+        self.connectServerAct.enabledChanged.connect(self.__connectButton.setEnabled)
+        self.__connectButton.clicked.connect(self.connectServerAct.triggered)
+
+        tb = QToolBar(self.tr("eric-ide Server"), self.__ui)
+        tb.setObjectName("EricServerToolbar")
+        tb.setToolTip(self.tr("eric-ide Server"))
+
+        act = tb.addWidget(self.__connectButton)
+        act.setText(self.connectServerAct.iconText())
+        act.setIcon(self.connectServerAct.icon())
+        tb.addAction(self.disconnectServerAct)
+        tb.addSeparator()
+        tb.addAction(self.stopServerAct)
+        tb.addSeparator()
+        tb.addAction(self.serverVersionsAct)
+
+        toolbarManager.addToolBar(tb, tb.windowTitle())
+
+        return tb
+
+    @pyqtSlot()
+    def __showEricServerMenu(self):
+        """
+        Private slot to display the server menu.
+        """
+        connected = self.isServerConnected()
+        self.connectServerAct.setEnabled(not connected)
+        self.disconnectServerAct.setEnabled(connected)
+        self.stopServerAct.setEnabled(connected)
+        self.serverVersionsAct.setEnabled(connected)
+
+        self.showMenu.emit("Main", self.__menus["Main"])
+
+    @pyqtSlot()
+    def __showServerProfilesMenu(self):
+        """
+        Private slot to prepare the eric server profiles menu.
+        """
+        profiles = Preferences.getEricServer("ConnectionProfiles")
+
+        self.__serverProfilesMenu.clear()
+
+        if not self.isServerConnected():
+            for profile in sorted(profiles):
+                act = self.__serverProfilesMenu.addAction(profile)
+                act.setData(profiles[profile])
+            self.__serverProfilesMenu.addSeparator()
+
+        self.__serverProfilesMenu.addAction(
+            self.tr("Manage Server Connections"), self.__manageServerProfiles
+        )
+
+    @pyqtSlot(bool)
+    def __connectionStateChanged(self, connected):
+        """
+        Private slot to handle the connection state change.
+
+        @param connected flag indicating the connection state
+        @type bool
+        """
+        self.connectServerAct.setEnabled(not connected)
+        self.disconnectServerAct.setEnabled(connected)
+        self.stopServerAct.setEnabled(connected)
+        self.serverVersionsAct.setEnabled(connected)
+
+        if connected:
+            peerName = self.__connection.peerName()
+            EricMessageBox.information(
+                None,
+                self.tr("Connect to eric-ide Server"),
+                self.tr(
+                    "<p>The eric-ide server at <b>{0}:{1}</b> was connected"
+                    " successfully.</p>"
+                ).format(
+                    f"[{peerName}]" if ":" in peerName else peerName,
+                    self.__connection.peerPort(),
+                ),
+            )
+        else:
+            EricMessageBox.information(
+                None,
+                self.tr("Disonnect from eric-ide Server"),
+                self.tr("""The eric-ide server was disconnected."""),
+            )
+
+    @pyqtSlot()
+    def __connectToServer(self):
+        """
+        Private slot to connect to a remote eric-ide server.
+        """
+        from .EricServerConnectionDialog import EricServerConnectionDialog
+
+        dlg = EricServerConnectionDialog(parent=self.__ui)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            hostname, port, timeout = dlg.getData()
+            self.connectToServer(hostname, port=port, timeout=timeout)
+
+    @pyqtSlot()
+    def __shutdownServer(self):
+        """
+        Private slot to shut down the currently connected eric-ide server.
+        """
+        ok = EricMessageBox.yesNo(
+            None,
+            self.tr("Stop Server"),
+            self.tr(
+                "Do you really want to stop the currently connected eric-ide server?"
+                " No further connections will be possible without restarting the"
+                " server."
+            ),
+        )
+        if ok:
+            self.shutdownServer()
+
+    @pyqtSlot(QAction)
+    def __serverProfileTriggered(self, act):
+        """
+        Private slot to handle the selection of a remote server connection.
+
+        @param act reference to the triggered profile action
+        @type QAction
+        """
+        data = act.data()
+        if data is not None:
+            # handle the connection
+            hostname, port, timeout = data
+            self.connectToServer(hostname, port=port, timeout=timeout)
+
+    @pyqtSlot()
+    def __manageServerProfiles(self):
+        """
+        Private slot to show a dialog to manage the eric-ide server connection
+        profiles.
+        """
+        from .EricServerProfilesDialog import EricServerProfilesDialog
+
+        dlg = EricServerProfilesDialog(
+            Preferences.getEricServer("ConnectionProfiles"), self.__ui
+        )
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            profiles = dlg.getConnectionProfiles()
+            Preferences.setEricServer("ConnectionProfiles", profiles)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerProfilesDialog.py	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to manage server connection profiles.
+"""
+
+import copy
+
+from PyQt6.QtCore import Qt, pyqtSlot
+from PyQt6.QtWidgets import QDialog, QListWidgetItem
+
+from eric7.EricWidgets import EricMessageBox
+
+from .EricServerConnectionDialog import EricServerConnectionDialog
+from .Ui_EricServerProfilesDialog import Ui_EricServerProfilesDialog
+
+
+class EricServerProfilesDialog(QDialog, Ui_EricServerProfilesDialog):
+    """
+    Class implementing a dialog to manage server connection profiles.
+    """
+
+    def __init__(self, profiles, parent=None):
+        """
+        Constructor
+
+        @param profiles dictionary containing the server connection profiles
+        @type dict
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.__profiles = copy.deepcopy(profiles)
+        self.__populateProfilesList()
+
+        self.on_connectionsList_itemSelectionChanged()
+
+    def __populateProfilesList(self):
+        """
+        Private method to (re-) populate the list of server connection profiles.
+        """
+        self.connectionsList.clear()
+
+        for profile in self.__profiles:
+            itm = QListWidgetItem(profile, self.connectionsList)
+            itm.setData(Qt.ItemDataRole.UserRole, self.__profiles[profile])
+
+    def __getProfilesList(self):
+        """
+        Private method to get the list of defined profile names.
+
+        @return list of defined profile names
+        @rtype list of str
+        """
+        profileNames = []
+        for row in range(self.connectionsList.count()):
+            itm = self.connectionsList.item(row)
+            profileNames.append(itm.text())
+
+        return profileNames
+
+    @pyqtSlot()
+    def on_connectionsList_itemSelectionChanged(self):
+        """
+        Private slot to handle a change of selected items.
+        """
+        selectedItems = self.connectionsList.selectedItems()
+        self.editButton.setEnabled(len(selectedItems) == 1)
+        self.removeButton.setEnabled(len(selectedItems) > 0)
+
+    @pyqtSlot()
+    def on_addButton_clicked(self):
+        """
+        Private slot add a new connection profile.
+        """
+        dlg = EricServerConnectionDialog(
+            profileNames=self.__getProfilesList(), parent=self
+        )
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            profileData = dlg.getProfileData()
+            itm = QListWidgetItem(profileData[0], self.connectionsList)
+            itm.setData(Qt.ItemDataRole.UserRole, profileData[1:])
+
+    @pyqtSlot()
+    def on_editButton_clicked(self):
+        """
+        Private slot to edit the selected entry.
+        """
+        selectedItems = self.connectionsList.selectedItems()
+        if selectedItems:
+            itm = selectedItems[0]
+            dlg = EricServerConnectionDialog(
+                profileNames=self.__getProfilesList(), parent=self
+            )
+            data = itm.data(Qt.ItemDataRole.UserRole)
+            dlg.setProfileData(itm.text(), *data)
+            if dlg.exec() == QDialog.DialogCode.Accepted:
+                profileData = dlg.getProfileData()
+                itm.setText(profileData[0])
+                itm.setData(Qt.ItemDataRole.UserRole, profileData[1:])
+
+    @pyqtSlot()
+    def on_removeButton_clicked(self):
+        """
+        Private slot to remove the selected connection profiles.
+        """
+        yes = EricMessageBox.yesNo(
+            self,
+            self.tr("Remove Selected Entries"),
+            self.tr(
+                "Do you really want to remove the selected entries from the list?"
+            ),
+        )
+        if yes:
+            for itm in self.connectionsList.selectedItems()[:]:
+                self.connectionsList.takeItem(self.connectionsList.row(itm))
+                del itm
+
+    @pyqtSlot()
+    def on_resetButton_clicked(self):
+        """
+        Private slot to reset all changes performed.
+        """
+        yes = EricMessageBox.yesNo(
+            self,
+            self.tr("Reset Changes"),
+            self.tr(
+                "Do you really want to reset all changes performed up to this point?"
+            ),
+        )
+        if yes:
+            self.__populateProfilesList()
+
+    def getConnectionProfiles(self):
+        """
+        Public method to get the configured connection profiles.
+
+        @return dictionary containing the configured connection profiles
+        @rtype dict
+        """
+        profiles = {}
+
+        for row in range(self.connectionsList.count()):
+            itm = self.connectionsList.item(row)
+            profiles[itm.text()] = itm.data(Qt.ItemDataRole.UserRole)
+
+        return profiles
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerProfilesDialog.ui	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EricServerProfilesDialog</class>
+ <widget class="QDialog" name="EricServerProfilesDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>441</width>
+    <height>281</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Manage Server Connections</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QListWidget" name="connectionsList">
+     <property name="editTriggers">
+      <set>QAbstractItemView::NoEditTriggers</set>
+     </property>
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+     <property name="selectionMode">
+      <enum>QAbstractItemView::ExtendedSelection</enum>
+     </property>
+     <property name="sortingEnabled">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <layout class="QVBoxLayout" name="verticalLayout">
+     <item>
+      <widget class="QPushButton" name="addButton">
+       <property name="toolTip">
+        <string>Press to open a dialog to add a new server connection.</string>
+       </property>
+       <property name="text">
+        <string>Add...</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="editButton">
+       <property name="toolTip">
+        <string>Press to open a dialog to edit the selected server connection.</string>
+       </property>
+       <property name="text">
+        <string>Edit...</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="removeButton">
+       <property name="toolTip">
+        <string>Press to remove the selected server connections.</string>
+       </property>
+       <property name="text">
+        <string>Remove</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="verticalSpacer">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>20</width>
+         <height>40</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="resetButton">
+       <property name="toolTip">
+        <string>Press to reset all changes performed.</string>
+       </property>
+       <property name="text">
+        <string>Reset</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item row="1" column="0" colspan="2">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>EricServerProfilesDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>EricServerProfilesDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/__init__.py	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Package implementing the components of the eric-ide remote server interface.
+"""
--- a/src/eric7/UI/UserInterface.py	Fri Jan 26 16:17:05 2024 +0100
+++ b/src/eric7/UI/UserInterface.py	Mon Jan 29 19:50:44 2024 +0100
@@ -83,6 +83,7 @@
 from eric7.Preferences import Shortcuts
 from eric7.Project.Project import Project
 from eric7.QScintilla.SpellChecker import SpellChecker
+from eric7.RemoteServerInterface.EricServerInterface import EricServerInterface
 from eric7.Sessions.SessionFile import SessionFile
 from eric7.SystemUtilities import (
     DesktopUtilities,
@@ -393,6 +394,9 @@
         self.stdout = Redirector(False, self)
         self.stderr = Redirector(True, self)
 
+        # create the remote server interface
+        self.__ericServerInterface = EricServerInterface(self)
+
         # set a few dialog members for non-modal dialogs created on demand
         self.programsDialog = None
         self.shortcutsDialog = None
@@ -635,6 +639,7 @@
             ericApp().registerObject("MicroPython", self.microPythonWidget)
         ericApp().registerObject("JediAssistant", self.jediAssistant)
         ericApp().registerObject("PluginRepositoryViewer", self.pluginRepositoryViewer)
+        ericApp().registerObject("EricServer", self.__ericServerInterface)
 
         # create the various JSON file interfaces
         self.__sessionFile = SessionFile(True)
@@ -3572,6 +3577,9 @@
         # initialize multi project actions
         self.multiProject.initActions()
 
+        # initialize eric-ide server actions
+        self.__ericServerInterface.initActions()
+
     def __initQtDocActions(self):
         """
         Private slot to initialize the action to show the Qt documentation.
@@ -3781,6 +3789,12 @@
             mb.setNativeMenuBar(False)
 
         ##############################################################
+        ## Remote Server menu
+        ##############################################################
+
+        self.__menus["server"] = self.__ericServerInterface.initMenu()
+
+        ##############################################################
         ## File menu
         ##############################################################
 
@@ -3795,6 +3809,9 @@
         act = self.__menus["file"].actions()[0]
         sep = self.__menus["file"].insertSeparator(act)
         self.__menus["file"].insertAction(sep, self.newWindowAct)
+        self.__menus["file"].insertSeparator(sep)
+        self.__menus["file"].insertMenu(sep, self.__menus["server"])
+        self.__menus["file"].insertSeparator(sep)
         self.__menus["file"].aboutToShow.connect(self.__showFileMenu)
 
         ##############################################################
@@ -4092,6 +4109,7 @@
         helptb = QToolBar(self.tr("Help"), self)
         profilestb = QToolBar(self.tr("Profiles"), self)
         pluginstb = QToolBar(self.tr("Plugins"), self)
+        servertb = self.__ericServerInterface.initToolbar(self.toolbarManager)
 
         toolstb.setObjectName("ToolsToolbar")
         testingtb.setObjectName("UnittestToolbar")
@@ -4194,6 +4212,7 @@
 
         # add the various toolbars
         self.addToolBar(filetb)
+        self.addToolBar(servertb)
         self.addToolBar(edittb)
         self.addToolBar(searchtb)
         self.addToolBar(viewtb)
@@ -4244,6 +4263,7 @@
         ]
         self.__toolbars["spelling"] = [spellingtb.windowTitle(), spellingtb, ""]
         self.__toolbars["vcs"] = [vcstb.windowTitle(), vcstb, "vcs"]
+        self.__toolbars["server"] = [servertb.windowTitle(), servertb, ""]
 
     def __initDebugToolbarsLayout(self):
         """
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/eric7_server.py	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+eric-ide Server.
+
+This is the main Python script of the eric-ide server. This is a server to perform
+remote development (e.g. code hosted on another computer or through a docker
+container).
+"""
+
+import argparse
+import socket
+import sys
+
+from eric7.RemoteServer.EricServer import EricServer
+from eric7.UI.Info import Version
+
+
+def createArgumentParser():
+    """
+    Function to create an argument parser.
+
+    @return created argument parser object
+    @rtype argparse.ArgumentParser
+    """
+    parser = argparse.ArgumentParser(
+        description=(
+            "Start the eric-ide server component. This will listen for connections"
+            " from the eric IDE in order to perform remote development."
+        ),
+        epilog="Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>.",
+    )
+
+    parser.add_argument(
+        "-p",
+        "--port",
+        type=int,
+        default=42024,
+        help="Listen on the given port for connections from an eric IDE."
+    )
+    parser.add_argument(
+        "-6",
+        "--with-ipv6",
+        action="store_true",
+        help="Listen on IPv6 interfaces as well if the system supports the creation"
+        "of TCP sockets which can handle both IPv4 and IPv6. {0}".format(
+            "This system supports this feature."
+            if socket.has_dualstack_ipv6()
+            else "This system does not support this feature. Option will be ignored."
+        )
+    )
+    parser.add_argument(
+        "-V",
+        "--version",
+        action="version",
+        version="%(prog)s {0}".format(Version),
+        help="Show version information and exit.",
+    )
+
+    return parser
+
+
+def main():
+    """
+    Main entry point into the application.
+    """
+    global supportedExtensions
+
+    parser = createArgumentParser()
+    args = parser.parse_args()
+
+    server = EricServer(port=args.port, useIPv6=args.with_ipv6)
+    ok = server.run()
+
+    sys.exit(0 if ok else 1)
+
+
+if __name__ == "__main__":
+    main()
+
+#
+# eflag: noqa = M801
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-dark/preferences-eric-server.svg	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="22"
+   width="22"
+   version="1.1"
+   id="svg1718"
+   sodipodi:docname="preferences-eric-server.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1722" />
+  <sodipodi:namedview
+     id="namedview1720"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="32.09375"
+     inkscape:cx="16.623174"
+     inkscape:cy="16.327167"
+     inkscape:window-width="2580"
+     inkscape:window-height="1381"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg1718" />
+  <linearGradient
+     id="b"
+     gradientTransform="matrix(0.7,0,0,0.7,-0.7,-1.1)"
+     gradientUnits="userSpaceOnUse"
+     x2="0"
+     y1="44"
+     y2="4">
+    <stop
+       offset="0"
+       stop-color="#1d1e1e"
+       id="stop1680" />
+    <stop
+       offset="1"
+       stop-color="#44484c"
+       id="stop1682" />
+  </linearGradient>
+  <linearGradient
+     id="c"
+     gradientTransform="matrix(0.7,0,0,0.7,-270.499,-350.76)"
+     x2="0"
+     xlink:href="#a"
+     y1="507.79999"
+     y2="506.79999" />
+  <linearGradient
+     id="a"
+     gradientUnits="userSpaceOnUse"
+     x2="0"
+     y1="507.79999"
+     y2="506.79999">
+    <stop
+       offset="0"
+       stop-color="#3da103"
+       id="stop1686" />
+    <stop
+       offset="1"
+       stop-color="#7ddf07"
+       id="stop1688" />
+  </linearGradient>
+  <linearGradient
+     id="d"
+     gradientTransform="matrix(0.7,0,0,0.7,-270.499,-339.76)"
+     x2="0"
+     xlink:href="#a"
+     y1="507.79999"
+     y2="506.79999" />
+  <linearGradient
+     id="e"
+     gradientTransform="matrix(0.7,0,0,0.7,-270.499,-328.76)"
+     x2="0"
+     xlink:href="#a"
+     y1="507.79999"
+     y2="506.79999" />
+  <linearGradient
+     id="f"
+     gradientUnits="userSpaceOnUse"
+     x1="5"
+     x2="18"
+     y1="12"
+     y2="25">
+    <stop
+       offset="0"
+       stop-color="#292c2f"
+       id="stop1693"
+       style="stop-color:#c0c0c0;stop-opacity:1;" />
+    <stop
+       offset="1"
+       stop-opacity="0"
+       id="stop1695" />
+  </linearGradient>
+  <g
+     id="g2294"
+     transform="matrix(0.72413793,0,0,0.71428571,-1.1724138,-0.42857143)">
+    <path
+       d="M 3,7 H 29 V 24 H 3 Z"
+       fill="#111213"
+       id="path1698" />
+    <path
+       d="M 3,2 V 30 H 29 V 2 Z m 1,6 h 24 v 4 H 4 Z m 0,11 h 24 v 4 H 4 Z"
+       fill="url(#b)"
+       id="path1700"
+       style="fill:#ececec;stroke-width:0.7" />
+    <path
+       d="M 5,4 H 7 V 5 H 5 Z"
+       fill="url(#c)"
+       id="path1702"
+       style="fill:url(#c);stroke-width:0.7" />
+    <path
+       d="m 5,15 h 2 v 1 H 5 Z"
+       fill="url(#d)"
+       id="path1704"
+       style="fill:url(#d);stroke-width:0.7" />
+    <path
+       d="m 5,26 h 2 v 1 H 5 Z"
+       fill="url(#e)"
+       id="path1706"
+       style="fill:url(#e);stroke-width:0.7" />
+    <path
+       d="m 3,29 h 26 v 1 H 3 Z"
+       opacity="0.2"
+       id="path1710" />
+    <path
+       d="m 4,12 7,7 h 17 v 4 H 15 l 7,7 h 7 V 13 l -1,-1 z"
+       fill="url(#f)"
+       fill-rule="evenodd"
+       opacity="0.4"
+       id="path1712"
+       style="fill:url(#f)" />
+    <rect
+       fill="#eff0f1"
+       height="16"
+       rx="2"
+       width="16"
+       x="16"
+       y="14"
+       id="rect1714" />
+    <path
+       d="m 19,16 v 2 h -1 v 2 h 1 v 4 h -1 v 2 h 1 v 2 H 30 V 16 Z m 1,1 h 1 v 10 h -1 z m 2,0 h 7 v 10 h -7 z"
+       fill="#232629"
+       id="path1716" />
+  </g>
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-light/preferences-eric-server.svg	Mon Jan 29 19:50:44 2024 +0100
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="22"
+   width="22"
+   version="1.1"
+   id="svg3827"
+   sodipodi:docname="preferences-eric-server.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs3831" />
+  <sodipodi:namedview
+     id="namedview3829"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="32.09375"
+     inkscape:cx="16.747809"
+     inkscape:cy="16.389484"
+     inkscape:window-width="2580"
+     inkscape:window-height="1321"
+     inkscape:window-x="0"
+     inkscape:window-y="60"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg3827" />
+  <linearGradient
+     id="b"
+     gradientTransform="matrix(0.7,0,0,0.7,-0.7,-1.1)"
+     gradientUnits="userSpaceOnUse"
+     x2="0"
+     y1="44"
+     y2="4">
+    <stop
+       offset="0"
+       stop-color="#1d1e1e"
+       id="stop3789" />
+    <stop
+       offset="1"
+       stop-color="#44484c"
+       id="stop3791" />
+  </linearGradient>
+  <linearGradient
+     id="c"
+     gradientTransform="matrix(0.7,0,0,0.7,-270.499,-350.76)"
+     x2="0"
+     xlink:href="#a"
+     y1="507.79999"
+     y2="506.79999" />
+  <linearGradient
+     id="a"
+     gradientUnits="userSpaceOnUse"
+     x2="0"
+     y1="507.79999"
+     y2="506.79999">
+    <stop
+       offset="0"
+       stop-color="#3da103"
+       id="stop3795" />
+    <stop
+       offset="1"
+       stop-color="#7ddf07"
+       id="stop3797" />
+  </linearGradient>
+  <linearGradient
+     id="d"
+     gradientTransform="matrix(0.7,0,0,0.7,-270.499,-339.76)"
+     x2="0"
+     xlink:href="#a"
+     y1="507.79999"
+     y2="506.79999" />
+  <linearGradient
+     id="e"
+     gradientTransform="matrix(0.7,0,0,0.7,-270.499,-328.76)"
+     x2="0"
+     xlink:href="#a"
+     y1="507.79999"
+     y2="506.79999" />
+  <linearGradient
+     id="f"
+     gradientUnits="userSpaceOnUse"
+     x1="5"
+     x2="18"
+     y1="12"
+     y2="25">
+    <stop
+       offset="0"
+       stop-color="#292c2f"
+       id="stop3802" />
+    <stop
+       offset="1"
+       stop-opacity="0"
+       id="stop3804" />
+  </linearGradient>
+  <g
+     id="g5049"
+     transform="matrix(0.72413793,0,0,0.71428571,-1.1724138,-0.42857143)">
+    <path
+       d="M 3,7 H 29 V 24 H 3 Z"
+       fill="#111213"
+       id="path3807" />
+    <g
+       stroke-width="0.7"
+       id="g3817">
+      <path
+         d="M 3,2 V 30 H 29 V 2 Z m 1,6 h 24 v 4 H 4 Z m 0,11 h 24 v 4 H 4 Z"
+         fill="url(#b)"
+         id="path3809"
+         style="fill:url(#b)" />
+      <path
+         d="M 5,4 H 7 V 5 H 5 Z"
+         fill="url(#c)"
+         id="path3811"
+         style="fill:url(#c)" />
+      <path
+         d="m 5,15 h 2 v 1 H 5 Z"
+         fill="url(#d)"
+         id="path3813"
+         style="fill:url(#d)" />
+      <path
+         d="m 5,26 h 2 v 1 H 5 Z"
+         fill="url(#e)"
+         id="path3815"
+         style="fill:url(#e)" />
+    </g>
+    <path
+       d="m 3,29 h 26 v 1 H 3 Z"
+       opacity="0.2"
+       id="path3819" />
+    <path
+       d="m 4,12 7,7 h 17 v 4 H 15 l 7,7 h 7 V 13 l -1,-1 z"
+       fill="url(#f)"
+       fill-rule="evenodd"
+       opacity="0.4"
+       id="path3821"
+       style="fill:url(#f)" />
+    <rect
+       fill="#eff0f1"
+       height="16"
+       rx="2"
+       width="16"
+       x="16"
+       y="14"
+       id="rect3823" />
+    <path
+       d="m 19,16 v 2 h -1 v 2 h 1 v 4 h -1 v 2 h 1 v 2 H 30 V 16 Z m 1,1 h 1 v 10 h -1 z m 2,0 h 7 v 10 h -7 z"
+       fill="#232629"
+       id="path3825" />
+  </g>
+</svg>
Binary file src/eric7/icons/oxygen/preferences-eric-server.png has changed

eric ide

mercurial