--- 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",