diff -r 3fc8dfeb6ebe -r b99e7fd55fd3 src/eric7/DebugClients/Python/MultiProcessDebugExtension.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/DebugClients/Python/MultiProcessDebugExtension.py Thu Jul 07 11:23:56 2022 +0200 @@ -0,0 +1,412 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2002 - 2022 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