1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing a function to patch the process creation functions to 8support multiprocess debugging. 9""" 10 11import contextlib 12 13from DebugUtilities import ( 14 patchArguments, patchArgumentStringWindows, isPythonProgram, 15 isWindowsPlatform 16) 17 18_debugClient = None 19 20 21def _shallPatch(): 22 """ 23 Function to determine, if the multiprocessing patches should be done. 24 25 @return flag indicating patching should be performed 26 @rtype bool 27 """ 28 return _debugClient.debugging and _debugClient.multiprocessSupport 29 30 31def patchModule(module, functionName, createFunction): 32 """ 33 Function to replace a function of a module with a modified one. 34 35 @param module reference to the module 36 @type types.ModuleType 37 @param functionName name of the function to be replaced 38 @type str 39 @param createFunction function creating the replacement 40 @type types.FunctionType 41 """ 42 if hasattr(module, functionName): 43 originalName = 'original_' + functionName 44 if not hasattr(module, originalName): 45 setattr(module, originalName, getattr(module, functionName)) 46 setattr(module, functionName, createFunction(originalName)) 47 48 49def createExecl(originalName): 50 """ 51 Function to patch the 'execl' process creation functions. 52 53 <ul> 54 <li>os.execl(path, arg0, arg1, ...)</li> 55 <li>os.execle(path, arg0, arg1, ..., env)</li> 56 <li>os.execlp(file, arg0, arg1, ...)</li> 57 <li>os.execlpe(file, arg0, arg1, ..., env)</li> 58 </ul> 59 60 @param originalName original name of the function to be patched 61 @type str 62 @return function replacing the original one 63 @rtype function 64 """ 65 def newExecl(path, *args): 66 """ 67 Function replacing the 'execl' functions of the os module. 68 """ 69 import os 70 if _shallPatch(): 71 args = patchArguments(_debugClient, args) 72 if isPythonProgram(args[0]): 73 path = args[0] 74 return getattr(os, originalName)(path, *args) 75 return newExecl 76 77 78def createExecv(originalName): 79 """ 80 Function to patch the 'execv' process creation functions. 81 82 <ul> 83 <li>os.execv(path, args)</li> 84 <li>os.execvp(file, args)</li> 85 </ul> 86 87 @param originalName original name of the function to be patched 88 @type str 89 @return function replacing the original one 90 @rtype function 91 """ 92 def newExecv(path, args): 93 """ 94 Function replacing the 'execv' functions of the os module. 95 """ 96 import os 97 if _shallPatch(): 98 args = patchArguments(_debugClient, args) 99 if isPythonProgram(args[0]): 100 path = args[0] 101 return getattr(os, originalName)(path, args) 102 return newExecv 103 104 105def createExecve(originalName): 106 """ 107 Function to patch the 'execve' process creation functions. 108 109 <ul> 110 <li>os.execve(path, args, env)</li> 111 <li>os.execvpe(file, args, env)</li> 112 </ul> 113 114 @param originalName original name of the function to be patched 115 @type str 116 @return function replacing the original one 117 @rtype function 118 """ 119 def newExecve(path, args, env): 120 """ 121 Function replacing the 'execve' functions of the os module. 122 """ 123 import os 124 if _shallPatch(): 125 args = patchArguments(_debugClient, args) 126 if isPythonProgram(args[0]): 127 path = args[0] 128 return getattr(os, originalName)(path, args, env) 129 return newExecve 130 131 132def createSpawnl(originalName): 133 """ 134 Function to patch the 'spawnl' process creation functions. 135 136 <ul> 137 <li>os.spawnl(mode, path, arg0, arg1, ...)</li> 138 <li>os.spawnlp(mode, file, arg0, arg1, ...)</li> 139 </ul> 140 141 @param originalName original name of the function to be patched 142 @type str 143 @return function replacing the original one 144 @rtype function 145 """ 146 def newSpawnl(mode, path, *args): 147 """ 148 Function replacing the 'spawnl' functions of the os module. 149 """ 150 import os 151 args = patchArguments(_debugClient, args) 152 return getattr(os, originalName)(mode, path, *args) 153 return newSpawnl 154 155 156def createSpawnv(originalName): 157 """ 158 Function to patch the 'spawnv' process creation functions. 159 160 <ul> 161 <li>os.spawnv(mode, path, args)</li> 162 <li>os.spawnvp(mode, file, args)</li> 163 </ul> 164 165 @param originalName original name of the function to be patched 166 @type str 167 @return function replacing the original one 168 @rtype function 169 """ 170 def newSpawnv(mode, path, args): 171 """ 172 Function replacing the 'spawnv' functions of the os module. 173 """ 174 import os 175 args = patchArguments(_debugClient, args) 176 return getattr(os, originalName)(mode, path, args) 177 return newSpawnv 178 179 180def createSpawnve(originalName): 181 """ 182 Function to patch the 'spawnve' process creation functions. 183 184 <ul> 185 <li>os.spawnve(mode, path, args, env)</li> 186 <li>os.spawnvpe(mode, file, args, env)</li> 187 </ul> 188 189 @param originalName original name of the function to be patched 190 @type str 191 @return function replacing the original one 192 @rtype function 193 """ 194 def newSpawnve(mode, path, args, env): 195 """ 196 Function replacing the 'spawnve' functions of the os module. 197 """ 198 import os 199 args = patchArguments(_debugClient, args) 200 return getattr(os, originalName)(mode, path, args, env) 201 return newSpawnve 202 203 204def createPosixSpawn(originalName): 205 """ 206 Function to patch the 'posix_spawn' process creation functions. 207 208 <ul> 209 <li>os.posix_spawn(path, argv, env, *, file_actions=None, ... 210 (6 more))</li> 211 <li>os.posix_spawnp(path, argv, env, *, file_actions=None, ... 212 (6 more))</li> 213 </ul> 214 215 @param originalName original name of the function to be patched 216 @type str 217 @return function replacing the original one 218 @rtype function 219 """ 220 def newPosixSpawn(path, argv, env, **kwargs): 221 """ 222 Function replacing the 'posix_spawn' functions of the os module. 223 """ 224 import os 225 argv = patchArguments(_debugClient, argv) 226 return getattr(os, originalName)(path, argv, env, **kwargs) 227 return newPosixSpawn 228 229 230def createForkExec(originalName): 231 """ 232 Function to patch the 'fork_exec' process creation functions. 233 234 <ul> 235 <li>_posixsubprocess.fork_exec(args, executable_list, close_fds, 236 ... (13 more))</li> 237 </ul> 238 239 @param originalName original name of the function to be patched 240 @type str 241 @return function replacing the original one 242 @rtype function 243 """ 244 def newForkExec(args, *other_args): 245 """ 246 Function replacing the 'fork_exec' functions of the _posixsubprocess 247 module. 248 """ 249 import _posixsubprocess 250 if _shallPatch(): 251 args = patchArguments(_debugClient, args) 252 return getattr(_posixsubprocess, originalName)(args, *other_args) 253 return newForkExec 254 255 256def createFork(originalName): 257 """ 258 Function to patch the 'fork' process creation functions. 259 260 <ul> 261 <li>os.fork()</li> 262 </ul> 263 264 @param originalName original name of the function to be patched 265 @type str 266 @return function replacing the original one 267 @rtype function 268 """ 269 def newFork(): 270 """ 271 Function replacing the 'fork' function of the os module. 272 """ 273 import os 274 import sys 275 276 # A simple fork will result in a new python process 277 isNewPythonProcess = True 278 frame = sys._getframe() 279 280 multiprocess = _shallPatch() 281 282 isSubprocessFork = False 283 isMultiprocessingPopen = False 284 while frame is not None: 285 if frame.f_code.co_name == "_Popen": 286 # fork() was called from multiprocessing; ignore this here 287 # because it is handled in 'MultiprocessingExtension.py'. 288 isMultiprocessingPopen = True 289 break 290 291 elif ( 292 frame.f_code.co_name == '_execute_child' and 293 'subprocess' in frame.f_code.co_filename 294 ): 295 isSubprocessFork = True 296 # If we're actually in subprocess.Popen creating a child, it 297 # may result in something which is not a Python process, (so, 298 # we don't want to connect with it in the forked version). 299 executable = frame.f_locals.get('executable') 300 if executable is not None: 301 isNewPythonProcess = False 302 if isPythonProgram(executable): 303 isNewPythonProcess = True 304 break 305 306 frame = frame.f_back 307 frame = None # Just make sure we don't hold on to it. 308 309 childProcess = getattr(os, originalName)() # fork 310 if ( 311 not childProcess and 312 not isMultiprocessingPopen and 313 isNewPythonProcess 314 ): 315 (wd, host, port, exceptions, tracePython, redirect, 316 noencoding) = _debugClient.startOptions 317 _debugClient.startDebugger( 318 filename=sys.argv[0], 319 host=host, 320 port=port, 321 enableTrace=multiprocess and not isSubprocessFork, 322 exceptions=exceptions, 323 tracePython=tracePython, 324 redirect=redirect, 325 passive=False, 326 multiprocessSupport=multiprocess) 327 return childProcess 328 329 return newFork 330 331 332def createCreateProcess(originalName): 333 """ 334 Function to patch the 'CreateProcess' process creation function of 335 Windows. 336 337 @param originalName original name of the function to be patched 338 @type str 339 @return function replacing the original one 340 @rtype function 341 """ 342 def newCreateProcess(appName, cmdline, *args): 343 """ 344 Function replacing the 'CreateProcess' function of the _subprocess 345 or _winapi module. 346 """ 347 try: 348 import _subprocess 349 except ImportError: 350 import _winapi as _subprocess 351 return getattr(_subprocess, originalName)( 352 appName, patchArgumentStringWindows(_debugClient, cmdline), *args) 353 return newCreateProcess 354 355 356def patchNewProcessFunctions(multiprocessEnabled, debugClient): 357 """ 358 Function to patch the process creation functions to support multiprocess 359 debugging. 360 361 @param multiprocessEnabled flag indicating multiprocess support 362 @type bool 363 @param debugClient reference to the debug client object 364 @type DebugClient 365 """ 366 global _debugClient 367 368 if not multiprocessEnabled: 369 # return without patching 370 return 371 372 import os 373 import sys 374 375 # patch 'os.exec...()' functions 376#- patchModule(os, "execl", createExecl) 377#- patchModule(os, "execle", createExecl) 378#- patchModule(os, "execlp", createExecl) 379#- patchModule(os, "execlpe", createExecl) 380#- patchModule(os, "execv", createExecv) 381#- patchModule(os, "execve", createExecve) 382#- patchModule(os, "execvp", createExecv) 383#- patchModule(os, "execvpe", createExecve) 384 385 # patch 'os.spawn...()' functions 386 patchModule(os, "spawnl", createSpawnl) 387 patchModule(os, "spawnle", createSpawnl) 388 patchModule(os, "spawnlp", createSpawnl) 389 patchModule(os, "spawnlpe", createSpawnl) 390 patchModule(os, "spawnv", createSpawnv) 391 patchModule(os, "spawnve", createSpawnve) 392 patchModule(os, "spawnvp", createSpawnv) 393 patchModule(os, "spawnvpe", createSpawnve) 394 395 # patch 'os.posix_spawn...()' functions 396 if sys.version_info >= (3, 8) and not isWindowsPlatform(): 397 patchModule(os, "posix_spawn", createPosixSpawn) 398 patchModule(os, "posix_spawnp", createPosixSpawn) 399 400 if isWindowsPlatform(): 401 try: 402 import _subprocess 403 except ImportError: 404 import _winapi as _subprocess 405 patchModule(_subprocess, 'CreateProcess', createCreateProcess) 406 else: 407 patchModule(os, "fork", createFork) 408 with contextlib.suppress(ImportError): 409 import _posixsubprocess 410 patchModule(_posixsubprocess, "fork_exec", createForkExec) 411 412 _debugClient = debugClient 413