#!/usr/bin/env python3 import threading from threading import Condition def is_main_thread(): return isinstance(threading.current_thread(), threading._MainThread) class MainThreadController(object): def __init__(self): if not is_main_thread(): raise Exception("A controller can only be created from the main thread") self._main_wake_up = Condition() self._main_wake_up.acquire() self._obj = None self._caller_wake_up = None self._args = None self._kwargs = None self._return = None self._quit = False def invoke(self, obj, *args, **kwargs): if is_main_thread(): return obj.__call__(*args, **kwargs) released = False self._main_wake_up.acquire() try: self._caller_wake_up = Condition() with self._caller_wake_up: self._obj = obj self._args = args self._kwargs = kwargs # tell the main thread to wake up self._main_wake_up.notify_all() self._main_wake_up.release() released = True self._caller_wake_up.wait() # tell the main thread that we received the result: self._caller_wake_up.notify_all() ret = self._return return ret finally: self._obj = None self._args = None self._kwargs = None self._caller_wake_up = None self._return = None if not released: self._main_wake_up.release() def quit(self): self._main_wake_up.acquire() try: self._quit = True self._main_wake_up.notify_all() finally: self._main_wake_up.release() def run(self): if not is_main_thread(): raise Exception("run can only be called from the main thread!") from . import signals def signal_handler(signal, frame): self._quit = True signals.add_sigint_handler(signal_handler) while True: try: self._main_wake_up.wait(1.0) except KeyboardInterrupt: self._quit = True if self._quit: return elif self._caller_wake_up is None: # we timed out continue with self._caller_wake_up: self._return = self._obj.__call__(*self._args, **self._kwargs) self._caller_wake_up.notify_all() # wait for the calling thread to confirm it received the result while not self._caller_wake_up.wait(1.0): if self._quit: return class MainThreadWrapper(object): def __init__(self, mainobj, controller): self._main = mainobj self._controller = controller def __call__(self, *args, **kwargs): ret = self._controller.invoke(self._main, *args, **kwargs) if id(self._main) == id(ret): return MainThreadWrapper(ret, self._controller) else: return ret def __getattribute__(self, name): if name == '_main' or name == '_controller': return object.__getattribute__(self, name) elif isinstance(getattr(type(self._main), name), property): return getattr(self._main, name) else: return MainThreadWrapper(getattr(self._main, name), self._controller) if __name__ == '__main__': class MainThreadOnlyClass(object): def do_stuff(self): if not is_main_thread(): raise Exception("ERROR!") return 1337 main_thread_only = MainThreadOnlyClass() controller = MainThreadController() def dostuff(mtoc): print(mtoc.do_stuff()) from threading import Thread thread = Thread(target = dostuff, args = (MainThreadWrapper(main_thread_only, controller),)) thread.start() controller.run() thread.join()