src/eric7/Debugger/DebuggerInterfacePython.py

branch
eric7-maintenance
changeset 10814
ba20efe10336
parent 10733
d96c69a235fc
parent 10766
d35d6f96c24b
child 10941
07cad049002c
--- a/src/eric7/Debugger/DebuggerInterfacePython.py	Sun Jun 02 09:51:47 2024 +0200
+++ b/src/eric7/Debugger/DebuggerInterfacePython.py	Wed Jul 03 09:20:41 2024 +0200
@@ -16,7 +16,7 @@
 import time
 import zlib
 
-from PyQt6.QtCore import QObject, QProcess, QProcessEnvironment, QTimer
+from PyQt6.QtCore import QObject, QProcess, QProcessEnvironment, QTimer, pyqtSlot
 
 from eric7 import Preferences, Utilities
 from eric7.EricWidgets import EricMessageBox
@@ -51,6 +51,23 @@
         self.__autoContinued = []
         self.__isStepCommand = False
 
+        self.__ericServerDebugging = False  # are we debugging via the eric-ide server?
+        try:
+            self.__ericServerDebuggerInterface = (
+                ericApp().getObject("EricServer").getServiceInterface("Debugger")
+            )
+            self.__ericServerDebuggerInterface.debugClientResponse.connect(
+                lambda jsonStr: self.handleJsonCommand(jsonStr, None)
+            )
+            self.__ericServerDebuggerInterface.debugClientDisconnected.connect(
+                self.__handleServerDebugClientDisconnected
+            )
+            self.__ericServerDebuggerInterface.lastClientExited.connect(
+                self.__handleServerLastClientExited
+            )
+        except KeyError:
+            self.__ericServerDebuggerInterface = None
+
         self.debugServer = debugServer
         self.passive = passive
         self.process = None
@@ -89,8 +106,9 @@
         @param fn filename to be translated
         @type str
         @param remote2local flag indicating the direction of translation
-            (False = local to remote, True = remote to local [default]) (unused)
-        @type bool
+            (False = local to remote, True = remote to local) (defaults to True)
+            (unused)
+        @type bool (optional)
         @return translated filename
         @rtype str
         """
@@ -103,8 +121,8 @@
         @param fn filename to be translated
         @type str
         @param remote2local flag indicating the direction of translation
-            (False = local to remote, True = remote to local [default])
-        @type bool
+            (False = local to remote, True = remote to local) (defaults to True)
+        @type bool (optional)
         @return translated filename
         @rtype str
         """
@@ -119,7 +137,24 @@
 
         return path
 
-    def __startProcess(self, program, arguments, environment=None, workingDir=None):
+    def __ericServerTranslation(self, fn, remote2local=True):
+        """
+        Private method to perform the eric-ide server path translation.
+
+        @param fn filename to be translated
+        @type str
+        @param remote2local flag indicating the direction of translation
+            (False = local to remote, True = remote to local) (defaults to True)
+        @type bool (optional)
+        @return translated filename
+        @rtype str
+        """
+        if remote2local:
+            return FileSystemUtilities.remoteFileName(fn)
+        else:
+            return FileSystemUtilities.plainFileName(fn)
+
+    def __startProcess(self, program, arguments, environment=None, workingDir=""):
         """
         Private method to start the debugger client process.
 
@@ -155,8 +190,9 @@
         runInConsole,
         venvName,
         originalPathString,
-        workingDir=None,
+        workingDir="",
         configOverride=None,
+        startRemote=None,
     ):
         """
         Public method to start a remote Python interpreter.
@@ -170,41 +206,65 @@
         @type str
         @param originalPathString original PATH environment variable
         @type str
