Added some more process creation function overrides. multi_processing

Thu, 10 Dec 2020 20:16:21 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 10 Dec 2020 20:16:21 +0100
branch
multi_processing
changeset 7871
eb65864ca038
parent 7870
ab8f95bc7d2d
child 7872
433dacbfa456

Added some more process creation function overrides.

eric6/DebugClients/Python/DebugClientBase.py file | annotate | diff | comparison | revisions
eric6/DebugClients/Python/DebugUtilities.py file | annotate | diff | comparison | revisions
eric6/DebugClients/Python/MultiProcessDebugExtension.py file | annotate | diff | comparison | revisions
eric6/Debugger/DebuggerInterfacePython.py file | annotate | diff | comparison | revisions
--- 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):
         """

eric ide

mercurial