src/eric7/RemoteServer/EricServer.py

branch
server
changeset 10531
3308e8349e4c
child 10539
4274f189ff78
--- /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,
+            )

eric ide

mercurial