--- /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)