Sat, 22 May 2021 18:51:46 +0200
Renamed the modules and classes of the E5Gui package to have the prefix 'Eric' instead of 'E5'.
# -*- coding: utf-8 -*- # Copyright (c) 2017 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the JSON based server base class. """ import contextlib import json from PyQt6.QtCore import ( pyqtSlot, QProcess, QProcessEnvironment, QCoreApplication, QEventLoop, QTimer ) from PyQt6.QtNetwork import QTcpServer, QHostAddress from E5Gui import EricMessageBox import Preferences import Utilities class EricJsonServer(QTcpServer): """ Class implementing a JSON based server base class. """ def __init__(self, name="", multiplex=False, parent=None): """ Constructor @param name name of the server (used for output only) @type str @param multiplex flag indicating a multiplexing server @type bool @param parent parent object @type QObject """ super().__init__(parent) self.__name = name self.__multiplex = multiplex if self.__multiplex: self.__clientProcesses = {} self.__connections = {} else: self.__clientProcess = None self.__connection = None # setup the network interface networkInterface = Preferences.getDebugger("NetworkInterface") if networkInterface == "all" or '.' in networkInterface: # IPv4 self.__hostAddress = '127.0.0.1' else: # IPv6 self.__hostAddress = '::1' self.listen(QHostAddress(self.__hostAddress)) self.newConnection.connect(self.handleNewConnection) port = self.serverPort() ## Note: Need the port if client is started external in debugger. print('JSON server ({1}) listening on: {0:d}' # __IGNORE_WARNING__ .format(port, self.__name)) @pyqtSlot() def handleNewConnection(self): """ Public slot for new incoming connections from a client. """ connection = self.nextPendingConnection() if not connection.isValid(): return if self.__multiplex: if not connection.waitForReadyRead(3000): return idString = bytes(connection.readLine()).decode( "utf-8", 'backslashreplace').strip() if idString in self.__connections: self.__connections[idString].close() self.__connections[idString] = connection else: idString = "" if self.__connection is not None: self.__connection.close() self.__connection = connection connection.readyRead.connect( lambda: self.__receiveJson(idString)) connection.disconnected.connect( lambda: self.__handleDisconnect(idString)) @pyqtSlot() def __handleDisconnect(self, idString): """ Private slot handling a disconnect of the client. @param idString id of the connection been disconnected @type str """ if idString: if idString in self.__connections: self.__connections[idString].close() del self.__connections[idString] else: if self.__connection is not None: self.__connection.close() self.__connection = None def connectionNames(self): """ Public method to get the list of active connection names. If this is not a multiplexing server, an empty list is returned. @return list of active connection names @rtype list of str """ if self.__multiplex: return list(self.__connections.keys()) else: return [] @pyqtSlot() def __receiveJson(self, idString): """ Private slot handling received data from the client. @param idString id of the connection been disconnected @type str """ if idString: try: connection = self.__connections[idString] except KeyError: connection = None else: connection = self.__connection while connection and connection.canReadLine(): data = connection.readLine() jsonLine = bytes(data).decode("utf-8", 'backslashreplace') #- print("JSON Server ({0}): {1}".format(self.__name, jsonLine)) #- this is for debugging only try: clientDict = json.loads(jsonLine.strip()) except (TypeError, ValueError) as err: EricMessageBox.critical( None, self.tr("JSON Protocol Error"), self.tr("""<p>The response received from the client""" """ could not be decoded. Please report""" """ this issue with the received data to the""" """ eric bugs email address.</p>""" """<p>Error: {0}</p>""" """<p>Data:<br/>{1}</p>""").format( str(err), Utilities.html_encode(jsonLine.strip())), EricMessageBox.Ok) return self.handleCall(clientDict["method"], clientDict["params"]) def sendJson(self, command, params, flush=False, idString=""): """ Public method to send a single command to a client. @param command command name to be sent @type str @param params dictionary of named parameters for the command @type dict @param flush flag indicating to flush the data to the socket @type bool @param idString id of the connection to send data to @type str """ commandDict = { "jsonrpc": "2.0", "method": command, "params": params, } cmd = json.dumps(commandDict) + '\n' if idString: try: connection = self.__connections[idString] except KeyError: connection = None else: connection = self.__connection if connection is not None: data = cmd.encode('utf8', 'backslashreplace') length = "{0:09d}".format(len(data)) connection.write(length.encode() + data) if flush: connection.flush() def startClient(self, interpreter, clientScript, clientArgs, idString="", environment=None): """ Public method to start a client process. @param interpreter interpreter to be used for the client @type str @param clientScript path to the client script @type str @param clientArgs list of arguments for the client @param idString id of the client to be started @type str @param environment dictionary of environment settings to pass @type dict @return flag indicating a successful client start and the exit code in case of an issue @rtype bool, int """ if interpreter == "" or not Utilities.isinpath(interpreter): return False exitCode = None proc = QProcess() proc.setProcessChannelMode( QProcess.ProcessChannelMode.ForwardedChannels) if environment is not None: env = QProcessEnvironment() for key, value in list(environment.items()): env.insert(key, value) proc.setProcessEnvironment(env) args = [clientScript, self.__hostAddress, str(self.serverPort())] if idString: args.append(idString) args.extend(clientArgs) proc.start(interpreter, args) if not proc.waitForStarted(10000): proc = None if idString: self.__clientProcesses[idString] = proc if proc: timer = QTimer() timer.setSingleShot(True) timer.start(30000) # 30s timeout while ( idString not in self.connectionNames() and timer.isActive() ): # Give the event loop the chance to process the new # connection of the client (= slow start). QCoreApplication.processEvents( QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents) # check if client exited prematurely if proc.state() == QProcess.ProcessState.NotRunning: exitCode = proc.exitCode() proc = None self.__clientProcesses[idString] = None break else: if proc: timer = QTimer() timer.setSingleShot(True) timer.start(1000) # 1s timeout while timer.isActive(): # check if client exited prematurely QCoreApplication.processEvents( QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents) if proc.state() == QProcess.ProcessState.NotRunning: exitCode = proc.exitCode() proc = None break self.__clientProcess = proc return proc is not None, exitCode def stopClient(self, idString=""): """ Public method to stop a client process. @param idString id of the client to be stopped @type str """ self.sendJson("Exit", {}, flush=True, idString=idString) if idString: try: connection = self.__connections[idString] except KeyError: connection = None else: connection = self.__connection if connection is not None: connection.waitForDisconnected() if idString: with contextlib.suppress(KeyError): if self .__clientProcesses[idString] is not None: self .__clientProcesses[idString].close() del self.__clientProcesses[idString] else: if self.__clientProcess is not None: self.__clientProcess.close() self.__clientProcess = None def stopAllClients(self): """ Public method to stop all clients. """ clientNames = self.connectionNames()[:] for clientName in clientNames: self.stopClient(clientName) ####################################################################### ## The following methods should be overridden by derived classes ####################################################################### def handleCall(self, method, params): """ Public method to handle a method call from the client. Note: This is an empty implementation that must be overridden in derived classes. @param method requested method name @type str @param params dictionary with method specific parameters @type dict """ pass