RefactoringRope/JsonServer.py

Mon, 25 Sep 2017 20:08:59 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 25 Sep 2017 20:08:59 +0200
branch
server_client_variant
changeset 195
5d614a567be3
parent 194
5c297b473425
child 197
7046ac1bcb4b
permissions
-rw-r--r--

Continued implementing the distributed Code Assist.

# -*- coding: utf-8 -*-

# Copyright (c) 2017 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the JSON based server base class.
"""

from __future__ import unicode_literals

import json

from PyQt5.QtCore import pyqtSlot, QProcess
from PyQt5.QtNetwork import QTcpServer, QHostAddress

from E5Gui import E5MessageBox

import Preferences
import Utilities


class JsonServer(QTcpServer):
    """
    Class implementing the JSON based server base class.
    """
    def __init__(self, multiplex=False, parent=None):
        """
        Constructor
        
        @param multiplex flag indicating a multiplexing server
        @type bool
        @param parent parent object
        @type QObject
        """
        super(JsonServer, self).__init__(parent)
        
        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 started external in debugger:
        print('Refactoring server listening on: {0:d}'.format(port))
        # __IGNORE_WARNING__
    
    @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[id]
        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
        """
        return list(self.__connections.keys())
    
    @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:
            connection = self.__connections[idString]
        else:
            connection = self.__connection
        
        while connection and connection.canReadLine():
            data = connection.readLine()
            jsonLine = bytes(data).decode("utf-8", 'backslashreplace')
            
##            print("JSON Server: ", jsonLine)          ##debug
            
            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/>{0}</p>""").format(
                        str(err), Utilities.html_encode(jsonLine.strip())),
                    E5MessageBox.StandardButtons(
                        E5MessageBox.Ok))
                return
            
            method = clientDict["method"]
            params = clientDict["params"]
            
            self.handleCall(method, 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:
            connection = self.__connections[idString]
        else:
            connection = self.__connection
        
        if connection is not None:
            connection.write(cmd.encode('utf8', 'backslashreplace'))
            if flush:
                connection.flush()
    
    def startClient(self, interpreter, clientScript, clientArgs, idString=""):
        """
        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
        @return flag indicating a successful client start
        @rtype bool
        """
        if interpreter == "" or not Utilities.isinpath(interpreter):
            return False
        
        proc = QProcess()
        proc.setProcessChannelMode(QProcess.ForwardedChannels)
        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
        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:
            connection = self.__connections[idString]
        else:
            connection = self.__connection
        if connection is not None:
            connection.waitForDisconnected()
        
        if idString:
            self .__clientProcesses[idString].close()
            del self.__clientProcesses[idString]
        else:
            if self.__clientProcess is not None:
                self.__clientProcess.close()
                self.__clientProcess = None
    
    #######################################################################
    ## 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

eric ide

mercurial