src/eric7/RemoteServer/EricServer.py

branch
server
changeset 10555
08e853c0c77b
parent 10547
a357729cb749
child 10561
be23a662d709
--- a/src/eric7/RemoteServer/EricServer.py	Wed Feb 07 15:28:08 2024 +0100
+++ b/src/eric7/RemoteServer/EricServer.py	Fri Feb 09 19:54:15 2024 +0100
@@ -9,17 +9,20 @@
 
 import io
 import json
-import select
+import selectors
 import socket
 import struct
 import sys
 import traceback
+import types
 import zlib
 
 from eric7.UI.Info import Version
 # TODO: remove dependency on 'eric7.UI.Info'
 
 from .EricRequestCategory import EricRequestCategory
+from .EricServerDebuggerRequestHandler import EricServerDebuggerRequestHandler
+from .EricServerFileSystemRequestHandler import EricServerFileSystemRequestHandler
 
 
 class EricServer:
@@ -45,15 +48,38 @@
 
         self.__connection = None
 
+        self.__selector = selectors.DefaultSelector()
+
+        # create and register the 'Debugger' request handler
+        self.__debuggerRequestHandler = EricServerDebuggerRequestHandler(self)
+        self.registerRequestHandler(
+            EricRequestCategory.Debugger,
+            self.__debuggerRequestHandler.handleRequest,
+        )
+
+        # create and register the 'File System' request handler
+        self.__fileSystemRequestHandler = EricServerFileSystemRequestHandler(self)
+        self.registerRequestHandler(
+            EricRequestCategory.FileSystem,
+            self.__fileSystemRequestHandler.handleRequest,
+        )
+
+        # TODO: 'Project' handler not implemented yet
+        # TODO: implement an 'EditorConfig' handler (?)
+
         address = ("", port)
         if socket.has_dualstack_ipv6() and useIPv6:
             self.__socket = socket.create_server(
                 address, family=socket.AF_INET6, dualstack_ipv6=True
             )
         else:
-            self.__socket = socket.create_server(
-                address, family=socket.AF_INET
-            )
+            self.__socket = socket.create_server(address, family=socket.AF_INET)
+
+    def getSelector(self):
+        """
+        Public method to get a reference to the selector object.
+        """
+        return self.__selector
 
     #######################################################################
     ## Methods for receiving requests and sending the results.
@@ -73,35 +99,109 @@
             (defaults to "", i.e. no UUID received)
         @type str
         """
-        commandDict = {
-            "jsonrpc": "2.0",
-            "category": category,
-            "reply": reply,
-            "params": params,
-            "uuid": reqestUuid,
-        }
-        data = json.dumps(commandDict).encode("utf8", "backslashreplace")
+        if self.__connection is not None:
+            commandDict = {
+                "jsonrpc": "2.0",
+                "category": category,
+                "reply": reply,
+                "params": params,
+                "uuid": reqestUuid,
+            }
+            self.sendJsonCommand(commandDict, self.__connection)
+
+    def sendJsonCommand(self, jsonCommand, sock):
+        """
+        Public method to send a JSON encoded command/response via a given socket.
+
+        @param jsonCommand dictionary containing the command data or a JSON encoded
+            command string
+        @type dict or str
+        @param sock reference to the socket to send the data to
+        @type socket.socket
+        """
+        if isinstance(jsonCommand, dict):
+            jsonCommand = json.dumps(jsonCommand)
+        data = jsonCommand.encode("utf8", "backslashreplace")
         header = struct.pack(b"!II", len(data), zlib.adler32(data) & 0xFFFFFFFF)
-        self.__connection.sendall(header)
-        self.__connection.sendall(data)
+        sock.sendall(header)
+        sock.sendall(data)
 
-    def __receiveBytes(self, length):
+    def __receiveBytes(self, length, sock):
         """
         Private method to receive the given length of bytes.
 
         @param length bytes to receive
         @type int
