eric6/DebugClients/Python/MultiProcessDebugExtension.py

Sat, 03 Jul 2021 11:47:24 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 03 Jul 2021 11:47:24 +0200
branch
maintenance
changeset 8450
be7369a19dc9
parent 8243
cc717c2ae956
permissions
-rw-r--r--

Merged with default branch to prepare release 21.7.

# -*- coding: utf-8 -*-

# Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a function to patch the process creation functions to
support multiprocess debugging.
"""

import contextlib

from DebugUtilities import (
    patchArguments, patchArgumentStringWindows, isPythonProgram,
    isWindowsPlatform
)

_debugClient = None


def _shallPatch():
    """
    Function to determine, if the multiprocessing patches should be done.
    
    @return flag indicating patching should be performed
    @rtype bool
    """
    return _debugClient.debugging and _debugClient.multiprocessSupport


def patchModule(module, functionName, createFunction):
    """
    Function to replace a function of a module with a modified one.
    
    @param module reference to the module
    @type types.ModuleType
    @param functionName name of the function to be replaced
    @type str
    @param createFunction function creating the replacement
    @type types.FunctionType
    """
    if hasattr(module, functionName):
        originalName = 'original_' + functionName
        if not hasattr(module, originalName):
            setattr(module, originalName, getattr(module, functionName))
            setattr(module, functionName, createFunction(originalName))


def createExecl(originalName):
    """
    Function to patch the 'execl' process creation functions.
    
    <ul>
        <li>os.execl(path, arg0, arg1, ...)</li>
        <li>os.execle(path, arg0, arg1, ..., env)</li>
        <li>os.execlp(file, arg0, arg1, ...)</li>
        <li>os.execlpe(file, arg0, arg1, ..., env)</li>
    </ul>
    
    @param originalName original name of the function to be patched
    @type str
    @return function replacing the original one
    @rtype function
    """
    def newExecl(path, *args):
        """
        Function replacing the 'execl' functions of the os module.
        """
        import os
        if _shallPatch():
            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>
    
    @param originalName original name of the function to be patched
    @type str
    @return function replacing the original one
    @rtype function
    """
    def newExecv(path, args):
        """
        Function replacing the 'execv' functions of the os module.
        """
        import os
        if _shallPatch():
            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>
    
    @param originalName original name of the function to be patched
    @type str
    @return function replacing the original one
    @rtype function
    """
    def newExecve(path, args, env):
        """
        Function replacing the 'execve' functions of the os module.
        """
        import os
        if _shallPatch():
            args = patchArguments(_debugClient, args)
            if isPythonProgram(args[0]):
                path = args[0]
        return getattr(os, originalName)(path, args, env)
    return newExecve


def createSpawnl(originalName):
    """
    Function to patch the 'spawnl' process creation functions.
    
    <ul>
        <li>os.spawnl(mode, path, arg0, arg1, ...)</li>
        <li>os.spawnlp(mode, file, arg0, arg1, ...)</li>
    </ul>
    
    @param originalName original name of the function to be patched
    @type str
    @return function replacing the original one
    @rtype function
    """
    def newSpawnl(mode, path, *args):
        """
        Function replacing the 'spawnl' functions of the os module.
        """
        import os
        args = patchArguments(_debugClient, args)
        return getattr(os, originalName)(mode, path, *args)
    return newSpawnl


def createSpawnv(originalName):
    """
    Function to patch the 'spawnv' process creation functions.
    
    <ul>
        <li>os.spawnv(mode, path, args)</li>
        <li>os.spawnvp(mode, file, args)</li>
    </ul>
    
    @param originalName original name of the function to be patched
    @type str
    @return function replacing the original one
    @rtype function
    """
    def newSpawnv(mode, path, args):
        """
        Function replacing the 'spawnv' functions of the os module.
        """
        import os
        args = patchArguments(_debugClient, args)
        return getattr(os, originalName)(mode, path, args)
    return newSpawnv


def createSpawnve(originalName):
    """
    Function to patch the 'spawnve' process creation functions.
    
    <ul>
        <li>os.spawnve(mode, path, args, env)</li>
        <li>os.spawnvpe(mode, file, args, env)</li>
    </ul>
    
    @param originalName original name of the function to be patched
    @type str
    @return function replacing the original one
    @rtype function
    """
    def newSpawnve(mode, path, args, env):
        """
        Function replacing the 'spawnve' functions of the os module.
        """
        import os
        args = patchArguments(_debugClient, args)
        return getattr(os, originalName)(mode, path, args, env)
    return newSpawnve


def createPosixSpawn(originalName):
    """
    Function to patch the 'posix_spawn' process creation functions.
    
    <ul>
        <li>os.posix_spawn(path, argv, env, *, file_actions=None, ...
            (6 more))</li>
        <li>os.posix_spawnp(path, argv, env, *, file_actions=None, ...
            (6 more))</li>
    </ul>
    
    @param originalName original name of the function to be patched
    @type str
    @return function replacing the original one
    @rtype function
    """
    def newPosixSpawn(path, argv, env, **kwargs):
        """
        Function replacing the 'posix_spawn' functions of the os module.
        """
        import os
        argv = patchArguments(_debugClient, argv)
        return getattr(os, originalName)(path, argv, env, **kwargs)
    return newPosixSpawn


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>
    
    @param originalName original name of the function to be patched
    @type str
    @return function replacing the original one
    @rtype function
    """
    def newForkExec(args, *other_args):
        """
        Function replacing the 'fork_exec' functions of the _posixsubprocess
        module.
        """
        import _posixsubprocess
        if _shallPatch():
            args = patchArguments(_debugClient, args)
        return getattr(_posixsubprocess, originalName)(args, *other_args)
    return newForkExec