-        @param workingDir directory to start the debugger client in
-        @type str
-        @param configOverride dictionary containing the global config override
-            data
-        @type dict
+        @param workingDir directory to start the debugger client in (defaults to "")
+        @type str (optional)
+        @param configOverride dictionary containing the global config override data
+            (defaults to None)
+        @type dict (optional)
+        @param startRemote flag indicating to start the client via an eric-ide server
+            (defaults to None)
+        @type bool (optional)
         @return client process object, a flag to indicate a network connection
             and the name of the interpreter in case of a local execution
         @rtype tuple of (QProcess, bool, str)
         """
         global origPathEnv
 
-        if not venvName:
-            venvName = Preferences.getDebugger("Python3VirtualEnv")
-        if venvName == self.debugServer.getProjectEnvironmentString():
-            project = ericApp().getObject("Project")
-            venvName = project.getProjectVenv()
-            execPath = project.getProjectExecPath()
-            interpreter = project.getProjectInterpreter()
+        if (
+            startRemote is True
+            or (
+                startRemote is None
+                and (
+                    venvName == self.debugServer.getEricServerEnvironmentString()
+                    or self.__ericServerDebugging
+                )
+            )
+        ) and ericApp().getObject("EricServer").isServerConnected():
+            # TODO change this once server environment definitions are supported
+            startRemote = True
+            if venvName:
+                venvManager = ericApp().getObject("VirtualEnvManager")
+                interpreter = venvManager.getVirtualenvInterpreter(venvName)
+            else:
+                venvName = self.debugServer.getEricServerEnvironmentString()
+                interpreter = ""  # use the interpreter of the server
         else:
-            venvManager = ericApp().getObject("VirtualEnvManager")
-            interpreter = venvManager.getVirtualenvInterpreter(venvName)
-            execPath = venvManager.getVirtualenvExecPath(venvName)
-        if interpreter == "":
-            # use the interpreter used to run eric for identical variants
-            interpreter = PythonUtilities.getPythonExecutable()
-        if interpreter == "":
-            EricMessageBox.critical(
-                None,
-                self.tr("Start Debugger"),
-                self.tr("""<p>No suitable Python3 environment configured.</p>"""),
-            )
-            return None, False, ""
+            if not venvName:
+                venvName = Preferences.getDebugger("Python3VirtualEnv")
+            if venvName == self.debugServer.getProjectEnvironmentString():
+                project = ericApp().getObject("Project")
+                venvName = project.getProjectVenv()
+                execPath = project.getProjectExecPath()
+                interpreter = project.getProjectInterpreter()
+            else:
+                venvManager = ericApp().getObject("VirtualEnvManager")
+                interpreter = venvManager.getVirtualenvInterpreter(venvName)
+                execPath = venvManager.getVirtualenvExecPath(venvName)
+            if interpreter == "":
+                # use the interpreter used to run eric for identical variants
+                interpreter = PythonUtilities.getPythonExecutable()
+            if interpreter == "":
+                EricMessageBox.critical(
+                    None,
+                    self.tr("Start Debugger"),
+                    self.tr("""<p>No suitable Python3 environment configured.</p>"""),
+                )
+                return None, False, ""
 
         self.__inShutdown = False
 
+        self.__ericServerDebugging = False
+
         redirect = (
             str(configOverride["redirect"])
             if configOverride and configOverride["enable"]
@@ -291,6 +351,28 @@
                 )
                 return None, False, ""
 
+        elif startRemote and self.__ericServerDebuggerInterface is not None:
+            # debugging via an eric-ide server
+            self.translate = self.__ericServerTranslation
+            self.__ericServerDebugging = True
+
+            args = []
+            if noencoding:
+                args.append(noencoding)
+            if multiprocessEnabled:
+                args.append(multiprocessEnabled)
+            if callTraceOptimization:
+                args.append(callTraceOptimization)
+            self.__ericServerDebuggerInterface.startClient(
+                interpreter,
+                originalPathString,
+                args,
+                workingDir=workingDir,
+            )
+            self.__startedVenv = venvName
+
+            return None, self.__isNetworked, ""
+
         else:
             # local debugging code below
             debugClient = self.__determineDebugClient()
@@ -399,6 +481,7 @@
         originalPathString,
         workingDir=None,
         configOverride=None,
+        startRemote=False,
     ):
         """
         Public method to start a remote Python interpreter for a project.
@@ -417,6 +500,9 @@
         @param configOverride dictionary containing the global config override
             data
         @type dict
+        @param startRemote flag indicating to start the client via an eric-ide server
+            (defaults to False)
+        @type bool (optional)
         @return client process object, a flag to indicate a network connection
             and the name of the interpreter in case of a local execution
         @rtype tuple of (QProcess, bool, str)
@@ -443,24 +529,48 @@
             else ""
         )
 
