diff --git a/blender_cloud/async_loop.py b/blender_cloud/async_loop.py index 6feed3b..7db5f6e 100644 --- a/blender_cloud/async_loop.py +++ b/blender_cloud/async_loop.py @@ -87,6 +87,16 @@ def ensure_async_loop(): log.debug('Result of starting modal operator is %r', result) +def erase_async_loop(): + global _loop_kicking_operator_running + + log.debug('Erasing async loop') + + loop = asyncio.get_event_loop() + loop.stop() + _loop_kicking_operator_running = False + + class AsyncLoopModalOperator(bpy.types.Operator): bl_idname = 'asyncio.loop' bl_label = 'Runs the asyncio main loop' @@ -115,6 +125,12 @@ class AsyncLoopModalOperator(bpy.types.Operator): def modal(self, context, event): global _loop_kicking_operator_running + # If _loop_kicking_operator_running is set to False, someone called + # erase_async_loop(). This is a signal that we really should stop + # running. + if not _loop_kicking_operator_running: + return {'FINISHED'} + if event.type != 'TIMER': return {'PASS_THROUGH'} diff --git a/blender_cloud/settings_sync.py b/blender_cloud/settings_sync.py index 627ae36..0db5a02 100644 --- a/blender_cloud/settings_sync.py +++ b/blender_cloud/settings_sync.py @@ -123,19 +123,30 @@ class PILLAR_OT_sync(async_loop.AsyncModalOperatorMixin, bpy.types.Operator): self._state = 'QUIT' return - if self.action == 'PUSH': - await self.action_push() - else: - self.report({'ERROR'}, 'Sorry, PULL not implemented yet.') + action = { + 'PUSH': self.action_push, + 'PULL': self.action_pull, + }[self.action] + await action(context) except Exception as ex: self.log.exception('Unexpected exception caught.') self.report({'ERROR'}, 'Unexpected error: %s' % ex) - self._state = 'QUIT' + try: + self._state = 'QUIT' + except ReferenceError: + # This happens after the call to bpy.ops.wm.read_homefile() in action_pull(). + # That call erases the StructRNA of this operator. As a result, it no longer + # runs as a modal operator. The currently running Python code is allowed + # to finish, though. + pass - async def action_push(self): + async def action_push(self, context): """Sends files to the Pillar server.""" + self.log.info('Saved user preferences to disk before pushing to cloud.') + bpy.ops.wm.save_userpref() + config_dir = pathlib.Path(bpy.utils.user_resource('CONFIG')) for fname in SETTINGS_FILES_TO_UPLOAD: @@ -149,6 +160,29 @@ class PILLAR_OT_sync(async_loop.AsyncModalOperatorMixin, bpy.types.Operator): self.report({'INFO'}, 'Settings pushed to Blender Cloud.') + async def action_pull(self, context): + """Loads files from the Pillar server.""" + + # Refuse to start if the file hasn't been saved. + if context.blend_data.is_dirty: + self.report({'ERROR'}, 'Please save your Blend file before pulling' + ' settings from the Blender Cloud.') + return + + self.report({'WARNING'}, 'Settings pulled from Blender Cloud, reloading.') + + # This call is tricy, as Blender destroys this modal operator's StructRNA. + # However, the Python code keeps running, so we have to be very careful + # what we do afterwards. + log.warning('Reloading home files (i.e. userprefs and startup)') + bpy.ops.wm.read_homefile() + + # The above call stops any running modal operator, so we have to be + # very careful with our asynchronous loop. Since it didn't stop by + # its own accord (because the current async task is still running), + # we need to shut it down forcefully. + async_loop.erase_async_loop() + async def find_sync_group_id(self) -> pillarsdk.Node: """Finds the group node in which to store sync assets.