def createFork(originalName):
    """
    Function to patch the 'fork' process creation functions.
    
    <ul>
        <li>os.fork()</li>
    </ul>
    
    @param originalName original name of the function to be patched
    @type str
    @return function replacing the original one
    @rtype function
    """
    def newFork():
        """
        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 = _shallPatch()
        
        isSubprocessFork = False
        isMultiprocessingPopen = False
        while frame is not None:
            if frame.f_code.co_name == "_Popen":
                # fork() was called from multiprocessing; ignore this here
                # because it is handled in 'MultiprocessingExtension.py'.
                isMultiprocessingPopen = True
                break
            
            elif (
                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, originalName)()     # fork
        if (
            not childProcess and
            not isMultiprocessingPopen and
            isNewPythonProcess
        ):
            (wd, host, port, exceptions, tracePython, redirect,
             noencoding) = _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 newFork


def createCreateProcess(originalName):
    """
    Function to patch the 'CreateProcess' process creation function of
    Windows.
    
    @param originalName original name of the function to be patched
    @type str
    @return function replacing the original one
    @rtype function
    """
    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


def patchNewProcessFunctions(multiprocessEnabled, debugClient):
    """
    Function to patch the process creation functions to support multiprocess
    debugging.
    
    @param multiprocessEnabled flag indicating multiprocess support
    @type bool
    @param debugClient reference to the debug client object
    @type DebugClient
    """
    global _debugClient
    
    if not multiprocessEnabled:
        # return without patching
        return
    
    import os
    import sys
    
    # patch 'os.exec...()' functions
#-    patchModule(os, "execl", createExecl)
#-    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)
    
    # patch 'os.spawn...()' functions
    patchModule(os, "spawnl", createSpawnl)
    patchModule(os, "spawnle", createSpawnl)
    patchModule(os, "spawnlp", createSpawnl)
    patchModule(os, "spawnlpe", createSpawnl)
    patchModule(os, "spawnv", createSpawnv)
    patchModule(os, "spawnve", createSpawnve)
    patchModule(os, "spawnvp", createSpawnv)
    patchModule(os, "spawnvpe", createSpawnve)
    
    # patch 'os.posix_spawn...()' functions
    if sys.version_info >= (3, 8) and not isWindowsPlatform():
        patchModule(os, "posix_spawn", createPosixSpawn)
        patchModule(os, "posix_spawnp", createPosixSpawn)
    
    if isWindowsPlatform():
        try:
            import _subprocess
        except ImportError:
            import _winapi as _subprocess
        patchModule(_subprocess, 'CreateProcess', createCreateProcess)
    else:
        patchModule(os, "fork", createFork)
        with contextlib.suppress(ImportError):
            import _posixsubprocess
            patchModule(_posixsubprocess, "fork_exec", createForkExec)
    
    _debugClient = debugClient

eric ide

mercurial