Mon, 12 Feb 2024 18:09:53 +0100
Made the eric-ide server a single connection server.
# -*- 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.__inStartClient = False 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"] # 1. process debug client messages before relaying 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)" # 2. 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}, ) # 3. process debug client messages after relaying if method == "ResponseExit": for sock in list(self.__connections.values()): if not self.__server.isSocketClosed(sock): self.__clientSocketDisconnected(sock) def __clientSocketDisconnected(self, sock): """ Private method handling a socket disconnecting. @param sock reference to the disconnected socket @type socket.socket """ 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.close() def __mainClientExited(self): """ Private method to handle exiting of the main debug client. """ self.__server.sendJson( category=EricRequestCategory.Debugger, reply="MainClientExited", params={"debugger_id": self.__mainClientId if self.__mainClientId else ""}, ) 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 def __shutdownSocket(self, debuggerId, sock): """ Private method 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 """ self.__inStartClient = True # 1. stop an already started debug client if self.__client is not None: # TODO: unregister stdin & stderr pass # 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 self.__inStartClient = False def __stopClient(self, params): # noqa: U100 """ 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 and "RequestBanner" in jsonStr: # modify the target for the 'RequestBanner' request debuggerId = self.__mainClientId if debuggerId == "<<all>>": # broadcast to all connected debug clients for sock in self.__connections.values(): self.__server.sendJsonCommand(jsonStr, sock) else: try: sock = self.__connections[debuggerId] self.__server.sendJsonCommand(jsonStr, sock) except KeyError: print(f"Command for unknown debugger ID '{debuggerId}' received.")