diff -r 684f491a3bfc -r 3308e8349e4c src/eric7/RemoteServer/EricServer.py --- /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, + )