Wed, 30 Dec 2020 11:00:05 +0100
Updated copyright for 2021.
# -*- 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. """ 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: if 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) try: import _posixsubprocess patchModule(_posixsubprocess, "fork_exec", createForkExec) except ImportError: pass _debugClient = debugClient