src/eric7/RemoteServer/EricServer.py

Mon, 05 Feb 2024 11:15:47 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 05 Feb 2024 11:15:47 +0100
branch
server
changeset 10546
300487f5f517
parent 10539
4274f189ff78
child 10547
a357729cb749
permissions
-rw-r--r--

eric-ide Server
- Integrated the eric-ide server into the ViewManager and Editor file handling logic.

# -*- 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
# TODO: remove dependency on 'eric7.UI.Info'

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
        self.__registerInternalHandlers()

        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

    #######################################################################
    ## Methods for registering and unregistering handlers.
    #######################################################################

    def registerRequestHandler(self, requestCategory, handler):
        """
        Public method to register a request handler method for the given request
        category.

        @param requestCategory request category to be registered
        @type EricRequestCategory or int (>= EricRequestCategory.UserCategory)
        @param handler reference to the handler method. This handler must accept
            the parameters 'request', 'params', and 'requestUuid'
        @type function(request:str, params:dict, requestUuid:str)
        @exception ValueError raised to signal a request category collision
        """
        if requestCategory in self.__requestCategoryHandlerRegistry:
            raise ValueError(f"Request category '{requestCategory} already registered.")

        self.__requestCategoryHandlerRegistry[requestCategory] = handler

    def unregisterRequestHandler(self, requestCategory, ignoreError=False):
        """
        Public method to unregister a handler for the given request category.

        Note: This method will raise a KeyError exception in case the request
        category has not been registered and ignoreError is False (the default).

        @param requestCategory request category to be unregistered
        @type EricRequestCategory or int (>= EricRequestCategory.UserCategory)
        @param ignoreError flag indicating to ignore errors (defaults to False)
        @type bool (optional)
        """
        try:
            del self.__requestCategoryHandlerRegistry[requestCategory]
        except KeyError:
            if not ignoreError:
                raise

    def __registerInternalHandlers(self):
        """
        Private method to register request handler categories of this class.
        """
        self.registerRequestHandler(EricRequestCategory.Echo, self.__handleEchoRequest)
        self.registerRequestHandler(
            EricRequestCategory.Server,  self.__handleServerRequest
        )
        self.registerRequestHandler(EricRequestCategory.Error, None)
        # Register a None handler to indicate we are not expecting a request of the
        # 'Error' category.

    #######################################################################
    ## 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
        @type str
        @exception ValueError raised to indicate an invalid or unsupported request
            handler catehory
        """
        try:
            handler = self.__requestCategoryHandlerRegistry[category]
        except KeyError:
            if (
                category < EricRequestCategory.UserCategory
                and category in EricRequestCategory
            ):
                # 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 (?)

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