Thu, 10 Dec 2020 20:16:21 +0100
Added some more process creation function overrides.
--- a/eric6/DebugClients/Python/DebugClientBase.py Mon Dec 07 19:53:15 2020 +0100 +++ b/eric6/DebugClients/Python/DebugClientBase.py Thu Dec 10 20:16:21 2020 +0100 @@ -31,7 +31,7 @@ from FlexCompleter import Completer from DebugUtilities import prepareJsonCommand from BreakpointWatch import Breakpoint, Watch -##from MultiProcessDebugExtension import patchNewProcessFunctions +from MultiProcessDebugExtension import patchNewProcessFunctions from DebugUtilities import getargvalues, formatargvalues @@ -68,25 +68,6 @@ ############################################################################### -def DebugClientFork(): - """ - Replacement for the standard os.fork(). - - @return result of the fork() call - """ - if DebugClientInstance is None: - return DebugClientOrigFork() - - return DebugClientInstance.fork() - -# use our own fork(). -if 'fork' in dir(os): - DebugClientOrigFork = os.fork - os.fork = DebugClientFork - -############################################################################### - - def DebugClientClose(fd): """ Replacement for the standard os.close(fd). @@ -182,9 +163,6 @@ self.debugging = False self.multiprocessSupport = False self.noDebugList = [] - - self.fork_auto = False - self.fork_child = False self.readstream = None self.writestream = None @@ -284,13 +262,29 @@ """ Private method to compile source code read from a file. - @param filename name of the source file (string) - @param mode kind of code to be generated (string, exec or eval) + @param filename name of the source file + @type str + @param mode kind of code to be generated (exec or eval) + @type str @return compiled code object (None in case of errors) """ with codecs.open(filename, encoding=self.__coding) as fp: statement = fp.read() + return self.__compileCommand(statement, filename=filename, mode=mode) + + def __compileCommand(self, statement, filename="<string>", mode="exec"): + """ + Private method to compile source code. + + @param statement source code string to be compiled + @type str + @param filename name of the source file + @type str + @param mode kind of code to be generated (exec or eval) + @type str + @return compiled code object (None in case of errors) + """ try: code = compile(statement + '\n', filename, mode) except SyntaxError: @@ -432,9 +426,6 @@ self.debugging = True self.multiprocessSupport = params["multiprocess"] - self.fork_auto = params["autofork"] - self.fork_child = params["forkChild"] - self.threads.clear() self.attachThread(mainThread=True) @@ -473,9 +464,6 @@ self.running = sys.argv[0] self.botframe = None - self.fork_auto = params["autofork"] - self.fork_child = params["forkChild"] - self.threads.clear() self.attachThread(mainThread=True) @@ -932,11 +920,6 @@ elif method == "RequestUTStop": self.testResult.stop() - - elif method == "ResponseForkTo": - # this results from a separate event loop - self.fork_child = (params["target"] == 'child') - self.eventExit = True def __assembleTestCasesList(self, suite, start): """ @@ -2054,7 +2037,7 @@ def startProgInDebugger(self, progargs, wd='', host=None, port=None, exceptions=True, tracePython=False, redirect=True, passive=True, - multiprocessSupport=False): + multiprocessSupport=False, codeStr=""): """ Public method used to start the remote debugger. @@ -2074,6 +2057,8 @@ @param multiprocessSupport flag indicating to enable multiprocess debugging support @type bool + @param codeStr string containing Python code to execute + @type str @return exit code of the debugged program @rtype int """ @@ -2088,15 +2073,19 @@ self._fncache = {} self.dircache = [] - sys.argv = progargs[:] - sys.argv[0] = os.path.abspath(sys.argv[0]) - sys.path = self.__getSysPath(os.path.dirname(sys.argv[0])) - if wd == '': - os.chdir(sys.path[1]) + if codeStr: + self.running = "<string>" + sys.argv = ["<string>"] + progargs[:] else: - os.chdir(wd) - self.running = sys.argv[0] - self.__setCoding(self.running) + sys.argv = progargs[:] + sys.argv[0] = os.path.abspath(sys.argv[0]) + sys.path = self.__getSysPath(os.path.dirname(sys.argv[0])) + if wd == '': + os.chdir(sys.path[1]) + else: + os.chdir(wd) + self.running = sys.argv[0] + self.__setCoding(self.running) self.debugging = True self.multiprocessSupport = multiprocessSupport @@ -2116,7 +2105,10 @@ # This will eventually enter a local event loop. self.debugMod.__dict__['__file__'] = self.running sys.modules['__main__'] = self.debugMod - code = self.__compileFileSource(self.running) + if codeStr: + code = self.__compileCommand(codeStr) + else: + code = self.__compileFileSource(self.running) if code: res = self.mainThread.run(code, self.debugMod.__dict__, debug=True) else: @@ -2179,6 +2171,7 @@ redirect = True passive = True multiprocess = False + hasCode = False while args[0]: if args[0] == '-h': host = args[1] @@ -2204,20 +2197,15 @@ elif args[0] == '--no-encoding': self.noencoding = True del args[0] - elif args[0] == '--fork-child': - self.fork_auto = True - self.fork_child = True - del args[0] - elif args[0] == '--fork-parent': - self.fork_auto = True - self.fork_child = False - del args[0] elif args[0] == '--no-passive': passive = False del args[0] elif args[0] == '--multiprocess': multiprocess = True del args[0] + elif args[0] == '--code': + hasCode = True + del args[0] elif args[0] == '--': del args[0] break @@ -2234,15 +2222,20 @@ # TODO: check which ones are really needed self.startOptions = ( wd, host, port, exceptions, tracePython, redirect, - self.noencoding, self.fork_auto, self.fork_child, + self.noencoding, ) if not self.noencoding: self.__coding = self.defaultCoding -## patchNewProcessFunctions(multiprocess, self) + patchNewProcessFunctions(multiprocess, self) + if hasCode: + codeStr = args.pop(0) + else: + codeStr="" res = self.startProgInDebugger( args, wd, host, port, exceptions=exceptions, tracePython=tracePython, redirect=redirect, - passive=passive, multiprocessSupport=multiprocess + passive=passive, multiprocessSupport=multiprocess, + codeStr=codeStr ) sys.exit(res) else: @@ -2291,49 +2284,17 @@ # TODO: check which ones are really needed self.startOptions = ( '', remoteAddress, port, True, False, redirect, - self.noencoding, self.fork_auto, self.fork_child, + self.noencoding, ) if not self.noencoding: self.__coding = self.defaultCoding + patchNewProcessFunctions(self.multiprocessSupport, self) self.connectDebugger(port, remoteAddress, redirect) -## patchNewProcessFunctions(self.multiprocessSupport, self) self.__interact() else: print("No network port given. Aborting...") # __IGNORE_WARNING_M801__ - def fork(self): - """ - Public method implementing a fork routine deciding which branch - to follow. - - @return process ID (integer) - """ -## if not self.fork_auto: -## self.sendJsonCommand("RequestForkTo", {}) -## self.eventLoop(True) - pid = DebugClientOrigFork() - if pid == 0: - # child -## if not self.fork_child: - sys.settrace(None) - sys.setprofile(None) - self.sessionClose(False) -## (wd, host, port, exceptions, tracePython, redirect, -## noencoding, fork_auto, fork_child) = self.startOptions -## self.startDebugger(sys.argv[0], host, port, -## exceptions=exceptions, -## tracePython=tracePython, -## redirect=redirect, -## passive=False) - else: - # parent - if self.fork_child: - sys.settrace(None) - sys.setprofile(None) - self.sessionClose(False) - return pid - def close(self, fd): """ Public method implementing a close method as a replacement for
--- a/eric6/DebugClients/Python/DebugUtilities.py Mon Dec 07 19:53:15 2020 +0100 +++ b/eric6/DebugClients/Python/DebugUtilities.py Thu Dec 10 20:16:21 2020 +0100 @@ -228,6 +228,51 @@ ) +def removeQuotesFromArgs(args): + """ + Function to remove quotes from the arguments list. + + @param args list of arguments + @type list of str + @return list of unquoted strings + @rtype list of str + """ + if isWindowsPlatform(): + newArgs = [] + for x in args: + if len(x) > 1 and x.startswith('"') and x.endswith('"'): + x = x[1:-1] + newArgs.append(x) + return newArgs + else: + return args + + +def quoteArgs(args): + """ + Function to quote the given list of arguments. + + @param args list of arguments to be quoted + @type list of str + @return list of quoted arguments + @rtype list of str + """ + if isWindowsPlatform(): + quotedArgs = [] + for x in args: + if x.startswith('"') and x.endswith('"'): + quotedArgs.append(x) + else: + if ' ' in x: + x = x.replace('"', '\\"') + quotedArgs.append('"{0}"'.format(x)) + else: + quotedArgs.append(x) + return quotedArgs + else: + return args + + def patchArguments(debugClient, arguments, noRedirect=False): """ Function to patch the arguments given to start a program in order to @@ -243,6 +288,7 @@ @rtype list of str """ args = list(arguments[:]) # create a copy of the arguments list + args = removeQuotesFromArgs(args) # support for shebang line program = os.path.basename(args[0]).lower() @@ -254,10 +300,8 @@ # insert our interpreter as first argument args.insert(0, sys.executable) - # check for -c or -m invocation => debugging not supported yet - if "-c" in args: - cm_position = args.index("-c") - elif "-m" in args: + # check for -m invocation => debugging not supported yet + if "-m" in args: cm_position = args.index("-m") else: cm_position = 0 @@ -272,18 +316,25 @@ found = False if found and cm_position < pos: # it belongs to the interpreter - return arguments + return quoteArgs(arguments) # extract list of interpreter arguments, i.e. all arguments before the # first one not starting with '-'. interpreter = args.pop(0) interpreterArgs = [] + hasCode = False while args: if args[0].startswith("-"): if args[0] in ("-W", "-X"): # take two elements off the list interpreterArgs.append(args.pop(0)) interpreterArgs.append(args.pop(0)) + elif args[0] == "-c": + # -c indicates code to be executed and ends the + # arguments list + args.pop(0) + hasCode = True + break else: interpreterArgs.append(args.pop(0)) else: @@ -313,10 +364,123 @@ modifiedArguments.append("--no-encoding") if debugClient.multiprocessSupport: modifiedArguments.append("--multiprocess") + if hasCode: + modifiedArguments.append("--code") modifiedArguments.append("--") # end the arguments for DebugClient # append the arguments for the program to be debugged modifiedArguments.extend(args) + modifiedArguments = quoteArgs(modifiedArguments) return modifiedArguments + + +def stringToArgumentsWindows(args): + """ + Function to prepare a string of arguments for Windows platform. + + @param args list of command arguments + @type str + @return list of command arguments + @rtype list of str + @exception RuntimeError raised to indicate an illegal arguments parsing + condition + """ + # see http:#msdn.microsoft.com/en-us/library/a1y7w461.aspx + result = [] + + DEFAULT = 0 + ARG = 1 + IN_DOUBLE_QUOTE = 2 + + state = DEFAULT + backslashes = 0 + buf = '' + + argsLen = len(args) + for i in range(argsLen): + ch = args[i] + if ch == '\\': + backslashes += 1 + continue + elif backslashes != 0: + if ch == '"': + while backslashes >= 2: + backslashes -= 2 + buf += '\\' + if backslashes == 1: + if state == DEFAULT: + state = ARG + + buf += '"' + backslashes = 0 + continue + else: + # false alarm, treat passed backslashes literally... + if state == DEFAULT: + state = ARG + + while backslashes > 0: + backslashes -= 1 + buf += '\\' + + if ch in (' ', '\t'): + if state == DEFAULT: + # skip + continue + elif state == ARG: + state = DEFAULT + result.append(buf) + buf = '' + continue + + if state in (DEFAULT, ARG): + if ch == '"': + state = IN_DOUBLE_QUOTE + else: + state = ARG + buf += ch + + elif state == IN_DOUBLE_QUOTE: + if ch == '"': + if i + 1 < argsLen and args[i + 1] == '"': + # Undocumented feature in Windows: + # Two consecutive double quotes inside a double-quoted + # argument are interpreted as a single double quote. + buf += '"' + i += 1 + elif len(buf) == 0: + result.append("\"\"") + state = DEFAULT + else: + state = ARG + else: + buf += ch + + else: + raise RuntimeError('Illegal condition') + + if len(buf) > 0 or state != DEFAULT: + result.append(buf) + + return result + + +def patchArgumentStringWindows(debugClient, argStr): + """ + Function to patch an argument string for Windows. + + @param debugClient reference to the debug client object + @type DebugClient + @param argStr argument string + @type str + @return patched argument string + @rtype str + """ + args = stringToArgumentsWindows(argStr) + if not args or not isPythonProgram(args[0]): + return argStr + + argStr = ' '.join(patchArguments(debugClient, args)) + return argStr
--- a/eric6/DebugClients/Python/MultiProcessDebugExtension.py Mon Dec 07 19:53:15 2020 +0100 +++ b/eric6/DebugClients/Python/MultiProcessDebugExtension.py Thu Dec 10 20:16:21 2020 +0100 @@ -9,7 +9,10 @@ """ -from DebugUtilities import patchArguments, isPythonProgram +from DebugUtilities import ( + patchArguments, patchArgumentStringWindows, isPythonProgram, + isWindowsPlatform +) _debugClient = None @@ -47,7 +50,56 @@ """ Function replacing the 'execl' functions of the os module. """ - print(args) + import os + if ( + _debugClient.debugging and + _debugClient.multiprocessSupport + ): + args = patchArguments(_debugClient, args) + if isPythonProgram(args[0]): + path = args[0] + return getattr(os, originalName)(path, *args) + return newExecl + + +def createExecv(originalName): + """ + Function to patch the 'execv' process creation functions. + + <ul> + <li>os.execv(path, args)</li> + <li>os.execvp(file, args)</li> + </ul> + """ + def newExecv(path, args): + """ + Function replacing the 'execv' functions of the os module. + """ + import os + if ( + _debugClient.debugging and + _debugClient.multiprocessSupport + ): + args = patchArguments(_debugClient, args) + if isPythonProgram(args[0]): + path = args[0] + return getattr(os, originalName)(path, args) + return newExecv + + +def createExecve(originalName): + """ + Function to patch the 'execve' process creation functions. + + <ul> + <li>os.execve(path, args, env)</li> + <li>os.execvpe(file, args, env)</li> + </ul> + """ + def newExecve(path, args, env): + """ + Function replacing the 'execve' functions of the os module. + """ import os if ( _debugClient.debugging and @@ -56,9 +108,123 @@ args = patchArguments(_debugClient, args) if isPythonProgram(args[0]): path = args[0] - print(args) - return getattr(os, originalName)(path, *args) - return newExecl + return getattr(os, originalName)(path, args, env) + return newExecve + + +# TODO: add createSpawn... + + +def createForkExec(originalName): + """ + Function to patch the 'fork_exec' process creation functions. + + <ul> + <li>_posixsubprocess.fork_exec(args, executable_list, close_fds, + ... (13 more))</li> + </ul> + """ + def newForkExec(args, *other_args): + """ + Function replacing the 'fork_exec' functions of the _posixsubprocess + module. + """ + import _posixsubprocess + if ( + _debugClient.debugging and + _debugClient.multiprocessSupport + ): + args = patchArguments(_debugClient, args) + return getattr(_posixsubprocess, originalName)(args, *other_args) + return newForkExec + + +def createFork(original_name): + """ + Function to patch the 'fork' process creation functions. + + <ul> + <li>os.fork()</li> + </ul> + """ + def new_fork(): + """ + Function replacing the 'fork' function of the os module. + """ + import os + import sys + + # A simple fork will result in a new python process + isNewPythonProcess = True + frame = sys._getframe() + + multiprocess = ( + _debugClient.debugging and _debugClient.multiprocessSupport + ) + + isSubprocessFork = False + while frame is not None: + if ( + frame.f_code.co_name == '_execute_child' and + 'subprocess' in frame.f_code.co_filename + ): + isSubprocessFork = True + # If we're actually in subprocess.Popen creating a child, it + # may result in something which is not a Python process, (so, + # we don't want to connect with it in the forked version). + executable = frame.f_locals.get('executable') + if executable is not None: + isNewPythonProcess = False + if isPythonProgram(executable): + isNewPythonProcess = True + break + + frame = frame.f_back + frame = None # Just make sure we don't hold on to it. + + childProcess = getattr(os, original_name)() # fork + if not childProcess: + if isNewPythonProcess: + sys.settrace(None) + sys.setprofile(None) + _debugClient.sessionClose(False) + (wd, host, port, exceptions, tracePython, redirect, + noencoding, fork_auto, fork_child) = _debugClient.startOptions + _debugClient.startDebugger( + filename=sys.argv[0], + host=host, + port=port, + enableTrace=multiprocess and not isSubprocessFork, + exceptions=exceptions, + tracePython=tracePython, + redirect=redirect, + passive=False, + multiprocessSupport=multiprocess) + return childProcess + + return new_fork + + +def createCreateProcess(originalName): + """ + Function to patch the 'CreateProcess' process creation function of + Windows. + """ + def newCreateProcess(appName, cmdline, *args): + """ + Function replacing the 'CreateProcess' function of the _subprocess + or _winapi module. + """ + try: + import _subprocess + except ImportError: + import _winapi as _subprocess + return getattr(_subprocess, originalName)( + appName, patchArgumentStringWindows(_debugClient, cmdline), *args) + return newCreateProcess + + +# TODO: add 'createFork' def patchNewProcessFunctions(multiprocessEnabled, debugClient): @@ -84,7 +250,25 @@ patchModule(os, "execle", createExecl) patchModule(os, "execlp", createExecl) patchModule(os, "execlpe", createExecl) + patchModule(os, "execv", createExecv) + patchModule(os, "execve", createExecve) + patchModule(os, "execvp", createExecv) + patchModule(os, "execvpe", createExecve) # TODO: implement patching of the various functions of the os module + if isWindowsPlatform(): + try: + import _subprocess + except ImportError: + import _winapi as _subprocess + patchModule(_subprocess, 'CreateProcess', createCreateProcess) + else: + patchModule(os, "fork", createFork) + try: + import _posixsubprocess + patchModule(_posixsubprocess, "fork_exec", createForkExec) + except ImportError: + pass + _debugClient = debugClient
--- a/eric6/Debugger/DebuggerInterfacePython.py Mon Dec 07 19:53:15 2020 +0100 +++ b/eric6/Debugger/DebuggerInterfacePython.py Thu Dec 10 20:16:21 2020 +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 @@ -667,6 +666,7 @@ self.__sendJsonCommand("RequestEnvironment", {"environment": env}, self.__master) + # TODO: remove autoFork and forkChild def remoteLoad(self, fn, argv, wd, traceInterpreter=False, autoContinue=True, autoFork=False, forkChild=False, enableMultiprocess=False): @@ -708,6 +708,7 @@ "multiprocess": enableMultiprocess, }, self.__master) + # TODO: remove autoFork and forkChild def remoteRun(self, fn, argv, wd, autoFork=False, forkChild=False): """ Public method to load a new program to run. @@ -1290,31 +1291,6 @@ """ self.__sendJsonCommand("RequestUTStop", {}) - def __askForkTo(self, debuggerId): - """ - Private method to ask the user which branch of a fork to follow. - - @param debuggerId ID of the debugger backend - @type str - """ - 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 (Debugger: {0}).") - .format(debuggerId), - selections, - 0, False) - if not ok or res == selections[0]: - self.__sendJsonCommand("ResponseForkTo", { - "target": "parent", - }, debuggerId) - else: - self.__sendJsonCommand("ResponseForkTo", { - "target": "child", - }, debuggerId) - def __parseClientLine(self, sock): """ Private method to handle data from the client. @@ -1561,9 +1537,6 @@ elif method == "ResponseUTTestSucceededUnexpected": self.debugServer.clientUtTestSucceededUnexpected( params["testname"], params["id"]) - - elif method == "RequestForkTo": - self.__askForkTo(params["debuggerId"]) def __sendJsonCommand(self, command, params, debuggerId="", sock=None): """