RefactoringRope/Refactoring.py

branch
server_client_variant
changeset 160
989cd767992b
parent 151
5260100b6700
child 162
55eaaed9d590
--- a/RefactoringRope/Refactoring.py	Mon Sep 11 13:34:20 2017 +0200
+++ b/RefactoringRope/Refactoring.py	Mon Sep 11 18:51:54 2017 +0200
@@ -24,18 +24,21 @@
 import rope.base.libutils
 import rope.base.exceptions
 
-from PyQt5.QtCore import QObject
+from PyQt5.QtCore import pyqtSlot, QProcess
 from PyQt5.QtWidgets import QMenu, QApplication, QDialog, QAction
 from PyQt5.Qsci import QsciScintilla
+from PyQt5.QtNetwork import QTcpServer, QHostAddress
 
 from E5Gui.E5Application import e5App
 from E5Gui import E5MessageBox
 from E5Gui.E5Action import E5Action
 
 import Utilities
+import Preferences
 
 
-class Refactoring(QObject):
+# TODO: rename this (and the module) to RefactoringServer once done
+class Refactoring(QTcpServer):
     """
     Class implementing the refactoring interface to rope.
     """
@@ -46,7 +49,7 @@
         @param plugin reference to the plugin object
         @param parent parent (QObject)
         """
-        QObject.__init__(self, parent)
+        super(Refactoring, self).__init__(parent)
         
         self.__plugin = plugin
         self.__ui = parent
@@ -58,11 +61,33 @@
         self.__mainMenu = None
         self.__helpDialog = None
         
+        self.__refactoringProcess = None
+        self.__refactoringConnection = None
+        
         # Rope objects
+        # TODO: move this to RefactoringClient
         self.__project = None
         
+        # TODO: split this between RefactoringClient and this server
         from FileSystemCommands import E5FileSystemCommands
         self.__fsCommands = E5FileSystemCommands(self.__e5project)
+        
+        # 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__
     
     def initActions(self):
         """
@@ -2211,11 +2236,33 @@
         self.__projectLanguage = self.__e5project.getProjectLanguage()
         
         if self.__projectLanguage.startswith("Python"):
-            import rope.base.project
-            self.__project = rope.base.project.Project(
-                self.__projectpath, fscommands=self.__fsCommands)
-            for act in self.actions:
-                act.setEnabled(True)
+            if self.__projectLanguage == "Python2":
+                interpreter = Preferences.getDebugger("PythonInterpreter")
+            elif self.__projectLanguage == "Python3":
+                interpreter = Preferences.getDebugger("Python3Interpreter")
+            else:
+                interpreter = ""
+            if interpreter:
+                process = self.__startRefactoringClient(interpreter)
+                if process is None:
+                    self.__ui.appendToStderr(self.tr(
+                        "Project language '{0}' is not supported because"
+                        " the configured interpreter could not be started."
+                        " Refactoring is disabled."
+                    ).format(self.__projectLanguage))
+                else:
+                    self.__refactoringProcess = process
+##            import rope.base.project
+##            self.__project = rope.base.project.Project(
+##                self.__projectpath, fscommands=self.__fsCommands)
+                    for act in self.actions:
+                        act.setEnabled(True)
+            else:
+                self.__ui.appendToStderr(self.tr(
+                    "Project language '{0}' is not supported because no"
+                    " suitable interpreter is configured. Refactoring is"
+                    " disabled."
+                ).format(self.__projectLanguage))
     
     def projectClosed(self):
         """
@@ -2224,14 +2271,16 @@
         for act in self.actions:
             act.setEnabled(False)
         
-        if self.__project is not None:
-            self.__project.close()
-            self.__project = None
+        self.__stopRefactoringClient()
+##        if self.__project is not None:
+##            self.__project.close()
+##            self.__project = None
         
         self.__projectopen = False
         self.__projectpath = ''
         self.__projectLanguage = ""
     
+    # TODO: delete this or move to client
     def getProject(self):
         """
         Public method to get a reference to the rope project object.
@@ -2306,3 +2355,175 @@
                 # this could come from trying to do PyQt4/PyQt5 mixed stuff
                 # simply ignore it
                 pass
