--- a/eric6/Debugger/DebuggerInterfacePython.py Sun Jan 17 13:53:08 2021 +0100 +++ b/eric6/Debugger/DebuggerInterfacePython.py Mon Feb 01 10:38:16 2021 +0100 @@ -14,7 +14,6 @@ from PyQt5.QtCore import ( QObject, QProcess, QProcessEnvironment, QTimer ) -from PyQt5.QtWidgets import QInputDialog from E5Gui.E5Application import e5App from E5Gui import E5MessageBox @@ -48,14 +47,18 @@ self.__isNetworked = True self.__autoContinue = False + self.__autoContinued = [] + self.__isStepCommand = False self.debugServer = debugServer self.passive = passive self.process = None self.__startedVenv = "" - self.qsock = None self.queue = [] + self.__master = None + self.__connections = {} + self.__pendingConnections = [] # set default values for capabilities of clients self.clientCapabilities = ClientDefaultCapabilities @@ -78,26 +81,32 @@ # attribute to remember the name of the executed script self.__scriptName = "" - + def __identityTranslation(self, fn, remote2local=True): """ Private method to perform the identity path translation. - @param fn filename to be translated (string) + @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]) - @return translated filename (string) + @type bool + @return translated filename + @rtype str """ return fn - + def __remoteTranslation(self, fn, remote2local=True): """ Private method to perform the path translation. - @param fn filename to be translated (string) + @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]) - @return translated filename (string) + @type bool + @return translated filename + @rtype str """ if remote2local: path = fn.replace(self.translateRemote, self.translateLocal) @@ -109,7 +118,7 @@ path = path.replace("\\", "/") return path - + def __startProcess(self, program, arguments, environment=None, workingDir=None): """ @@ -140,7 +149,7 @@ proc = None return proc - + def startRemote(self, port, runInConsole, venvName, originalPathString, workingDir=None): """ @@ -195,6 +204,10 @@ redirect = str(Preferences.getDebugger("Python3Redirect")) noencoding = (Preferences.getDebugger("Python3NoEncoding") and '--no-encoding' or '') + multiprocessEnabled = ( + '--multiprocess' if Preferences.getDebugger("MultiProcessEnabled") + else '' + ) if Preferences.getDebugger("RemoteDbgEnabled"): ipaddr = self.debugServer.getHostAddress(False) @@ -204,8 +217,12 @@ rhost = "localhost" if rexec: args = Utilities.parseOptionString(rexec) + [ - rhost, interpreter, debugClient, noencoding, str(port), - redirect, ipaddr] + rhost, interpreter, debugClient] + if noencoding: + args.append(noencoding) + if multiprocessEnabled: + args.append(multiprocessEnabled) + args.extend([str(port), redirect, ipaddr]) if Utilities.isWindowsPlatform(): if not os.path.splitext(args[0])[1]: for ext in [".exe", ".com", ".cmd", ".bat"]: @@ -270,8 +287,12 @@ ccmd = Preferences.getDebugger("ConsoleDbgCommand") if ccmd: args = Utilities.parseOptionString(ccmd) + [ - interpreter, os.path.abspath(debugClient), noencoding, - str(port), '0', ipaddr] + interpreter, os.path.abspath(debugClient)] + if noencoding: + args.append(noencoding) + if multiprocessEnabled: + args.append(multiprocessEnabled) + args.extend([str(port), '0', ipaddr]) args[0] = Utilities.getExecutablePath(args[0]) process = self.__startProcess(args[0], args[1:], clientEnv, workingDir=workingDir) @@ -284,11 +305,14 @@ """ started.</p>""")) return process, self.__isNetworked, interpreter - process = self.__startProcess( - interpreter, - [debugClient, noencoding, str(port), redirect, ipaddr], - clientEnv, - workingDir=workingDir) + args = [debugClient] + if noencoding: + args.append(noencoding) + if multiprocessEnabled: + args.append(multiprocessEnabled) + args.extend([str(port), redirect, ipaddr]) + process = self.__startProcess(interpreter, args, clientEnv, + workingDir=workingDir) if process is None: self.__startedVenv = "" E5MessageBox.critical( @@ -300,7 +324,7 @@ self.__startedVenv = venvName return process, self.__isNetworked, interpreter - + def startRemoteForProject(self, port, runInConsole, venvName, originalPathString, workingDir=None): """ @@ -337,7 +361,12 @@ redirect = str(project.getDebugProperty("REDIRECT")) noencoding = ( - project.getDebugProperty("NOENCODING") and '--no-encoding' or '') + '--no-encoding' if project.getDebugProperty("NOENCODING") else '' + ) + multiprocessEnabled = ( + '--multiprocess' if Preferences.getDebugger("MultiProcessEnabled") + else '' + ) venvManager = e5App().getObject("VirtualEnvManager") interpreter = venvManager.getVirtualenvInterpreter(venvName) @@ -364,8 +393,12 @@ rhost = "localhost" if rexec: args = Utilities.parseOptionString(rexec) + [ - rhost, interpreter, debugClient, noencoding, str(port), - redirect, ipaddr] + rhost, interpreter, debugClient] + if noencoding: + args.append(noencoding) + if multiprocessEnabled: + args.append(multiprocessEnabled) + args.extend([str(port), redirect, ipaddr]) if Utilities.isWindowsPlatform(): if not os.path.splitext(args[0])[1]: for ext in [".exe", ".com", ".cmd", ".bat"]: @@ -398,7 +431,7 @@ else: # remote shell command is missing return None, self.__isNetworked, "" - + # set translation function self.translate = self.__identityTranslation @@ -432,8 +465,12 @@ Preferences.getDebugger("ConsoleDbgCommand")) if ccmd: args = Utilities.parseOptionString(ccmd) + [ - interpreter, os.path.abspath(debugClient), noencoding, - str(port), '0', ipaddr] + interpreter, os.path.abspath(debugClient)] + if noencoding: + args.append(noencoding) + if multiprocessEnabled: + args.append(multiprocessEnabled) + args.extend([str(port), '0', ipaddr]) args[0] = Utilities.getExecutablePath(args[0]) process = self.__startProcess(args[0], args[1:], clientEnv, workingDir=workingDir) @@ -446,11 +483,14 @@ """ started.</p>""")) return process, self.__isNetworked, interpreter - process = self.__startProcess( - interpreter, - [debugClient, noencoding, str(port), redirect, ipaddr], - clientEnv, - workingDir=workingDir) + args = [debugClient] + if noencoding: + args.append(noencoding) + if multiprocessEnabled: + args.append(multiprocessEnabled) + args.extend([str(port), redirect, ipaddr]) + process = self.__startProcess(interpreter, args, clientEnv, + workingDir=workingDir) if process is None: self.__startedVenv = "" E5MessageBox.critical( @@ -462,12 +502,13 @@ self.__startedVenv = venvName return process, self.__isNetworked, interpreter - + def getClientCapabilities(self): """ Public method to retrieve the debug clients capabilities. - @return debug client capabilities (integer) + @return debug client capabilities + @rtype int """ return self.clientCapabilities @@ -475,91 +516,193 @@ """ Public slot to handle a new connection. - @param sock reference to the socket object (QTcpSocket) - @return flag indicating success (boolean) + @param sock reference to the socket object + @type QTcpSocket + @return flag indicating success + @rtype bool """ - # If we already have a connection, refuse this one. It will be closed - # automatically. - if self.qsock is not None: - return False + self.__pendingConnections.append(sock) - sock.disconnected.connect(self.debugServer.startClient) - sock.readyRead.connect(self.__parseClientLine) + sock.readyRead.connect(lambda: self.__parseClientLine(sock)) + sock.disconnected.connect(lambda: self.__socketDisconnected(sock)) - self.qsock = sock - - # Get the remote clients capabilities - self.remoteCapabilities() return True - def flush(self): + def __assignDebuggerId(self, sock, debuggerId): + """ + Private method to set the debugger id for a recent debugger connection + attempt. + + @param sock reference to the socket object + @type QTcpSocket + @param debuggerId id of the connected debug client + @type str + """ + if sock in self.__pendingConnections: + self.__connections[debuggerId] = sock + self.__pendingConnections.remove(sock) + + if self.__master is None: + self.__master = debuggerId + # Get the remote clients capabilities + self.remoteCapabilities(debuggerId) + + self.debugServer.signalClientDebuggerId(debuggerId) + + if debuggerId == self.__master: + self.__flush() + self.debugServer.masterClientConnected() + + self.debugServer.initializeClient(debuggerId) + + # perform auto-continue except for master + if ( + debuggerId != self.__master and + self.__autoContinue and + not self.__isStepCommand + ): + QTimer.singleShot( + 0, lambda: self.remoteContinue(debuggerId)) + + def __socketDisconnected(self, sock): """ - Public slot to flush the queue. + Private slot handling a socket disconnecting. + + @param sock reference to the disconnected socket + @type QTcpSocket """ - if self.qsock is not None: + for debuggerId in self.__connections: + if self.__connections[debuggerId] is sock: + del self.__connections[debuggerId] + if debuggerId == self.__master: + self.__master = None + if debuggerId in self.__autoContinued: + self.__autoContinued.remove(debuggerId) + self.debugServer.signalClientDisconnected(debuggerId) + break + else: + if sock in self.__pendingConnections: + self.__pendingConnections.remove(sock) + + if not self.__connections: + # no active connections anymore + try: + self.debugServer.signalLastClientExited() + except RuntimeError: + # debug server object might have been deleted already + # ignore this + pass + self.__autoContinued.clear() + self.debugServer.startClient() + + def getDebuggerIds(self): + """ + Public method to return the IDs of the connected debugger backends. + + @return list of connected debugger backend IDs + @rtype list of str + """ + return sorted(self.__connections.keys()) + + def __flush(self): + """ + Private slot to flush the queue. + """ + if self.__master: # Send commands that were waiting for the connection. for cmd in self.queue: - self.__writeJsonCommandToSocket(cmd) - - self.queue = [] + self.__writeJsonCommandToSocket( + cmd, self.__connections[self.__master]) + + self.queue = [] def shutdown(self): """ Public method to cleanly shut down. - It closes our socket and shuts down - the debug client. (Needed on Win OS) + It closes our sockets and shuts down the debug clients. + (Needed on Win OS) """ - if self.qsock is None: + if not self.__master: return - # do not want any slots called during shutdown - self.qsock.disconnected.disconnect(self.debugServer.startClient) - self.qsock.readyRead.disconnect(self.__parseClientLine) + while self.__connections: + debuggerId, sock = self.__connections.popitem() + self.__shutdownSocket(sock) - # close down socket, and shut down client as well. - self.__sendJsonCommand("RequestShutdown", {}) - self.qsock.flush() - self.qsock.close() + while self.__pendingConnections: + sock = self.__pendingConnections.pop() + self.__shutdownSocket(sock) # reinitialize - self.qsock = None self.queue = [] + + self.__master = None + + def __shutdownSocket(self, sock): + """ + Private slot to shut down a socket. + + @param sock reference to the socket + @type QTcpSocket + """ + # do not want any slots called during shutdown + sock.readyRead.disconnect() + sock.disconnected.disconnect() + + # close down socket, and shut down client as well. + self.__sendJsonCommand("RequestShutdown", {}, sock=sock) + sock.flush() + sock.close() + + sock.setParent(None) + sock.deleteLater() + del sock def isConnected(self): """ Public method to test, if a debug client has connected. - @return flag indicating the connection status (boolean) + @return flag indicating the connection status + @rtype bool """ - return self.qsock is not None + return bool(self.__connections) def remoteEnvironment(self, env): """ Public method to set the environment for a program to debug, run, ... - @param env environment settings (dictionary) + @param env environment settings + @type dict """ - self.__sendJsonCommand("RequestEnvironment", {"environment": env}) + if self.__master: + self.__sendJsonCommand("RequestEnvironment", {"environment": env}, + self.__master) def remoteLoad(self, fn, argv, wd, traceInterpreter=False, - autoContinue=True, autoFork=False, forkChild=False): + autoContinue=True, enableMultiprocess=False): """ Public method to load a new program to debug. - @param fn the filename to debug (string) - @param argv the commandline arguments to pass to the program (string) - @param wd the working directory for the program (string) - @keyparam traceInterpreter flag indicating if the interpreter library - should be traced as well (boolean) - @keyparam autoContinue flag indicating, that the debugger should not - stop at the first executable line (boolean) - @keyparam autoFork flag indicating the automatic fork mode (boolean) - @keyparam forkChild flag indicating to debug the child after forking - (boolean) + @param fn the filename to debug + @type str + @param argv the commandline arguments to pass to the program + @type str + @param wd the working directory for the program + @type str + @param traceInterpreter flag indicating if the interpreter library + should be traced as well + @type bool + @param autoContinue flag indicating, that the debugger should not + stop at the first executable line + @type bool + @param enableMultiprocess flag indicating to perform multiprocess + debugging + @type bool """ self.__autoContinue = autoContinue self.__scriptName = os.path.abspath(fn) + self.__isStepCommand = False wd = self.translate(wd, False) fn = self.translate(os.path.abspath(fn), False) @@ -568,20 +711,19 @@ "filename": fn, "argv": Utilities.parseOptionString(argv), "traceInterpreter": traceInterpreter, - "autofork": autoFork, - "forkChild": forkChild, - }) + "multiprocess": enableMultiprocess, + }, self.__master) - def remoteRun(self, fn, argv, wd, autoFork=False, forkChild=False): + def remoteRun(self, fn, argv, wd): """ Public method to load a new program to run. - @param fn the filename to run (string) - @param argv the commandline arguments to pass to the program (string) - @param wd the working directory for the program (string) - @keyparam autoFork flag indicating the automatic fork mode (boolean) - @keyparam forkChild flag indicating to debug the child after forking - (boolean) + @param fn the filename to run + @type str + @param argv the commandline arguments to pass to the program + @type str + @param wd the working directory for the program + @type str """ self.__scriptName = os.path.abspath(fn) @@ -591,19 +733,21 @@ "workdir": wd, "filename": fn, "argv": Utilities.parseOptionString(argv), - "autofork": autoFork, - "forkChild": forkChild, - }) + }, self.__master) def remoteCoverage(self, fn, argv, wd, erase=False): """ Public method to load a new program to collect coverage data. - @param fn the filename to run (string) - @param argv the commandline arguments to pass to the program (string) - @param wd the working directory for the program (string) - @keyparam erase flag indicating that coverage info should be - cleared first (boolean) + @param fn the filename to run + @type str + @param argv the commandline arguments to pass to the program + @type str + @param wd the working directory for the program + @type str + @param erase flag indicating that coverage info should be + cleared first + @type bool """ self.__scriptName = os.path.abspath(fn) @@ -614,17 +758,21 @@ "filename": fn, "argv": Utilities.parseOptionString(argv), "erase": erase, - }) - + }, self.__master) + def remoteProfile(self, fn, argv, wd, erase=False): """ Public method to load a new program to collect profiling data. - @param fn the filename to run (string) - @param argv the commandline arguments to pass to the program (string) - @param wd the working directory for the program (string) - @keyparam erase flag indicating that timing info should be cleared - first (boolean) + @param fn the filename to run + @type str + @param argv the commandline arguments to pass to the program + @type str + @param wd the working directory for the program + @type str + @param erase flag indicating that timing info should be cleared + first + @type bool """ self.__scriptName = os.path.abspath(fn) @@ -635,189 +783,310 @@ "filename": fn, "argv": Utilities.parseOptionString(argv), "erase": erase, - }) - - def remoteStatement(self, stmt): + }, self.__master) + + def remoteStatement(self, debuggerId, stmt): """ Public method to execute a Python statement. - @param stmt the Python statement to execute (string). It - should not have a trailing newline. + @param debuggerId ID of the debugger backend + @type str + @param stmt the Python statement to execute. + @type str """ self.__sendJsonCommand("ExecuteStatement", { "statement": stmt, - }) - - def remoteStep(self): + }, debuggerId) + + def remoteStep(self, debuggerId): """ Public method to single step the debugged program. + + @param debuggerId ID of the debugger backend + @type str """ - self.__sendJsonCommand("RequestStep", {}) - - def remoteStepOver(self): + self.__isStepCommand = True + self.__sendJsonCommand("RequestStep", {}, debuggerId) + + def remoteStepOver(self, debuggerId): """ Public method to step over the debugged program. + + @param debuggerId ID of the debugger backend + @type str """ - self.__sendJsonCommand("RequestStepOver", {}) - - def remoteStepOut(self): + self.__isStepCommand = True + self.__sendJsonCommand("RequestStepOver", {}, debuggerId) + + def remoteStepOut(self, debuggerId): """ Public method to step out the debugged program. + + @param debuggerId ID of the debugger backend + @type str """ - self.__sendJsonCommand("RequestStepOut", {}) - - def remoteStepQuit(self): + self.__isStepCommand = True + self.__sendJsonCommand("RequestStepOut", {}, debuggerId) + + def remoteStepQuit(self, debuggerId): """ Public method to stop the debugged program. + + @param debuggerId ID of the debugger backend + @type str """ - self.__sendJsonCommand("RequestStepQuit", {}) - - def remoteContinue(self, special=False): + self.__isStepCommand = True + self.__sendJsonCommand("RequestStepQuit", {}, debuggerId) + + def remoteContinue(self, debuggerId, special=False): """ Public method to continue the debugged program. + @param debuggerId ID of the debugger backend + @type str @param special flag indicating a special continue operation + @type bool """ + self.__isStepCommand = False self.__sendJsonCommand("RequestContinue", { "special": special, - }) - - def remoteMoveIP(self, line): + }, debuggerId) + + def remoteContinueUntil(self, debuggerId, line): + """ + Public method to continue the debugged program to the given line + or until returning from the current frame. + + @param debuggerId ID of the debugger backend + @type str + @param line the new line, where execution should be continued to + @type int + """ + self.__isStepCommand = False + self.__sendJsonCommand("RequestContinueUntil", { + "newLine": line, + }, debuggerId) + + def remoteMoveIP(self, debuggerId, line): """ Public method to move the instruction pointer to a different line. + @param debuggerId ID of the debugger backend + @type str @param line the new line, where execution should be continued + @type int """ self.__sendJsonCommand("RequestMoveIP", { "newLine": line, - }) - - def remoteBreakpoint(self, fn, line, setBreakpoint, cond=None, temp=False): + }, debuggerId) + + def remoteBreakpoint(self, debuggerId, fn, line, setBreakpoint, cond=None, + temp=False): """ Public method to set or clear a breakpoint. - @param fn filename the breakpoint belongs to (string) - @param line linenumber of the breakpoint (int) - @param setBreakpoint flag indicating setting or resetting a - breakpoint (boolean) - @param cond condition of the breakpoint (string) - @param temp flag indicating a temporary breakpoint (boolean) + @param debuggerId ID of the debugger backend + @type str + @param fn filename the breakpoint belongs to + @type str + @param line linenumber of the breakpoint + @type int + @param setBreakpoint flag indicating setting or resetting a breakpoint + @type bool + @param cond condition of the breakpoint + @type str + @param temp flag indicating a temporary breakpoint + @type bool """ - self.__sendJsonCommand("RequestBreakpoint", { - "filename": self.translate(fn, False), - "line": line, - "temporary": temp, - "setBreakpoint": setBreakpoint, - "condition": cond, - }) + if debuggerId: + debuggerList = [debuggerId] + else: + debuggerList = list(self.__connections.keys()) + for debuggerId in debuggerList: + self.__sendJsonCommand("RequestBreakpoint", { + "filename": self.translate(fn, False), + "line": line, + "temporary": temp, + "setBreakpoint": setBreakpoint, + "condition": cond, + }, debuggerId) - def remoteBreakpointEnable(self, fn, line, enable): + def remoteBreakpointEnable(self, debuggerId, fn, line, enable): """ Public method to enable or disable a breakpoint. - @param fn filename the breakpoint belongs to (string) - @param line linenumber of the breakpoint (int) + @param debuggerId ID of the debugger backend + @type str + @param fn filename the breakpoint belongs to + @type str + @param line linenumber of the breakpoint + @type int @param enable flag indicating enabling or disabling a breakpoint - (boolean) + @type bool """ - self.__sendJsonCommand("RequestBreakpointEnable", { - "filename": self.translate(fn, False), - "line": line, - "enable": enable, - }) + if debuggerId: + debuggerList = [debuggerId] + else: + debuggerList = list(self.__connections.keys()) + for debuggerId in debuggerList: + self.__sendJsonCommand("RequestBreakpointEnable", { + "filename": self.translate(fn, False), + "line": line, + "enable": enable, + }, debuggerId) - def remoteBreakpointIgnore(self, fn, line, count): + def remoteBreakpointIgnore(self, debuggerId, fn, line, count): """ Public method to ignore a breakpoint the next couple of occurrences. - @param fn filename the breakpoint belongs to (string) - @param line linenumber of the breakpoint (int) - @param count number of occurrences to ignore (int) + @param debuggerId ID of the debugger backend + @type str + @param fn filename the breakpoint belongs to + @type str + @param line linenumber of the breakpoint + @type int + @param count number of occurrences to ignore + @type int """ - self.__sendJsonCommand("RequestBreakpointIgnore", { - "filename": self.translate(fn, False), - "line": line, - "count": count, - }) + if debuggerId: + debuggerList = [debuggerId] + else: + debuggerList = list(self.__connections.keys()) + for debuggerId in debuggerList: + self.__sendJsonCommand("RequestBreakpointIgnore", { + "filename": self.translate(fn, False), + "line": line, + "count": count, + }, debuggerId) - def remoteWatchpoint(self, cond, setWatch, temp=False): + def remoteWatchpoint(self, debuggerId, cond, setWatch, temp=False): """ Public method to set or clear a watch expression. - @param cond expression of the watch expression (string) + @param debuggerId ID of the debugger backend + @type str + @param cond expression of the watch expression + @type str @param setWatch flag indicating setting or resetting a watch expression - (boolean) - @param temp flag indicating a temporary watch expression (boolean) + @type bool + @param temp flag indicating a temporary watch expression + @type bool """ - # cond is combination of cond and special (s. watch expression viewer) - self.__sendJsonCommand("RequestWatch", { - "temporary": temp, - "setWatch": setWatch, - "condition": cond, - }) + if debuggerId: + debuggerList = [debuggerId] + else: + debuggerList = list(self.__connections.keys()) + for debuggerId in debuggerList: + # cond is combination of cond and special (s. watch expression + # viewer) + self.__sendJsonCommand("RequestWatch", { + "temporary": temp, + "setWatch": setWatch, + "condition": cond, + }, debuggerId) - def remoteWatchpointEnable(self, cond, enable): + def remoteWatchpointEnable(self, debuggerId, cond, enable): """ Public method to enable or disable a watch expression. - @param cond expression of the watch expression (string) + @param debuggerId ID of the debugger backend + @type str + @param cond expression of the watch expression + @type str @param enable flag indicating enabling or disabling a watch expression - (boolean) + @type bool """ - # cond is combination of cond and special (s. watch expression viewer) - self.__sendJsonCommand("RequestWatchEnable", { - "condition": cond, - "enable": enable, - }) + if debuggerId: + debuggerList = [debuggerId] + else: + debuggerList = list(self.__connections.keys()) + for debuggerId in debuggerList: + # cond is combination of cond and special (s. watch expression + # viewer) + self.__sendJsonCommand("RequestWatchEnable", { + "condition": cond, + "enable": enable, + }, debuggerId) - def remoteWatchpointIgnore(self, cond, count): + def remoteWatchpointIgnore(self, debuggerId, cond, count): """ Public method to ignore a watch expression the next couple of occurrences. - @param cond expression of the watch expression (string) - @param count number of occurrences to ignore (int) + @param debuggerId ID of the debugger backend + @type str + @param cond expression of the watch expression + @type str + @param count number of occurrences to ignore + @type int """ - # cond is combination of cond and special (s. watch expression viewer) - self.__sendJsonCommand("RequestWatchIgnore", { - "condition": cond, - "count": count, - }) + if debuggerId: + debuggerList = [debuggerId] + else: + debuggerList = list(self.__connections.keys()) + for debuggerId in debuggerList: + # cond is combination of cond and special (s. watch expression + # viewer) + self.__sendJsonCommand("RequestWatchIgnore", { + "condition": cond, + "count": count, + }, debuggerId) - def remoteRawInput(self, s): + def remoteRawInput(self, debuggerId, inputString): """ Public method to send the raw input to the debugged program. - @param s the raw input (string) + @param debuggerId ID of the debugger backend + @type str + @param inputString the raw input + @type str """ self.__sendJsonCommand("RawInput", { - "input": s, - }) + "input": inputString, + }, debuggerId) - def remoteThreadList(self): + def remoteThreadList(self, debuggerId): """ Public method to request the list of threads from the client. + + @param debuggerId ID of the debugger backend + @type str """ - self.__sendJsonCommand("RequestThreadList", {}) - - def remoteSetThread(self, tid): + self.__sendJsonCommand("RequestThreadList", {}, debuggerId) + + def remoteSetThread(self, debuggerId, tid): """ Public method to request to set the given thread as current thread. - @param tid id of the thread (integer) + @param debuggerId ID of the debugger backend + @type str + @param tid id of the thread + @type int """ self.__sendJsonCommand("RequestThreadSet", { "threadID": tid, - }) + }, debuggerId) + + def remoteClientStack(self, debuggerId): + """ + Public method to request the stack of the main thread. - def remoteClientVariables(self, scope, filterList, framenr=0, maxSize=0): + @param debuggerId ID of the debugger backend + @type str + """ + self.__sendJsonCommand("RequestStack", {}, debuggerId) + + def remoteClientVariables(self, debuggerId, scope, filterList, framenr=0, + maxSize=0): """ Public method to request the variables of the debugged program. + @param debuggerId ID of the debugger backend + @type str @param scope the scope of the variables (0 = local, 1 = global) @type int @param filterList list of variable types to filter out - @type list of int + @type list of str @param framenr framenumber of the variables to retrieve @type int @param maxSize maximum size the formatted value of a variable will @@ -830,17 +1099,19 @@ "scope": scope, "filters": filterList, "maxSize": maxSize, - }) + }, debuggerId) - def remoteClientVariable(self, scope, filterList, var, framenr=0, - maxSize=0): + def remoteClientVariable(self, debuggerId, scope, filterList, var, + framenr=0, maxSize=0): """ Public method to request the variables of the debugged program. + @param debuggerId ID of the debugger backend + @type str @param scope the scope of the variables (0 = local, 1 = global) @type int @param filterList list of variable types to filter out - @type list of int + @type list of str @param var list encoded name of variable to retrieve @type list of str @param framenr framenumber of the variables to retrieve @@ -856,36 +1127,61 @@ "scope": scope, "filters": filterList, "maxSize": maxSize, - }) + }, debuggerId) - def remoteClientDisassembly(self): + def remoteClientDisassembly(self, debuggerId): """ Public method to ask the client for the latest traceback disassembly. + + @param debuggerId ID of the debugger backend + @type str """ - self.__sendJsonCommand("RequestDisassembly", {}) + self.__sendJsonCommand("RequestDisassembly", {}, debuggerId) - def remoteClientSetFilter(self, scope, filterStr): + def remoteClientSetFilter(self, debuggerId, scope, filterStr): """ Public method to set a variables filter list. + @param debuggerId ID of the debugger backend + @type str @param scope the scope of the variables (0 = local, 1 = global) + @type int @param filterStr regexp string for variable names to filter out - (string) + @type str """ self.__sendJsonCommand("RequestSetFilter", { "scope": scope, "filter": filterStr, - }) + }, debuggerId) - def setCallTraceEnabled(self, on): + def setCallTraceEnabled(self, debuggerId, on): """ Public method to set the call trace state. - @param on flag indicating to enable the call trace function (boolean) + @param debuggerId ID of the debugger backend + @type str + @param on flag indicating to enable the call trace function + @type bool """ self.__sendJsonCommand("RequestCallTrace", { "enable": on, - }) + }, debuggerId) + + def remoteNoDebugList(self, debuggerId, noDebugList): + """ + Public method to set a list of programs not to be debugged. + + The programs given in the list will not be run under the control + of the multi process debugger. + + @param debuggerId ID of the debugger backend + @type str + @param noDebugList list of Python programs not to be debugged + @type list of str + """ + self.__sendJsonCommand("RequestSetNoDebugList", { + "noDebug": noDebugList, + }, debuggerId) def remoteBanner(self): """ @@ -893,22 +1189,28 @@ """ self.__sendJsonCommand("RequestBanner", {}) - def remoteCapabilities(self): + def remoteCapabilities(self, debuggerId): """ Public slot to get the debug clients capabilities. + + @param debuggerId ID of the debugger backend + @type str """ - self.__sendJsonCommand("RequestCapabilities", {}) + self.__sendJsonCommand("RequestCapabilities", {}, debuggerId) - def remoteCompletion(self, text): + def remoteCompletion(self, debuggerId, text): """ Public slot to get the a list of possible commandline completions from the remote client. - @param text the text to be completed (string) + @param debuggerId ID of the debugger backend + @type str + @param text the text to be completed + @type str """ self.__sendJsonCommand("RequestCompletion", { "text": text, - }) + }, debuggerId) def remoteUTDiscover(self, syspath, workdir, discoveryStart): """ @@ -1008,43 +1310,24 @@ """ self.__sendJsonCommand("RequestUTStop", {}) - def __askForkTo(self): - """ - Private method to ask the user which branch of a fork to follow. - """ - selections = [self.tr("Parent Process"), - self.tr("Child process")] - res, ok = QInputDialog.getItem( - None, - self.tr("Client forking"), - self.tr("Select the fork branch to follow."), - selections, - 0, False) - if not ok or res == selections[0]: - self.__sendJsonCommand("ResponseForkTo", { - "target": "parent", - }) - else: - self.__sendJsonCommand("ResponseForkTo", { - "target": "child", - }) - - def __parseClientLine(self): + def __parseClientLine(self, sock): """ Private method to handle data from the client. + + @param sock reference to the socket to read data from + @type QTcpSocket """ - while self.qsock and self.qsock.canReadLine(): - qs = self.qsock.readLine() + while sock and sock.canReadLine(): + qs = sock.readLine() line = bytes(qs).decode( encoding=Preferences.getSystem("StringEncoding")) logging.debug("<Debug-Server> %s", line) -## print("Server: ", line) ##debug +## print("Server: ", line) ## debug # __IGNORE_WARNING_M891__ - self.__handleJsonCommand(line) - continue + self.__handleJsonCommand(line, sock) - def __handleJsonCommand(self, jsonStr): + def __handleJsonCommand(self, jsonStr, sock): """ Private method to handle a command or response serialized as a JSON string. @@ -1052,6 +1335,8 @@ @param jsonStr string containing the command or response received from the debug backend @type str + @param sock reference to the socket the data was received from + @type QTcpSocket """ import json @@ -1075,26 +1360,36 @@ method = commandDict["method"] params = commandDict["params"] - if method == "ClientOutput": - self.debugServer.signalClientOutput(params["text"]) + if method == "DebuggerId": + self.__assignDebuggerId(sock, params["debuggerId"]) + + elif method == "ClientOutput": + self.debugServer.signalClientOutput( + params["text"], params["debuggerId"]) elif method in ["ResponseLine", "ResponseStack"]: - # Check if obsolet thread was clicked + # Check if obsolete thread was clicked if params["stack"] == []: # Request updated list - self.remoteThreadList() + self.remoteThreadList(params["debuggerId"]) return for s in params["stack"]: s[0] = self.translate(s[0], True) cf = params["stack"][0] - if self.__autoContinue: - self.__autoContinue = False - QTimer.singleShot(0, self.remoteContinue) + if ( + self.__autoContinue and + params["debuggerId"] not in self.__autoContinued + ): + self.__autoContinued.append(params["debuggerId"]) + QTimer.singleShot( + 0, lambda: self.remoteContinue(params["debuggerId"])) else: self.debugServer.signalClientLine( - cf[0], int(cf[1]), - method == "ResponseStack") - self.debugServer.signalClientStack(params["stack"]) + cf[0], int(cf[1]), params["debuggerId"], + method == "ResponseStack", threadName=params["threadName"]) + self.debugServer.signalClientStack( + params["stack"], params["debuggerId"], + threadName=params["threadName"]) elif method == "CallTrace": isCall = params["event"].lower() == "c" @@ -1105,112 +1400,125 @@ fromInfo["filename"], str(fromInfo["linenumber"]), fromInfo["codename"], toInfo["filename"], str(toInfo["linenumber"]), - toInfo["codename"]) + toInfo["codename"], + params["debuggerId"]) elif method == "ResponseVariables": self.debugServer.signalClientVariables( - params["scope"], params["variables"]) + params["scope"], params["variables"], params["debuggerId"]) elif method == "ResponseVariable": self.debugServer.signalClientVariable( - params["scope"], [params["variable"]] + params["variables"]) + params["scope"], [params["variable"]] + params["variables"], + params["debuggerId"]) elif method == "ResponseThreadList": self.debugServer.signalClientThreadList( - params["currentID"], params["threadList"]) + params["currentID"], params["threadList"], + params["debuggerId"]) elif method == "ResponseThreadSet": - self.debugServer.signalClientThreadSet() + self.debugServer.signalClientThreadSet(params["debuggerId"]) elif method == "ResponseCapabilities": self.clientCapabilities = params["capabilities"] - self.debugServer.signalClientCapabilities( - params["capabilities"], - params["clientType"], - self.__startedVenv, - ) + if params["debuggerId"] == self.__master: + # signal only for the master connection + self.debugServer.signalClientCapabilities( + params["capabilities"], + params["clientType"], + self.__startedVenv, + ) elif method == "ResponseBanner": - self.debugServer.signalClientBanner( - params["version"], - params["platform"], - params["dbgclient"], - self.__startedVenv, - ) + if params["debuggerId"] == self.__master: + # signal only for the master connection + self.debugServer.signalClientBanner( + params["version"], + params["platform"], + self.__startedVenv, + ) elif method == "ResponseOK": - self.debugServer.signalClientStatement(False) + self.debugServer.signalClientStatement(False, params["debuggerId"]) elif method == "ResponseContinue": - self.debugServer.signalClientStatement(True) + self.debugServer.signalClientStatement(True, params["debuggerId"]) elif method == "RequestRaw": self.debugServer.signalClientRawInput( - params["prompt"], params["echo"]) + params["prompt"], params["echo"], params["debuggerId"]) elif method == "ResponseBPConditionError": fn = self.translate(params["filename"], True) self.debugServer.signalClientBreakConditionError( - fn, params["line"]) + fn, params["line"], params["debuggerId"]) elif method == "ResponseClearBreakpoint": fn = self.translate(params["filename"], True) - self.debugServer.signalClientClearBreak(fn, params["line"]) + self.debugServer.signalClientClearBreak( + fn, params["line"], params["debuggerId"]) elif method == "ResponseWatchConditionError": self.debugServer.signalClientWatchConditionError( - params["condition"]) + params["condition"], params["debuggerId"]) elif method == "ResponseClearWatch": - self.debugServer.signalClientClearWatch(params["condition"]) + self.debugServer.signalClientClearWatch( + params["condition"], params["debuggerId"]) elif method == "ResponseDisassembly": - self.debugServer.signalClientDisassembly(params["disassembly"]) + self.debugServer.signalClientDisassembly( + params["disassembly"], params["debuggerId"]) elif method == "ResponseException": - if params: - exctype = params["type"] - excmessage = params["message"] - stack = params["stack"] - if stack: + exctype = params["type"] + excmessage = params["message"] + stack = params["stack"] + if stack: + for stackEntry in stack: + stackEntry[0] = self.translate(stackEntry[0], True) + if stack[0] and stack[0][0] == "<string>": for stackEntry in stack: - stackEntry[0] = self.translate(stackEntry[0], True) - if stack[0] and stack[0][0] == "<string>": - for stackEntry in stack: - if stackEntry[0] == "<string>": - stackEntry[0] = self.__scriptName - else: - break - else: - exctype = '' - excmessage = '' - stack = [] + if stackEntry[0] == "<string>": + stackEntry[0] = self.__scriptName + else: + break self.debugServer.signalClientException( - exctype, excmessage, stack) + exctype, excmessage, stack, params["debuggerId"], + params["threadName"]) elif method == "ResponseSyntax": self.debugServer.signalClientSyntaxError( params["message"], self.translate(params["filename"], True), - params["linenumber"], params["characternumber"]) + params["linenumber"], params["characternumber"], + params["debuggerId"], params["threadName"]) elif method == "ResponseSignal": self.debugServer.signalClientSignal( params["message"], self.translate(params["filename"], True), - params["linenumber"], params["function"], params["arguments"]) + params["linenumber"], params["function"], params["arguments"], + params["debuggerId"]) elif method == "ResponseExit": self.__scriptName = "" self.debugServer.signalClientExit( - params["status"], params["message"]) + params["program"], params["status"], params["message"], + params["debuggerId"]) elif method == "PassiveStartup": self.debugServer.passiveStartUp( - self.translate(params["filename"], True), params["exceptions"]) + self.translate(params["filename"], True), params["exceptions"], + params["debuggerId"]) elif method == "ResponseCompletion": self.debugServer.signalClientCompletionList( - params["completions"], params["text"]) + params["completions"], params["text"], params["debuggerId"]) + + ################################################################### + ## Unit test related stuff is not done with multi processing + ################################################################### elif method == "ResponseUTDiscover": self.debugServer.clientUtDiscovered( @@ -1250,11 +1558,8 @@ elif method == "ResponseUTTestSucceededUnexpected": self.debugServer.clientUtTestSucceededUnexpected( params["testname"], params["id"]) - - elif method == "RequestForkTo": - self.__askForkTo() - def __sendJsonCommand(self, command, params): + def __sendJsonCommand(self, command, params, debuggerId="", sock=None): """ Private method to send a single command to the client. @@ -1262,6 +1567,11 @@ @type str @param params dictionary of named parameters for the command @type dict + @param debuggerId id of the debug client to send the command to + @type str + @param sock reference to the socket object to be used (only used if + debuggerId is not given) + @type QTcpSocket """ import json @@ -1271,22 +1581,29 @@ "params": params, } cmd = json.dumps(commandDict) + '\n' - if self.qsock is not None: - self.__writeJsonCommandToSocket(cmd) + + if debuggerId and debuggerId in self.__connections: + sock = self.__connections[debuggerId] + elif sock is None and self.__master is not None: + sock = self.__connections[self.__master] + if sock is not None: + self.__writeJsonCommandToSocket(cmd, sock) else: self.queue.append(cmd) - def __writeJsonCommandToSocket(self, cmd): + def __writeJsonCommandToSocket(self, cmd, sock): """ Private method to write a JSON command to the socket. @param cmd JSON command to be sent @type str + @param sock reference to the socket to write to + @type QTcpSocket """ data = cmd.encode('utf8', 'backslashreplace') length = "{0:09d}".format(len(data)) - self.qsock.write(length.encode() + data) - self.qsock.flush() + sock.write(length.encode() + data) + sock.flush() def createDebuggerInterfacePython3(debugServer, passive):