-        if venvName and venvName != self.debugServer.getProjectEnvironmentString():
-            venvManager = ericApp().getObject("VirtualEnvManager")
-            interpreter = venvManager.getVirtualenvInterpreter(venvName)
-            execPath = venvManager.getVirtualenvExecPath(venvName)
+        if (
+            startRemote is True
+            or (
+                startRemote is None
+                and (
+                    venvName == self.debugServer.getEricServerEnvironmentString()
+                    or self.__ericServerDebugging
+                )
+            )
+        ) and ericApp().getObject("EricServer").isServerConnected():
+            # TODO change this once server environment definitions are supported
+            startRemote = True
+            if venvName and venvName != self.debugServer.getProjectEnvironmentString():
+                venvManager = ericApp().getObject("VirtualEnvManager")
+                interpreter = venvManager.getVirtualenvInterpreter(venvName)
+            else:
+                venvName = project.getProjectVenv()
+                interpreter = project.getProjectInterpreter()
+            if not venvName:
+                venvName = self.debugServer.getEricServerEnvironmentString()
+                interpreter = ""  # use the interpreter of the server
         else:
-            venvName = project.getProjectVenv()
-            execPath = project.getProjectExecPath()
-            interpreter = project.getProjectInterpreter()
-        if interpreter == "":
-            EricMessageBox.critical(
-                None,
-                self.tr("Start Debugger"),
-                self.tr("""<p>No suitable Python3 environment configured.</p>"""),
-            )
-            return None, self.__isNetworked, ""
+            if venvName and venvName != self.debugServer.getProjectEnvironmentString():
+                venvManager = ericApp().getObject("VirtualEnvManager")
+                interpreter = venvManager.getVirtualenvInterpreter(venvName)
+                execPath = venvManager.getVirtualenvExecPath(venvName)
+            else:
+                venvName = project.getProjectVenv()
+                execPath = project.getProjectExecPath()
+                interpreter = project.getProjectInterpreter()
+            if interpreter == "":
+                EricMessageBox.critical(
+                    None,
+                    self.tr("Start Debugger"),
+                    self.tr("""<p>No suitable Python3 environment configured.</p>"""),
+                )
+                return None, self.__isNetworked, ""
 
         self.__inShutdown = False
 
+        self.__ericServerDebugging = False
+
         if project.getDebugProperty("REMOTEDEBUGGER"):
             # remote debugging code
             ipaddr = self.debugServer.getHostAddress(False)
@@ -518,6 +628,28 @@
                 # remote shell command is missing
                 return None, self.__isNetworked, ""
 
+        elif startRemote and self.__ericServerDebuggerInterface is not None:
+            # debugging via an eric-ide server
+            self.translate = self.__ericServerTranslation
+            self.__ericServerDebugging = True
+
+            args = []
+            if noencoding:
+                args.append(noencoding)
+            if multiprocessEnabled:
+                args.append(multiprocessEnabled)
+            if callTraceOptimization:
+                args.append(callTraceOptimization)
+            self.__ericServerDebuggerInterface.startClient(
+                interpreter,
+                originalPathString,
+                args,
+                workingDir=workingDir,
+            )
+            self.__startedVenv = venvName
+
+            return None, self.__isNetworked, ""
+
         else:
             # local debugging code below
             debugClient = project.getDebugProperty("DEBUGCLIENT")
@@ -620,7 +752,7 @@
         """
         self.__pendingConnections.append(sock)
 
-        sock.readyRead.connect(lambda: self.__parseClientLine(sock))
+        sock.readyRead.connect(lambda: self.__receiveJson(sock))
         sock.disconnected.connect(lambda: self.__socketDisconnected(sock))
 
         return True
@@ -635,30 +767,30 @@
         @param debuggerId id of the connected debug client
         @type str
         """
-        if sock in self.__pendingConnections:
+        if sock and sock in self.__pendingConnections:
             self.__connections[debuggerId] = sock
             self.__pendingConnections.remove(sock)
 
-            if self.__mainDebugger is None:
-                self.__mainDebugger = debuggerId
-                # Get the remote clients capabilities
-                self.remoteCapabilities(debuggerId)
+        if self.__mainDebugger is None:
+            self.__mainDebugger = debuggerId
+            # Get the remote clients capabilities
+            self.remoteCapabilities(debuggerId)
 
