Hey all,
so I found a way to make it work both for houdini 19.5 and Houdini 20 and above.
You can control this with the
CREATE_THREAD variable. In 19.5 the event loop needs to run in a separate thread otherwise it's blocking the UI.
The scripts tries to include all sorts of asyncio stuff, like endless coroutines, async generators, executing a last coroutine before houdini shuts down as well as gracefully ending the loop.
Save this in a
pythonrc.py file and include it in HOUDINI_PATH so the loop gets started when houdini boots up.
Note: sheduling a coroutine in the atexit event leads to seg fault in houdini 20 and above.
import threading
import asyncio
import atexit
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("hou-asyncio")
logger.setLevel(logging.DEBUG)
print("Hello from pythonrc")
CREATE_THREAD = False
def get_event_loop() -> asyncio.AbstractEventLoop:
try:
loop = asyncio.get_running_loop()
logger.info("Take existing event loop: %s", loop)
except RuntimeError:
loop = asyncio.new_event_loop()
logger.info("Created new event loop: %s", loop)
return loop
async def async_start_heartbeat():
while True:
logger.info("Heartbeat")
await asyncio.sleep(1.5)
async def async_generator():
number = 0
while True:
yield number
number += 1
await asyncio.sleep(1)
async def async_use_async_generator():
async for number in async_generator():
logger.info("Received number: %i", number)
async def async_cancel_all_tasks(loop: asyncio.AbstractEventLoop):
tasks = [
t
for t in asyncio.all_tasks(loop=loop)
if t is not asyncio.current_task(loop=loop)
]
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
async def async_shutdown_tasks(loop: asyncio.AbstractEventLoop):
logger.info("Cancelling all tasks...")
await async_cancel_all_tasks(loop)
logger.info("Shutting down async generators...")
await loop.shutdown_asyncgens()
async def async_last_coroutine():
logger.info("Houdini about to exit, executing last coroutine...")
for i in range(3):
logger.info("Last couroutine done in: %i", 3 - i)
await asyncio.sleep(1)
logger.info("Last coroutine done")
def on_exit(loop: asyncio.AbstractEventLoop, thread: threading.Thread = None):
logger.info("Detected houdini exit event")
future = asyncio.run_coroutine_threadsafe(async_last_coroutine(), loop)
future.result()
future = asyncio.run_coroutine_threadsafe(async_shutdown_tasks(loop), loop)
future.result()
logger.info("Stopping event loop...")
loop.call_soon_threadsafe(loop.stop)
logger.info("Closing event loop...")
loop.call_soon_threadsafe(loop.close)
if thread:
thread.join()
logger.info("Joined thread: %s", thread.native_id)
def _start_loop_target(loop: asyncio.AbstractEventLoop):
asyncio.set_event_loop(loop)
loop.run_forever()
def main():
loop = get_event_loop()
asyncio.set_event_loop(loop)
# No need to start the event loop in Houdini?
thread = None
if not loop.is_running():
if CREATE_THREAD:
thread = threading.Thread(target=_start_loop_target, args=(loop,))
thread.start()
logger.info("Starting event loop: %s in thread: %s", loop, thread.native_id)
else:
logger.info("Starting event loop: %s", loop)
loop.run_forever()
logger.info("Scheduling main coroutines...")
asyncio.run_coroutine_threadsafe(async_start_heartbeat(), loop)
asyncio.run_coroutine_threadsafe(async_use_async_generator(), loop)
atexit.register(on_exit, loop, thread)
logger.info("Registered atexit callback")
if __name__ == "__main__":
main()