Fri, 07 Oct 2016 22:50:48 +0200
Insert import hook to modify thread module to use our bootstrap.
DebugClients/Python/DebugBase.py | file | annotate | diff | comparison | revisions | |
DebugClients/Python/ThreadExtension.py | file | annotate | diff | comparison | revisions |
--- a/DebugClients/Python/DebugBase.py Thu Oct 06 22:51:04 2016 +0200 +++ b/DebugClients/Python/DebugBase.py Fri Oct 07 22:50:48 2016 +0200 @@ -74,7 +74,8 @@ """ self._dbgClient = dbgClient - self._mainThread = True + # Some informations about the thread + self.isMainThread = False self.quitting = False self.id = -1 self.name = '' @@ -321,10 +322,12 @@ if self.stopframe and frame.f_code.co_flags & CO_GENERATOR: return # The program has finished if we have just left the first frame - if (frame == self.botframe and - self._mainThread): - atexit._run_exitfuncs() - self._dbgClient.progTerminated(arg) + if frame == self.botframe: + if self.isMainThread: + atexit._run_exitfuncs() + self._dbgClient.progTerminated(arg) + else: + self._dbgClient.threadTerminated(self.id) if self.quitting and not self._dbgClient.passive: raise SystemExit @@ -384,8 +387,9 @@ frame.f_trace = self.trace_dispatch while frame is not None: - # stop at erics debugger frame - if frame.f_back.f_code == stopOnHandleLine: + # stop at erics debugger frame or the threading bootstrap + if (frame.f_back.f_code == stopOnHandleLine or + frame.f_back.f_code.co_name == bootstrap): frame.f_trace = self.trace_dispatch self.botframe = frame break @@ -396,24 +400,37 @@ sys.settrace(self.trace_dispatch) sys.setprofile(self._dbgClient.callTraceEnabled) - def bootstrap(self): + def bootstrap(self, target, args, kwargs): """ - Public method to bootstrap the thread. + Public method to bootstrap a thread. It wraps the call to the user function to enable tracing before hand. + + @param target function which is called in the new created thread + @type function pointer + @param args arguments to pass to target + @type tuple + @param kwargs keyword arguments to pass to target + @type dict """ try: - self._threadRunning = True - self.traceThread() - self._target(*self._args, **self._kwargs) + frame = sys._getframe() + self.botframe = frame + # First time the dispach function is called, a "base debug" + # function has to be returned, which is called at every user code + # function call. Because of setting botframe manually, the + # specific branch returning the trace_dispatch itself is skipped. + # Because the next step is always in threading.py and we assume + # that there is no breakpoint in it, it's save to return + # trace_dispatch unconditionally. + sys.settrace(lambda frame, event, arg: self.trace_dispatch) + frame.f_trace = self.trace_dispatch + + target(*args, **kwargs) except SystemExit: pass finally: - self._threadRunning = False - self.quitting = True - self._dbgClient.threadTerminated(self) - sys.settrace(None) sys.setprofile(None) def run(self, cmd, globals=None, locals=None):
--- a/DebugClients/Python/ThreadExtension.py Thu Oct 06 22:51:04 2016 +0200 +++ b/DebugClients/Python/ThreadExtension.py Fri Oct 07 22:50:48 2016 +0200 @@ -32,6 +32,7 @@ Constructor """ self.threadNumber = 1 + self.enableImportHooks = True self._original_start_new_thread = None self.clientLock = threading.RLock() @@ -54,6 +55,8 @@ del sys.modules[self.threadModName] del sys.modules['threading'] + sys.meta_path.insert(0, self) + # provide a hook to perform a hard breakpoint # Use it like this: # if hasattr(sys, 'breakpoint): sys.breakpoint() @@ -79,7 +82,7 @@ ident = _thread.get_ident() name = 'MainThread' newThread = self.mainThread - newThread._mainThread = True + newThread.isMainThread = True if self.debugging: sys.setprofile(newThread.profile) @@ -97,15 +100,16 @@ return ident - def threadTerminated(self, dbgThread): + def threadTerminated(self, threadId): """ Public method called when a DebugThread has exited. - @param dbgThread the DebugThread that has exited + @param threadId id of the DebugThread that has exited + @type int """ self.lockClient() try: - del self.threads[dbgThread.get_ident()] + del self.threads[threadId] except KeyError: pass finally: @@ -142,10 +146,10 @@ """ try: self.lockClient() - if id is None or id not in self.threads: + if id is None: self.currentThread = None else: - self.currentThread = self.threads[id] + self.currentThread = self.threads.get(id) finally: self.unlockClient() @@ -180,6 +184,46 @@ "currentID": currentId, "threadList": threadList, }) + + def find_module(self, fullname, path=None): + """ + Public method returning the module loader. + + @param fullname name of the module to be loaded + @type str + @param path path to resolve the module name + @type str + @return module loader object + @rtype object + """ + if fullname in sys.modules or not self.debugging: + return None + if fullname == self.threadModName and self.enableImportHooks: + # Disable hook to be able to import original module + self.enableImportHooks = False + return self + + return None + + def load_module(self, fullname): + """ + Public method to load a module. + + @param fullname name of the module to be loaded + @type str + @return reference to the loaded module + @rtype module + """ + module = __import__(fullname) + sys.modules[fullname] = module + if (fullname == self.threadModName and + self._original_start_new_thread is None): + # make thread hooks available to system + self._original_start_new_thread = module.start_new_thread + module.start_new_thread = self.attachThread + + self.enableImportHooks = True + return module #