--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/E5Network/E5JsonServer.py Sun May 09 14:53:27 2021 +0200 @@ -0,0 +1,313 @@ +# -*- 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 PyQt5.QtCore import ( + pyqtSlot, QProcess, QProcessEnvironment, QCoreApplication, QEventLoop, + QTimer +) +from PyQt5.QtNetwork import QTcpServer, QHostAddress + +from E5Gui import E5MessageBox + +import Preferences +import Utilities + + +class E5JsonServer(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: + E5MessageBox.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())), + E5MessageBox.StandardButtons( + E5MessageBox.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 + @rtype bool + """ + if interpreter == "" or not Utilities.isinpath(interpreter): + return False + + 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) + else: + self.__clientProcess = proc + + return proc is not None + + 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): + 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