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