|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2020 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing an import hook patching modules to support debugging. |
|
8 """ |
|
9 |
|
10 import sys |
|
11 import importlib |
|
12 |
|
13 from QProcessExtension import patchQProcess |
|
14 from SubprocessExtension import patchSubprocess |
|
15 from MultiprocessingExtension import patchMultiprocessing |
|
16 |
|
17 |
|
18 class ModuleLoader: |
|
19 """ |
|
20 Class implementing an import hook patching modules to support debugging. |
|
21 """ |
|
22 |
|
23 def __init__(self, debugClient): |
|
24 """ |
|
25 Constructor |
|
26 |
|
27 @param debugClient reference to the debug client object |
|
28 @type DebugClient |
|
29 """ |
|
30 self.__dbgClient = debugClient |
|
31 |
|
32 self.__enableImportHooks = set() |
|
33 |
|
34 # reset already imported thread module to apply hooks at next import |
|
35 for moduleName in ("thread", "_thread", "threading"): |
|
36 if moduleName in sys.modules: |
|
37 del sys.modules[moduleName] |
|
38 |
|
39 self.__modulesToPatch = ( |
|
40 "_thread", |
|
41 "threading", |
|
42 "greenlet", |
|
43 "subprocess", |
|
44 "multiprocessing", |
|
45 "PyQt5.QtCore", |
|
46 "PyQt6.QtCore", |
|
47 "PySide2.QtCore", |
|
48 "PySide6.QtCore", |
|
49 ) |
|
50 |
|
51 sys.meta_path.insert(0, self) |
|
52 |
|
53 def __loadModule(self, fullname): |
|
54 """ |
|
55 Private method to load a module. |
|
56 |
|
57 @param fullname name of the module to be loaded |
|
58 @type str |
|
59 @return reference to the loaded module |
|
60 @rtype module |
|
61 """ |
|
62 module = importlib.import_module(fullname) |
|
63 sys.modules[fullname] = module |
|
64 self.__enableImportHooks.remove(fullname) |
|
65 ## Add hook for _thread.start_new_thread |
|
66 if fullname == "_thread" and not hasattr(module, "eric7_patched"): |
|
67 module.eric7_patched = True |
|
68 self.__dbgClient.patchPyThread(module) |
|
69 |
|
70 ## Add hook for threading.run() |
|
71 elif fullname == "threading" and not hasattr(module, "eric7_patched"): |
|
72 module.eric7_patched = True |
|
73 self.__dbgClient.patchPyThreading(module) |
|
74 |
|
75 ## greenlet support |
|
76 elif fullname == "greenlet" and not hasattr(module, "eric7_patched"): |
|
77 if self.__dbgClient.patchGreenlet(module): |
|
78 module.eric7_patched = True |
|
79 |
|
80 ## Add hook for subprocess.Popen() |
|
81 elif fullname == "subprocess" and not hasattr(module, "eric7_patched"): |
|
82 module.eric7_patched = True |
|
83 patchSubprocess(module, self.__dbgClient) |
|
84 |
|
85 ## Add hook for multiprocessing.Process |
|
86 elif fullname == "multiprocessing" and not hasattr(module, "eric7_patched"): |
|
87 module.eric7_patched = True |
|
88 patchMultiprocessing(module, self.__dbgClient) |
|
89 |
|
90 ## Add hook for *.QThread and *.QProcess |
|
91 elif fullname in ( |
|
92 "PyQt5.QtCore", |
|
93 "PyQt6.QtCore", |
|
94 "PySide2.QtCore", |
|
95 "PySide6.QtCore", |
|
96 ) and not hasattr(module, "eric7_patched"): |
|
97 module.eric7_patched = True |
|
98 self.__dbgClient.patchQThread(module) |
|
99 patchQProcess(module, self.__dbgClient) |
|
100 |
|
101 return module |
|
102 |
|
103 def find_spec(self, fullname, path, target=None): |
|
104 """ |
|
105 Public method returning the module spec. |
|
106 |
|
107 @param fullname name of the module to be loaded |
|
108 @type str |
|
109 @param path path to resolve the module name |
|
110 @type str |
|
111 @param target module object to use for a more educated guess |
|
112 about what spec to return |
|
113 @type module |
|
114 @return module spec object pointing to the module loader |
|
115 @rtype ModuleSpec |
|
116 """ |
|
117 if fullname in sys.modules or self.__dbgClient.debugging is False: |
|
118 return None |
|
119 |
|
120 if ( |
|
121 fullname in self.__modulesToPatch |
|
122 and fullname not in self.__enableImportHooks |
|
123 ): |
|
124 # Disable hook to be able to import original module |
|
125 self.__enableImportHooks.add(fullname) |
|
126 return importlib.machinery.ModuleSpec(fullname, self) |
|
127 |
|
128 return None |
|
129 |
|
130 def create_module(self, spec): |
|
131 """ |
|
132 Public method to create a module based on the passed in spec. |
|
133 |
|
134 @param spec module spec object for loading the module |
|
135 @type ModuleSpec |
|
136 @return created and patched module |
|
137 @rtype module |
|
138 """ |
|
139 return self.__loadModule(spec.name) |
|
140 |
|
141 def exec_module(self, module): |
|
142 """ |
|
143 Public method to execute the created module. |
|
144 |
|
145 @param module module to be executed |
|
146 @type module |
|
147 """ |
|
148 pass |