--- 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): """