+        @param sock reference to the socket to receive the data from
+        @type socket.socket
         @return received bytes or None if connection closed
         @rtype bytes
         """
         data = bytearray()
         while len(data) < length:
-            newData = self.__connection.recv(length - len(data))
-            if not newData:
-                return None
+            try:
+                newData = sock.recv(length - len(data))
+                if not newData:
+                    print(str(newData))
+                    return None
+
+                data += newData
+            except OSError as err:
+                if err.errno != 11:
+                    data = None  # in case some data was received already
+                    break
+        return data
+
+    def receiveJsonCommand(self, sock):
+        """
+        Public method to receive a JSON encoded command and data.
+
+        @param sock reference to the socket to receive the data from
+        @type socket.socket
+        @return dictionary containing the JSON command data or None to signal
+            an issue while receiving data
+        @rtype dict
+        """
+        if self.isSocketClosed(sock):
+            return None
+
+        header = self.__receiveBytes(struct.calcsize(b"!II"), sock)
+        if not header:
+            return {}
+
+        length, datahash = struct.unpack(b"!II", header)
 
-            data += newData
-        return data
+        length = int(length)
+        data = self.__receiveBytes(length, sock)
+        if data is None:
+            return None
+
+        if not data or zlib.adler32(data) & 0xFFFFFFFF != datahash:
+            self.sendJson(
+                category=EricRequestCategory.Error,
+                reply="EricServerChecksumException",
+                params={
+                    "ExceptionType": "ProtocolChecksumError",
+                    "ExceptionValue": "The checksum of the data does not match.",
+                    "ProtocolData": data.decode("utf8", "backslashreplace"),
+                },
+            )
+            return {}
+
+        jsonStr = data.decode("utf8", "backslashreplace")
+        try:
+            return json.loads(jsonStr.strip())
+        except (TypeError, ValueError) as err:
+            self.sendJson(
+                category=EricRequestCategory.Error,
+                reply="EricServerException",
+                params={
+                    "ExceptionType": "ProtocolError",
+                    "ExceptionValue": str(err),
+                    "ProtocolData": jsonStr.strip(),
+                },
+            )
+            return {}
 
     def __receiveJson(self):
         """
@@ -113,41 +213,9 @@
             request
         @rtype tuple of (int, str, dict, str)
         """
-        # step 1: receive the data
-        header = self.__receiveBytes(struct.calcsize(b"!II"))
-        if not header:
-            return EricRequestCategory.Error, None, None, None
-
-        length, datahash = struct.unpack(b"!II", header)
+        requestDict = self.receiveJsonCommand(self.__connection)
 
-        length = int(length)
-        data = self.__receiveBytes(length)
-        if not data or zlib.adler32(data) & 0xFFFFFFFF != datahash:
-            self.sendJson(
-                category=EricRequestCategory.Error,
-                reply="ClientChecksumException",
-                params={
-                    "ExceptionType": "ProtocolChecksumError",
-                    "ExceptionValue": "The checksum of the data does not match.",
-                    "ProtocolData": data.decode("utf8", "backslashreplace"),
-                },
-            )
-            return EricRequestCategory.Error, None, None, None
-
-        # step 2: decode and convert the data
-        jsonString = data.decode("utf8", "backslashreplace")
-        try:
-            requestDict = json.loads(jsonString.strip())
-        except (TypeError, ValueError) as err:
-            self.sendJson(
-                category=EricRequestCategory.Error,
-                reply="ClientException",
-                params={
-                    "ExceptionType": "ProtocolError",
-                    "ExceptionValue": str(err),
-                    "ProtocolData": jsonString.strip(),
-                },
-            )
+        if not requestDict:
             return EricRequestCategory.Error, None, None, None
 
         category = requestDict["category"]
@@ -157,6 +225,28 @@
 
         return category, request, params, reqestUuid
 
+    def isSocketClosed(self, sock):
+        """
+        Public method to check, if a given socket is closed.
+
+        @param sock reference to the socket to be checked
+        @type socket.socket
+        @return flag indicating a closed state
+        @rtype bool
+        """
+        try:
+            # this will try to read bytes without removing them from buffer (peek only)
+            data = sock.recv(16, socket.MSG_PEEK)
+            if len(data) == 0:
+                return True
+        except BlockingIOError:
+            return False  # socket is open and reading from it would block
+        except ConnectionError:
+            return True  # socket was closed for some other reason
+        except Exception:
+            return False
+        return False
+
     #######################################################################
     ## Methods for the server main loop.
     #######################################################################
@@ -165,9 +255,66 @@
         """
         Private method to shut down the server.
         """
+        self.__closeIdeConnection()
+
+        print("Stop listening for 'eric-ide' connections.")
         self.__socket.shutdown(socket.SHUT_RDWR)
         self.__socket.close()
 
+        self.__selector.close()
+
+    def __acceptIdeConnection(self, sock):
+        """
+        Private method to accept the connection on the listening IDE server socket.
+
+        @param sock reference to the listening socket
+        @type socket.socket
+        """
+        self.__connection, address = sock.accept()  # Should be ready to read
+        print(f"'eric-ide' connection from {address[0]},  port {address[1]}")
+        self.__connection.setblocking(False)
+        data = types.SimpleNamespace(
+            name="eric-ide", address=address, handler=self.__serviceIdeConnection
+        )
+        events = selectors.EVENT_READ
+        self.__selector.register(self.__connection, events, data=data)
+
+    def __closeIdeConnection(self):
+        """
+        Private method to close the connection to an eric-ide.
+        """
+        if self.__connection is not None:
+            print(
+                f"Closing 'eric-ide' connection to {self.__connection.getpeername()}."
+            )
+            self.__selector.unregister(self.__connection)
+            self.__connection.shutdown(socket.SHUT_RDWR)
+            self.__connection.close()
+            self.__connection = None
+
+            self.__debuggerRequestHandler.shutdownClients()
+
+    def __serviceIdeConnection(self, key):
+        """
+        Private method to service the eric-ide connection.
+        
+        @param key reference to the SelectorKey object associated with the connection
+            to be serviced
+        @type selectors.SelectorKey
+        """
+        if key.data.name == "eric-ide":
+            category, request, params, reqestUuid = self.__receiveJson()
+            if category == EricRequestCategory.Error or request is None:
+                self.__closeIdeConnection()
+                return
+
+            if category == EricRequestCategory.Server:
+                if request.lower() == "shutdown":
+                    self.__shouldStop = True
+                    return
+
+            self.__handleRequest(category, request, params, reqestUuid)
+
     def run(self):
         """
         Public method implementing the remote server main loop.