-            self.debugServer.signalClientDebuggerId(debuggerId)
+        self.debugServer.signalClientDebuggerId(debuggerId)
 
-            if debuggerId == self.__mainDebugger:
-                self.__flush()
-                self.debugServer.mainClientConnected()
+        if debuggerId == self.__mainDebugger:
+            self.__flush()
+            self.debugServer.mainClientConnected()
 
-            self.debugServer.initializeClient(debuggerId)
+        self.debugServer.initializeClient(debuggerId)
 
-            # perform auto-continue except for main
-            if (
-                debuggerId != self.__mainDebugger
-                and self.__autoContinue
-                and not self.__isStepCommand
-            ):
-                QTimer.singleShot(0, lambda: self.remoteContinue(debuggerId))
+        # perform auto-continue except for main
+        if (
+            debuggerId != self.__mainDebugger
+            and self.__autoContinue
+            and not self.__isStepCommand
+        ):
+            QTimer.singleShot(0, lambda: self.remoteContinue(debuggerId))
 
     def __socketDisconnected(self, sock):
         """
@@ -670,14 +802,7 @@
         for debuggerId in list(self.__connections):
             if self.__connections[debuggerId] is sock:
                 del self.__connections[debuggerId]
-                if debuggerId == self.__mainDebugger:
-                    self.__mainDebugger = None
-                if debuggerId in self.__autoContinued:
-                    self.__autoContinued.remove(debuggerId)
-                if not self.__inShutdown:
-                    with contextlib.suppress(RuntimeError):
-                        # can be ignored during a shutdown
-                        self.debugServer.signalClientDisconnected(debuggerId)
+                self.__handleServerDebugClientDisconnected(debuggerId)
                 break
         else:
             if sock in self.__pendingConnections:
@@ -685,13 +810,37 @@
 
         if not self.__connections:
             # no active connections anymore
+            self.__handleServerLastClientExited()
+
+    @pyqtSlot(str)
+    def __handleServerDebugClientDisconnected(self, debuggerId):
+        """
+        Private slot handling the disconnect of a debug client.
+
+        @param debuggerId ID of the disconnected debugger
+        @type str
+        """
+        if debuggerId == self.__mainDebugger:
+            self.__mainDebugger = None
+        if debuggerId in self.__autoContinued:
+            self.__autoContinued.remove(debuggerId)
+        if not self.__inShutdown:
             with contextlib.suppress(RuntimeError):
-                # debug server object might have been deleted already
-                # ignore this
+                # can be ignored during a shutdown
+                self.debugServer.signalClientDisconnected(debuggerId)
+
+    @pyqtSlot()
+    def __handleServerLastClientExited(self):
+        """
+        Private slot to handle the exit of the last debug client connected.
+        """
+        with contextlib.suppress(RuntimeError):
+            # debug server object might have been deleted already
+            # ignore this
+            self.__autoContinued.clear()
+            if not self.__inShutdown:
                 self.debugServer.signalLastClientExited()
-                self.__autoContinued.clear()
-                if not self.__inShutdown:
-                    self.debugServer.startClient()
+                self.debugServer.startClient()
 
     def getDebuggerIds(self):
         """
@@ -708,9 +857,15 @@
         """
         if self.__mainDebugger:
             # Send commands that were waiting for the connection.
-            conn = self.__connections[self.__mainDebugger]
-            for jsonStr in self.__commandQueue:
-                self.__writeJsonCommandToSocket(jsonStr, conn)
+            if self.__ericServerDebugging:
+                for jsonStr in self.__commandQueue:
+                    self.__ericServerDebuggerInterface.sendClientCommand(
+                        self.__mainDebugger, jsonStr
+                    )
+            else:
+                conn = self.__connections[self.__mainDebugger]
+                for jsonStr in self.__commandQueue:
+                    self.__writeJsonCommandToSocket(jsonStr, conn)
 
         self.__commandQueue.clear()
 
@@ -734,6 +889,8 @@
             sock = self.__pendingConnections.pop()
             self.__shutdownSocket(sock)
 
+        self.__ericServerDebuggerInterface.stopClient()
+
         # reinitialize
         self.__commandQueue.clear()
 
@@ -766,7 +923,7 @@
         @return flag indicating the connection status
         @rtype bool
         """
