src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py

branch
server
changeset 10555
08e853c0c77b
child 10561
be23a662d709
diff -r d80184d38152 -r 08e853c0c77b src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py	Fri Feb 09 19:54:15 2024 +0100
@@ -0,0 +1,353 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the debugger request handler of the eric-ide server.
+"""
+
+import json
+import os
+import selectors
+import socket
+import subprocess
+import sys
+import types
+
+from .EricRequestCategory import EricRequestCategory
+
+class EricServerDebuggerRequestHandler:
+    """
+    Class implementing the debugger request handler of the eric-ide server.
+    """
+
+    def __init__(self, server):
+        """
+        Constructor
+
+        @param server reference to the eric-ide server object
+        @type EricServer
+        """
+        self.__server = server
+        
+        self.__requestMethodMapping = {
+            "StartClient": self.__startClient,
+            "StopClient": self.__stopClient,
+            "DebugClientCommand": self.__relayDebugClientCommand
+        }
+
+        self.__mainClientId = None
+        self.__client = None
+        self.__pendingConnections = []
+        self.__connections = {}
+
+        address = ("127.0.0.1", 0)
+        self.__socket = socket.create_server(address, family=socket.AF_INET)
+
+    def initServerSocket(self):
+        """
+        Public method to initialize the server socket listening for debug client
+        connections.
+        """
+        # listen on the debug server socket
+        self.__socket.listen()
+        self.__socket.setblocking(False)
+        print(
+            f"Listening for 'debug client' connections on"
+            f" {self.__socket.getsockname()}"
+        )
+        data = types.SimpleNamespace(
+            name="server", acceptHandler=self.__acceptDbgClientConnection
+        )
+        self.__server.getSelector().register(
+            self.__socket, selectors.EVENT_READ, data=data
+        )
+
+    def handleRequest(self, request, params, reqestUuid):
+        """
+        Public method handling the received debugger requests.
+
+        @param request request name
+        @type str
+        @param params dictionary containing the request parameters
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+        @type str
+        """
+        try:
+            result = self.__requestMethodMapping[request](params)
+            if result:
+                self.__server.sendJson(
+                    category=EricRequestCategory.Debugger,
+                    reply=request,
+                    params=result,
+                    reqestUuid=reqestUuid,
+                )
+
+        except KeyError:
+            self.__server.sendJson(
+                category=EricRequestCategory.Debugger,
+                reply="DebuggerRequestError",
+                params={"Error": f"Request type '{request}' is not supported."},
+            )
+
+    #######################################################################
+    ## DebugServer like methods.
+    #######################################################################
+
+    def __acceptDbgClientConnection(self, sock):
+        """
+        Private method to accept the connection on the listening debug server socket.
+
+        @param sock reference to the listening socket
+        @type socket.socket
+        """
+        connection, address = sock.accept()  # Should be ready to read
+        print(f"'Debug client' connection from {address[0]},  port {address[1]}")
+        connection.setblocking(False)
+        self.__pendingConnections.append(connection)
+
+        data = types.SimpleNamespace(
+            name="debug_client",
+            address=address,
+            handler=self.__serviceDbgClientConnection,
+        )
+        events = selectors.EVENT_READ
+        self.__server.getSelector().register(connection, events, data=data)
+
+    def __serviceDbgClientConnection(self, key):
+        """
+        Private method to service the debug client connection.
+        
+        @param key reference to the SelectorKey object associated with the connection
+            to be serviced
+        @type selectors.SelectorKey
+        """
+        sock = key.fileobj
+        data = self.__server.receiveJsonCommand(sock)
+
+        if data is None:
+            # socket was closed by debug client
+            self.__clientSocketDisconnected(sock)
+        elif data:
+            method = data["method"]
+            if method == "DebuggerId" and sock in self.__pendingConnections:
+                debuggerId = data['params']['debuggerId']
+                self.__connections[debuggerId] = sock
+                self.__pendingConnections.remove(sock)
+                if self.__mainClientId is None:
+                    self.__mainClientId = debuggerId
+
+            elif method == "ResponseBanner":
+                # add an indicator for the eric-ide server
+                data["params"]["platform"] += " (eric-ide Server)"
+
+            # pass on the data to the eric-ide
+            jsonStr = json.dumps(data)
+            print("Client Response:", jsonStr)
+            self.__server.sendJson(
+                category=EricRequestCategory.Debugger,
+                reply="DebugClientResponse",
+                params={"response": jsonStr},
+            )
+
+    def __clientSocketDisconnected(self, sock):
+        """
+        Private slot handling a socket disconnecting.
+
+        @param sock reference to the disconnected socket
+        @type QTcpSocket
+        """
+        self.__server.getSelector().unregister(sock)
+
+        for debuggerId in list(self.__connections):
+            if self.__connections[debuggerId] is sock:
+                del self.__connections[debuggerId]
+                self.__server.sendJson(
+                    category=EricRequestCategory.Debugger,
+                    reply="DebugClientDisconnected",
+                    params={"debugger_id": debuggerId},
+                )
+
+                if debuggerId == self.__mainClientId:
+                    self.__mainClientId = None
+
+                break
+        else:
+            if sock in self.__pendingConnections:
+                self.__pendingConnections.remove(sock)
+
+        sock.shutdown(socket.SHUT_RDWR)
+        sock.close()
+
+        if not self.__connections:
+            # no active connections anymore
+            self.__server.sendJson(
+                category=EricRequestCategory.Debugger,
+                reply="LastDebugClientExited",
+                params={},
+            )
+
+    def __serviceDbgClientStdoutStderr(self, key):
+        """
+        Private method to service the debug client stdout and stderr channels.
+        
+        @param key reference to the SelectorKey object associated with the connection
+            to be serviced
+        @type selectors.SelectorKey
+        """
+        data = key.fileobj.read()
+        if key.data.name == "debug_client_stdout":
+            # TODO: stdout handling not implemented yet
+            print("stdout:", data)
+        elif key.data.name == "debug_client_stderr":
+            # TODO: stderr handling not implemented yet
+            print("stderr:", data)
+
+    def shutdownClients(self):
+        """
+        Public method to shut down all connected clients.
+        """
+        if not self.__client:
+            # no client started yet
+            return
+
+        while self.__pendingConnections:
+            sock = self.__pendingConnections.pop()
+            commandDict = self.__prepareClientCommand("RequestShutdown", {})
+            self.__server.sendJsonCommand(commandDict, sock)
+            self.__shutdownSocket("", sock)
+
+        while self.__connections:
+            debuggerId, sock = self.__connections.popitem()
+            commandDict = self.__prepareClientCommand("RequestShutdown", {})
+            self.__server.sendJsonCommand(commandDict, sock)
+            self.__shutdownSocket(debuggerId, sock)
+
+        # reinitialize
+        self.__mainClientId = None
+        self.__client = None
+
+        # no active connections anymore
+        self.__server.sendJson(
+            category=EricRequestCategory.Debugger,
+            reply="LastDebugClientExited",
+            params={},
+        )
+
+    def __shutdownSocket(self, debuggerId, sock):
+        """
+        Private slot to shut down a socket.
+
+        @param debuggerId ID of the debugger the socket belongs to
+        @type str
+        @param sock reference to the socket
+        @type socket.socket
+        """
+        self.__server.getSelector().unregister(sock)
+        sock.shutdown(socket.SHUT_RDWR)
+        sock.close()
+
+        if debuggerId:
+            self.__server.sendJson(
+                category=EricRequestCategory.Debugger,
+                reply="DebugClientDisconnected",
+                params={"debugger_id": debuggerId},
+            )
+
+    def __prepareClientCommand(self, command, params):
+        """
+        Private method to prepare a command dictionary for the debug client.
+
+        @param command command to be sent
+        @type str
+        @param params dictionary containing the command parameters
+        @type dict
+        @return completed command dictionary to be sent to the debug client
+        @rtype dict
+        """
+        return {
+            "jsonrpc": "2.0",
+            "method": command,
+            "params": params,
+        }
+
+    #######################################################################
+    ## Individual request handler methods.
+    #######################################################################
+
+    def __startClient(self, params):
+        """
+        Private method to start a debug client process.
+
+        @param params dictionary containing the request data
+        @type dict
+        """
+        # 1. stop an already started debug client
+        if self.__client is not None:
+            self.__client.terminate()
+            self.__client = None
+
+        # 2. start a debug client
+        debugClient = os.path.abspath(
+            os.path.join(
+                os.path.dirname(__file__),
+                "..",
+                "DebugClients",
+                "Python",
+                "DebugClient.py",
+            )
+        )
+        ipaddr, port = self.__socket.getsockname()
+        args = [sys.executable, debugClient]
+        args.extend(params["arguments"])
+        args.extend([str(port), "True", ipaddr])
+
+        self.__client = subprocess.Popen(
+            args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+        )
+        # TODO: register stdin & stderr with selector
+
+    def __stopClient(self, params):
+        """
+        Private method to stop the current debug client process.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        self.shutdownClients()
+
+        return {"ok": True}
+
+    def __relayDebugClientCommand(self, params):
+        """
+        Private method to relay a debug client command to the client.
+
+        @param params dictionary containing the request data
+        @type dict
+        """
+        debuggerId = params["debugger_id"]
+        jsonStr = params["command"]
+        print(debuggerId, "->", jsonStr)
+
+        if not debuggerId and self.__mainClientId:
+            debuggerId = self.__mainClientId
+
+        try:
+            sock = self.__connections[debuggerId]
+        except KeyError:
+            print(f"Command for unknown debugger ID '{debuggerId}' received.")
+            # tell the eric-ide again, that this debugger ID is gone
+            self.__server.sendJson(
+                category=EricRequestCategory.Debugger,
+                reply="DebugClientDisconnected",
+                params={"debugger_id": debuggerId},
+            )
+            sock = (
+                self.__connections[self.__mainClientId] if self.__mainClientId else None
+            )
+        if sock:
+            self.__server.sendJsonCommand(jsonStr, sock)

eric ide

mercurial