@@ -182,51 +329,33 @@
         @return flag indicating a clean shutdown
         @rtype bool
         """
-        shutdown = False
         cleanExit = True
+        self.__shouldStop = False
 
         # listen on the server socket for new connections
         self.__socket.listen(1)
+        self.__socket.setblocking(False)
+        print(f"Listening for 'eric-ide' connections on {self.__socket.getsockname()}")
+        data = types.SimpleNamespace(
+            name="server", acceptHandler=self.__acceptIdeConnection
+        )
+        self.__selector.register(self.__socket, selectors.EVENT_READ, data=data)
+
+        self.__debuggerRequestHandler.initServerSocket()
 
         while True:
             try:
-                # accept the next pending connection
-                print("Waiting for connection...")
-                self.__connection, address = self.__socket.accept()
-                print(f"Connection from {address[0]},  port {address[1]}")
-
-                selectErrors = 0
-                while selectErrors <= 10:  # selected arbitrarily
-                    try:
-                        rrdy, wrdy, xrdy = select.select([self.__connection], [], [])
-
-                        # Just waiting for self.__connection. Therefore no check
-                        # needed.
-                        category, request, params, reqestUuid = self.__receiveJson()
-                        if category == EricRequestCategory.Error or request is None:
-                            selectErrors += 1
-                        elif category == EricRequestCategory.Server:
-                            if request.lower() == "exit":
-                                break
-                            elif request.lower() == "shutdown":
-                                shutdown = True
-                                break
-                            else:
-                                self.__handleRequest(
-                                    category, request, params, reqestUuid
-                                )
-                        else:
-                            self.__handleRequest(category, request, params, reqestUuid)
-
-                            # reset select errors
-                            selectErrors = 0
-
-                    except (select.error, socket.error):
-                        selectErrors += 1
+                events = self.__selector.select(timeout=None)
+                for key, mask in events:
+                    if key.data.name == "server":
+                        # it is an event for a server socket
+                        key.data.acceptHandler(key.fileobj)
+                    else:
+                        key.data.handler(key)
 
             except KeyboardInterrupt:
                 # intercept user pressing Ctrl+C
-                shutdown = True
+                self.__shouldStop = True
 
             except Exception:
                 exctype, excval, exctb = sys.exc_info()
@@ -237,15 +366,10 @@
                 
                 print(f"{str(exctype)} / {str(excval)} / {tbinfo}")
 
-                shutdown = True
+                self.__shouldStop = True
                 cleanExit = False
 
-            if self.__connection is not None:
-                self.__connection.shutdown(socket.SHUT_RDWR)
-                self.__connection.close()
-                self.__connection = None
-
-            if shutdown:
+            if self.__shouldStop:
                 # exit the outer loop and shut down the server
                 self.__shutdown()
                 break
@@ -324,36 +448,8 @@
         """
         try:
             handler = self.__requestCategoryHandlerRegistry[category]
+            handler(request=request, params=params, reqestUuid=reqestUuid)
         except KeyError:
-            if category < EricRequestCategory.UserCategory:
-                # it is an internally supported category
-                if category == EricRequestCategory.FileSystem:
-                    from .EricServerFileSystemRequestHandler import (
-                        EricServerFileSystemRequestHandler,
-                    )
-                    handler = EricServerFileSystemRequestHandler(self).handleRequest
-
-                elif category == EricRequestCategory.Project:
-                    # TODO: 'Project' handler not implemented yet
-                    handler = None
-
-                elif category == EricRequestCategory.Debugger:
-                    # TODO: 'Debugger' handler not implemented yet
-                    handler = None
-
-                # TODO: implement an 'EditorConfig' handler (?)
-
-                else:
-                    # That internal category does not exist.
-                    handler = None
-
-                self.registerRequestHandler(category, handler)
-            else:
-                handler = None
-
-        if handler is not None:
-            handler(request=request, params=params, reqestUuid=reqestUuid)
-        else:
             self.sendJson(
                 category=EricRequestCategory.Error,
                 reply="UnsupportedServiceCategory",

eric ide

mercurial