Asyncio loop kicked via modal operator.

The old way (using a scene_update_pre handler) turned out to work due to
a bug in Blender, where scene_update_pre was called too frequently.
This commit is contained in:
Sybren A. Stüvel 2016-03-23 13:45:28 +01:00
parent ed9821afa6
commit 09e9c02d65
2 changed files with 68 additions and 30 deletions

View File

@ -77,14 +77,16 @@ def register():
from . import blender, gui, async_loop from . import blender, gui, async_loop
async_loop.setup_asyncio_executor() async_loop.setup_asyncio_executor()
async_loop.register()
blender.register() blender.register()
gui.register() gui.register()
def unregister(): def unregister():
from . import blender, gui from . import blender, gui, async_loop
gui.unregister() gui.unregister()
blender.unregister() blender.unregister()
async_loop.unregister()

View File

@ -9,6 +9,9 @@ import bpy
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Keeps track of whether a loop-kicking operator is already running.
_loop_kicking_operator_running = False
def setup_asyncio_executor(): def setup_asyncio_executor():
"""Sets up AsyncIO to run on a single thread. """Sets up AsyncIO to run on a single thread.
@ -24,16 +27,21 @@ def setup_asyncio_executor():
# loop.set_debug(True) # loop.set_debug(True)
def kick_async_loop(*args): def kick_async_loop(*args) -> bool:
"""Performs a single iteration of the asyncio event loop.
:return: whether the asyncio loop should stop after this kick.
"""
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
# We always need to do one more 'kick' to handle task-done callbacks. # Even when we want to stop, we always need to do one more
# 'kick' to handle task-done callbacks.
stop_after_this_kick = False stop_after_this_kick = False
if loop.is_closed(): if loop.is_closed():
log.warning('loop closed, stopping immediately.') log.warning('loop closed, stopping immediately.')
stop_async_loop() return True
return
all_tasks = asyncio.Task.all_tasks() all_tasks = asyncio.Task.all_tasks()
if not len(all_tasks): if not len(all_tasks):
@ -63,33 +71,61 @@ def kick_async_loop(*args):
loop.stop() loop.stop()
loop.run_forever() loop.run_forever()
if stop_after_this_kick: return stop_after_this_kick
stop_async_loop()
def async_loop_handler() -> callable:
"""Returns the asynchronous loop handler `kick_async_loop`
Only returns the function if it is installed as scene_update_pre handler, otherwise
it returns None.
"""
name = kick_async_loop.__name__
for handler in bpy.app.handlers.scene_update_pre:
if getattr(handler, '__name__', '') == name:
return handler
return None
def ensure_async_loop(): def ensure_async_loop():
if async_loop_handler() is not None: log.debug('Starting asyncio loop')
return result = bpy.ops.asyncio.loop()
bpy.app.handlers.scene_update_pre.append(kick_async_loop) log.debug('Result of starting modal operator is %r', result)
def stop_async_loop(): class AsyncLoopModalOperator(bpy.types.Operator):
handler = async_loop_handler() bl_idname = 'asyncio.loop'
if handler is None: bl_label = 'Runs the asyncio main loop'
return
bpy.app.handlers.scene_update_pre.remove(handler) timer = None
log.debug('stopped async loop.') log = logging.getLogger(__name__ + '.AsyncLoopModalOperator')
def execute(self, context):
return self.invoke(context, None)
def invoke(self, context, event):
global _loop_kicking_operator_running
if _loop_kicking_operator_running:
self.log.debug('Another loop-kicking operator is already running.')
return {'PASS_THROUGH'}
context.window_manager.modal_handler_add(self)
_loop_kicking_operator_running = True
wm = context.window_manager
self.timer = wm.event_timer_add(0.00001, context.window)
return {'RUNNING_MODAL'}
def modal(self, context, event):
global _loop_kicking_operator_running
if event.type != 'TIMER':
return {'PASS_THROUGH'}
# self.log.debug('KICKING LOOP')
stop_after_this_kick = kick_async_loop()
if stop_after_this_kick:
context.window_manager.event_timer_remove(self.timer)
_loop_kicking_operator_running = False
self.log.debug('Stopped asyncio loop kicking')
return {'FINISHED'}
return {'RUNNING_MODAL'}
def register():
bpy.utils.register_class(AsyncLoopModalOperator)
def unregister():
bpy.utils.unregister_class(AsyncLoopModalOperator)