-        return bool(self.__connections)
+        return bool(self.__connections) or self.__ericServerDebugging
 
     def remoteEnvironment(self, env):
         """
@@ -811,12 +968,15 @@
             instead of unhandled exceptions only
         @type bool
         """
+        if FileSystemUtilities.isPlainFileName(fn):
+            fn = os.path.abspath(fn)
+
         self.__autoContinue = autoContinue
-        self.__scriptName = os.path.abspath(fn)
+        self.__scriptName = fn
         self.__isStepCommand = False
 
         wd = self.translate(wd, False)
-        fn = self.translate(os.path.abspath(fn), False)
+        fn = self.translate(fn, False)
         self.__sendJsonCommand(
             "RequestLoad",
             {
@@ -841,10 +1001,13 @@
         @param wd working directory for the program
         @type str
         """
-        self.__scriptName = os.path.abspath(fn)
+        if FileSystemUtilities.isPlainFileName(fn):
+            fn = os.path.abspath(fn)
+
+        self.__scriptName = fn
 
         wd = self.translate(wd, False)
-        fn = self.translate(os.path.abspath(fn), False)
+        fn = self.translate(fn, False)
         self.__sendJsonCommand(
             "RequestRun",
             {
@@ -869,10 +1032,13 @@
             cleared first
         @type bool
         """
-        self.__scriptName = os.path.abspath(fn)
+        if FileSystemUtilities.isPlainFileName(fn):
+            fn = os.path.abspath(fn)
+
+        self.__scriptName = fn
 
         wd = self.translate(wd, False)
-        fn = self.translate(os.path.abspath(fn), False)
+        fn = self.translate(fn, False)
         self.__sendJsonCommand(
             "RequestCoverage",
             {
@@ -898,10 +1064,13 @@
             first
         @type bool
         """
-        self.__scriptName = os.path.abspath(fn)
+        if FileSystemUtilities.isPlainFileName(fn):
+            fn = os.path.abspath(fn)
+
+        self.__scriptName = fn
 
         wd = self.translate(wd, False)
-        fn = self.translate(os.path.abspath(fn), False)
+        fn = self.translate(fn, False)
         self.__sendJsonCommand(
             "RequestProfile",
             {
@@ -1043,7 +1212,12 @@
         @param temp flag indicating a temporary breakpoint
         @type bool
         """
-        debuggerList = [debuggerId] if debuggerId else list(self.__connections)
+        if debuggerId:
+            debuggerList = [debuggerId]
+        elif self.__ericServerDebugging:
+            debuggerList = ["<<all>>"]
+        else:
+            debuggerList = list(self.__connections)
         for debuggerId in debuggerList:
             self.__sendJsonCommand(
                 "RequestBreakpoint",
@@ -1070,7 +1244,12 @@
         @param enable flag indicating enabling or disabling a breakpoint
         @type bool
         """
-        debuggerList = [debuggerId] if debuggerId else list(self.__connections)
+        if debuggerId:
+            debuggerList = [debuggerId]
+        elif self.__ericServerDebugging:
+            debuggerList = ["<<all>>"]
+        else:
+            debuggerList = list(self.__connections)
         for debuggerId in debuggerList:
             self.__sendJsonCommand(
                 "RequestBreakpointEnable",
@@ -1095,7 +1274,12 @@
         @param count number of occurrences to ignore
         @type int
         """
-        debuggerList = [debuggerId] if debuggerId else list(self.__connections)
+        if debuggerId:
+            debuggerList = [debuggerId]
+        elif self.__ericServerDebugging:
+            debuggerList = ["<<all>>"]
+        else:
+            debuggerList = list(self.__connections)
         for debuggerId in debuggerList:
             self.__sendJsonCommand(
                 "RequestBreakpointIgnore",
@@ -1120,7 +1304,12 @@
         @param temp flag indicating a temporary watch expression
         @type bool
         """
-        debuggerList = [debuggerId] if debuggerId else list(self.__connections)
+        if debuggerId:
+            debuggerList = [debuggerId]
+        elif self.__ericServerDebugging:
+            debuggerList = ["<<all>>"]
+        else:
+            debuggerList = list(self.__connections)
         for debuggerId in debuggerList:
             # cond is combination of cond and special (s. watch expression
             # viewer)
@@ -1145,7 +1334,12 @@
         @param enable flag indicating enabling or disabling a watch expression
         @type bool
         """
-        debuggerList = [debuggerId] if debuggerId else list(self.__connections)
+        if debuggerId:
+            debuggerList = [debuggerId]
+        elif self.__ericServerDebugging:
+            debuggerList = ["<<all>>"]
+        else:
+            debuggerList = list(self.__connections)
         for debuggerId in debuggerList:
             # cond is combination of cond and special (s. watch expression
             # viewer)
@@ -1170,7 +1364,12 @@
         @param count number of occurrences to ignore
         @type int
         """
-        debuggerList = [debuggerId] if debuggerId else list(self.__connections)
+        if debuggerId:
+            debuggerList = [debuggerId]
+        elif self.__ericServerDebugging:
+            debuggerList = ["<<all>>"]
+        else:
+            debuggerList = list(self.__connections)
         for debuggerId in debuggerList:
             # cond is combination of cond and special (s. watch expression
             # viewer)
@@ -1397,7 +1596,7 @@
             debuggerId,
         )
 
-    def __parseClientLine(self, sock):
+    def __receiveJson(self, sock):
         """
         Private method to handle data from the client.
 
@@ -1434,14 +1633,14 @@
 
             jsonStr = data.decode("utf-8", "backslashreplace")
 
-            logging.debug("<Debug-Server> %s", jsonStr)
+            logging.getLogger(__name__).debug("<Debug-Server> %s", jsonStr)
             ##print("Server: ", jsonStr)    ## debug       # __IGNORE_WARNING_M891__
 
-            self.__handleJsonCommand(jsonStr, sock)
+            self.handleJsonCommand(jsonStr, sock)
 
-    def __handleJsonCommand(self, jsonStr, sock):
+    def handleJsonCommand(self, jsonStr, sock):
         """
-        Private method to handle a command or response serialized as a
+        Public method to handle a command or response serialized as a
         JSON string.
 
         @param jsonStr string containing the command or response received
@@ -1567,6 +1766,7 @@
             self.debugServer.signalClientRawInput(
                 params["prompt"], params["echo"], params["debuggerId"]
             )
+            pass
 
         elif method == "ResponseBPConditionError":
             fn = self.translate(params["filename"], True)
@@ -1636,7 +1836,7 @@
         elif method == "ResponseExit":
             self.__scriptName = ""
             self.debugServer.signalClientExit(
-                params["program"],
+                self.translate(params["program"], True),
                 params["status"],
                 params["message"],
                 params["debuggerId"],
@@ -1677,14 +1877,26 @@
         }
         jsonStr = json.dumps(commandDict)
 
-        if debuggerId and debuggerId in self.__connections:
-            sock = self.__connections[debuggerId]
-        elif sock is None and self.__mainDebugger is not None:
-            sock = self.__connections[self.__mainDebugger]
-        if sock is not None:
-            self.__writeJsonCommandToSocket(jsonStr, sock)
+        if self.__ericServerDebugging:
+            # Debugging via the eric-ide server -> pass the command on to it
+            if self.__mainDebugger is None:
+                # debugger has not connected yet -> queue the command
+                self.__commandQueue.append(jsonStr)
+            else:
+                self.__ericServerDebuggerInterface.sendClientCommand(
+                    debuggerId, jsonStr
+                )
         else:
-            self.__commandQueue.append(jsonStr)
+            # Local debugging -> send the command to the client
+            if debuggerId and debuggerId in self.__connections:
+                sock = self.__connections[debuggerId]
+            elif sock is None and self.__mainDebugger is not None:
+                with contextlib.suppress(KeyError):
+                    sock = self.__connections[self.__mainDebugger]
+            if sock is not None:
+                self.__writeJsonCommandToSocket(jsonStr, sock)
+            else:
+                self.__commandQueue.append(jsonStr)
 
     def __writeJsonCommandToSocket(self, jsonCommand, sock):
         """

eric ide

mercurial