+    
+    #######################################################################
+    ## Methods below handle the network connection
+    #######################################################################
+    
+    @pyqtSlot()
+    def __handleNewConnection(self):
+        """
+        Private slot for new incomming connections from the refactoring client.
+        """
+        if self.__refactoringConnection is not None:
+            self.__refactoringConnection.close()
+            self.__refactoringConnection = None
+        
+        connection = self.nextPendingConnection()
+        if not connection.isValid():
+            return
+        
+        self.__refactoringConnection = connection
+        connection.readyRead.connect(self.__receiveJson)
+        connection.disconnected.connect(self.__handleDisconnect)
+        
+        self.__sendJson("ping", {})
+    
+    @pyqtSlot()
+    def __handleDisconnect(self):
+        """
+        Private slot handling a disconnect of the refactoring client.
+        """
+        if self.__refactoringConnection is not None:
+            self.__refactoringConnection.close()
+        
+        self.__refactoringConnection = None
+    
+    @pyqtSlot()
+    def __receiveJson(self):
+        """
+        Private slot handling received data from the refactoring client.
+        """
+        while self.__refactoringConnection and \
+                self.__refactoringConnection.canReadLine():
+            data = self.__refactoringConnection.readLine()
+            jsonLine = bytes(data).decode()
+            
+            print("Refactoring Server: ", jsonLine)          ##debug
+            
+            self.__processJson(jsonLine)
+            continue
+    
+    def __processJson(self, jsonStr):
+        """
+        Private method to process the JSON serialized client data.
+        
+        @param jsonStr string containing the data structure received
+            from the refactoring client
+        @type str
+        """
+        import json
+        
+        try:
+            clientDict = json.loads(jsonStr.strip())
+        except (TypeError, ValueError) as err:
+            E5MessageBox.critical(
+                None,
+                self.tr("Refactoring Protocol Error"),
+                self.tr("""<p>The response received from the refactoring"""
+                        """ 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(jsonStr.strip())),
+                E5MessageBox.StandardButtons(
+                    E5MessageBox.Ok))
+            return
+        
+        method = clientDict["method"]
+        params = clientDict["params"]
+        
+        print("Method:", method)
+        print("Params:", params)
+        
+        if method == "pong":
+            pass
+        
+        elif method == "ClientException":
+            if params["ExceptionType"] == "ProtocolError":
+                E5MessageBox.critical(
+                    None,
+                    self.tr("Refactoring Protocol Error"),
+                    self.tr("""<p>The data received from the refactoring"""
+                            """ server 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(
+                        params["ExceptionValue"],
+                        Utilities.html_encode(params["ProtocolData"])),
+                    E5MessageBox.StandardButtons(
+                        E5MessageBox.Ok))
+            else:
+                E5MessageBox.critical(
+                    None,
+                    self.tr("Refactoring Client Error"),
+                    self.tr("<p>An exception happened in the refactoring"
+                            " client. Please report it to the eric bugs"
+                            " email address.</p>"
+                            "<p>Exception: {0}</p>"
+                            "<p>Value: {1}</p>"
+                            "Traceback: {2}</p>").format(
+                        Utilities.html_encode(params["ExceptionType"]),
+                        params["ExceptionValue"],
+                        params["Traceback"].replace("\r\n", "<br/>")
+                        .replace("\n", "<br/>").replace("\r", "<br/>"),
+                    ),
+                    E5MessageBox.StandardButtons(
+                        E5MessageBox.Ok))
+            return
+    
+    def __sendJson(self, command, params):
+        """
+        Private method to send a single refactoring command to the client.
+        
+        @param command command name to be sent
+        @type str
+        @param params dictionary of named parameters for the command
+        @type dict
+        """
+        import json
+        
+        commandDict = {
+            "jsonrpc": "2.0",
+            "method": command,
+            "params": params,
+        }
+        cmd = json.dumps(commandDict) + '\n'
+        if self.__refactoringConnection is not None:
+            self.__refactoringConnection.write(
+                cmd.encode('utf8', 'backslashreplace'))
+    
+    def __startRefactoringClient(self, interpreter):
+        """
+        Private method to start the refactoring client.
+        
+        @param interpreter interpreter to be used for the refactoring client
+        @type str
+        @return reference to the refactoring client process
+        """
+        if interpreter == "" or not Utilities.isinpath(interpreter):
+            return None
+        
+        client = os.path.join(os.path.dirname(__file__),
+                              "RefactoringClient.py")
+        proc = QProcess()
+        proc.setProcessChannelMode(QProcess.ForwardedChannels)
+        args = [client, self.__hostAddress, str(self.serverPort()),
+                self.__projectpath]
+        proc.start(interpreter, args)
+        if not proc.waitForStarted(10000):
+            proc = None
+        
+        return proc
+    
+    def __stopRefactoringClient(self):
+        """
+        Private method to stop the refactoring client process.
+        """
+        self.__refactoringProcess.close()
+        self.__refactoringProcess = None
+
+#
+# eflag: noqa = M801

eric ide

mercurial