Search code examples
pythonblenderreload

How to survive revert_mainfile() in memory?


I'm trying to reload the blend file inside the loop in my script. Modal operator doesn't work, because after scene reload the operator's instance dies.

So, I create my own class, which has my loop in generator, which yields after calling revert_mainfile(), and returning control to it from blender's callbacks (chained load_post, scene_update_pre).

Once it fires ok, but on the 2nd iteration it closes blender with AV.

What am I doing wrong?

Here is the code:

"""
    The script shows how to have the iterator with Operator.modal()
    iterating with stored generator function to be able to reload blend file
    and wait for blender to load the scene asynchronously with callback to have
    a valid context to execute the next iteration.
    
    see also: https://blender.stackexchange.com/questions/17960/calling-delete-operator-after-revert-mainfile
    
    Sadly, a modal operator doesn't survive the scene reload, so use stored runner
    instance, getting control back with callbacks.
"""

import bpy
#from bpy.props import IntProperty, FloatProperty
import time
from bpy.app.handlers import persistent

ITERATIONS = 3

my_start_time = 0.0    # deb for my_report()

@persistent
def my_report(mess):
    print ("deb@ {:.3f} > {}".format(time.time() - my_start_time, mess))


@persistent
def scene_update_callback(scene):
    """ this fires after actual scene reloading """
    my_report("scene_update_callback start")
    # self-removal, only run once
    bpy.app.handlers.scene_update_pre.remove(scene_update_callback)
    # register that we have updated the scene
    MyShadowOperator.scene_loaded = True
    # emulate event for modal()
    MyShadowOperator.instance.modal()
    my_report("scene_update_callback finish")
    pass
    # after this, it seems we have nowhere to return, and an AV occurs :-(

    
        
@persistent
def load_post_callback(dummy):
    """ it's called immediately after revert_mainfile() before any actual loading """
    my_report("load_post_callback")
    # self-removal, so it isn't called again
    bpy.app.handlers.load_post.remove(load_post_callback)
    # use a scene update handler to delay execution of code
    bpy.app.handlers.scene_update_pre.append(scene_update_callback)

class MyShadowOperator():
    """ This must survive after the file reload. """
    
    instance = None
    scene_loaded = False
    
    def __init__(self, operator):
        __class__.instance = self
        # store all of the original operator parameters (in case of any)
        self.params = operator.as_keywords()
        # create the generator (no code from it is executing here)
        self.generator = self.processor()
    
    def modal(self):
        """ here we are on each iteration """
        my_report("Shadow modal start")
        return next(self.generator)

    def processor(self):  #, context
        """ this is a generator function to process chunks.
            returns {'FINISHED'} or {'RUNNING_MODAL'} """
        my_report("processor init")
        for i in range(ITERATIONS):
            my_report(f"processor iteration step {i}")
            # here we reload the main blend file
            # first registering the callback
            bpy.app.handlers.load_post.append(load_post_callback)
            __class__.scene_loaded = False
            my_report("processor reloading the file")
            bpy.ops.wm.revert_mainfile()
            # the scene is already loaded
            my_report("processor after reloading the file")
            # yield something which will not confuse ops.__call__()          
            if i == ITERATIONS-1:
                yield {} #'FINISHED'
            else:
                yield {} #'RUNNING_MODAL'
        return
    
class ShadowOperatorRunner(bpy.types.Operator):
    """Few times reload original file, and create an object. """
    bl_idname = "object.shadow_operator_runner"
    bl_label = "Simple Modal Operator"

    def execute(self, context):
        """ Here we start from the UI """
        global my_start_time
        my_start_time = time.time() # deb
        
        my_report("runner execute() start")
        
        # create and init a shadow operator
        shadow = MyShadowOperator(self)
        
        # start the 1st iteration
        shadow.modal()
        
        # and just leave and hope the next execution in shadow via the callback
        my_report("runner execute() return")
        return {'FINISHED'} # no sense to keep it modal because it dies anyway
       

def register():
    bpy.utils.register_class(ShadowOperatorRunner)


def unregister():
    bpy.utils.unregister_class(ShadowOperatorRunner)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.object.shadow_operator_runner('EXEC_DEFAULT')

And this is console output:

deb@ 0.000 > runner execute() start
deb@ 0.001 > Shadow modal start
deb@ 0.001 > processor init
deb@ 0.002 > processor iteration step 0
deb@ 0.003 > processor reloading the file
Read blend: D:\mch\MyDocuments\scripts\blender\modal_operator_reloading_blend\da
ta\runner_modal_operator.blend
deb@ 0.026 > load_post_callback
deb@ 0.027 > processor after reloading the file
deb@ 0.028 > runner execute() return
deb@ 0.031 > scene_update_callback start
deb@ 0.031 > Shadow modal start
deb@ 0.033 > processor iteration step 1
deb@ 0.034 > processor reloading the file
Read blend: D:\mch\MyDocuments\scripts\blender\modal_operator_reloading_blend\da
ta\runner_modal_operator.blend
deb@ 0.057 > load_post_callback
deb@ 0.060 > processor after reloading the file
deb@ 0.060 > scene_update_callback finish
Error   : EXCEPTION_ACCESS_VIOLATION
Address : 0x00007FF658DDC2F7
Module  : D:\Program files\Blender Foundation\blender-2.79.0\blender.exe

Blender version:

Blender 2.79 (sub 7)
        build date: 28/07/2019
        build time: 17:48
        build commit date: 2019-06-27
        build commit time: 10:41
        build hash: e045fe53f1b0
        build platform: Windows
        build type: Release

After the last line of scene_update_callback() there is nothing to return to (if I watch the call stack in the debugger), it just closes blender with the Access Violation error.


Solution

  • After fiddling a lot I ended up with a modal operator which stays in memory as long as it can on file reloading. After that I just restart the operator instance with the stored context passed to it.

    That's the code, if someone is interested:

    import bpy
    import time
    from bpy.app.handlers import persistent
    from bpy.props import BoolProperty
    
    ITERATIONS = 3
    
    my_start_time = 0.0    # deb for my_report()
    
    @persistent
    def my_report(mess):
        print ("deb@ {:.3f} > {}".format(time.time() - my_start_time, mess))
    
    
    @persistent
    def scene_update_callback(scene):
        """ this fires after actual scene reloading """
        my_report("scene_update_callback start")
        # self-removal, only run once
        bpy.app.handlers.scene_update_post.remove(scene_update_callback)
        
        # set the flag to signal warm start, and use stored parameters to restart the zombie.
        bpy.ops.object.zombie_operator('EXEC_DEFAULT', warm_restart=True, **ZombieOperator._params)
        
        my_report("scene_update_callback finish")
    
    @persistent
    def load_post_callback(dummy):
        """ it's called immediately after revert_mainfile() before any actual loading """
        my_report("load_post_callback start")
        # self-removal, so it isn't called again
        bpy.app.handlers.load_post.remove(load_post_callback)
        # use a scene update handler to delay execution of code
        bpy.app.handlers.scene_update_post.append(scene_update_callback)
        my_report("load_post_callback finish")
    
    
    
    class ZombieOperator(bpy.types.Operator):
        """Operator which restrts its self periodically after the blend file reload."""
        bl_idname = "object.zombie_operator"
        bl_label = "Zombie resurrecting operator"
    
        # these must survive the instance death.
        # The class survives the file reload, so here it's safe to keep my execution context
        _params = None
        _generetor = None
        
        _timer = None
        
        # the flag to indicate the warm restart
        warm_restart = BoolProperty(
            name="warm_restart",
            default=False,
        )
    
        def step(self):
            """ here we are on each iteration """
            my_report("step()")
            return next(__class__._generator)
    
        def processor(self):  #, context
            """ this is a generator function to process chunks.
                returns {'FINISHED'} or {'RUNNING_MODAL'} """
            my_report("processor init")
            
            for i in range(ITERATIONS):
                my_report(f"processor iteration step {i}")
                # if not last iteration we prepare the next step
                _to_continue = (i < ITERATIONS-1)
                if _to_continue:
                    # here we reload the main blend file
                    # first registering the callbacks
                    bpy.app.handlers.load_post.append(load_post_callback)
                    my_report("processor reloading the file")
                    bpy.ops.wm.revert_mainfile()
                    my_report("processor after reloading the file")
                    yield {'RUNNING_MODAL'}
                else:
                    yield {'FINISHED'}
     
        def modal(self, context, event):
            # we run actual processing step on first timer event
            if event.type == 'TIMER':
                my_report("modal() start")
                # disable timer
                wm = context.window_manager
                wm.event_timer_remove(self._timer)
                _ret = self.step()
                my_report(f"modal() finish ({_ret})")
                return _ret
            return {'PASS_THROUGH'} 
    
        def execute(self, context):
            global my_start_time
            my_start_time = time.time() # deb
            # handle possible warm restart
            my_report(f"execute()   warm_restart = {self.warm_restart}")
            if not self.warm_restart:
                # store everything in a safe place
                # store actual parameters
                __class__._params = self.as_keywords(ignore=('warm_restart',))
                # create the generator
                __class__._generator = self.processor()
                
            # and reset it
            self.warm_restart = False
            # set a timer to ensure an event for my modal() handler 
            wm = context.window_manager
            self._timer = wm.event_timer_add(0.5, context.window)
            wm.modal_handler_add(self)
            my_report(f"execute() finish")
            return {'RUNNING_MODAL'}
    
        def cancel(self, context):
            my_report("on cancel. Instance is dead.")
    
    
    def register():
        bpy.utils.register_class(ZombieOperator)
    
    
    def unregister():
        bpy.utils.unregister_class(ZombieOperator)
    
    
    if __name__ == "__main__":
        register()
    
        # test call
        bpy.ops.object.zombie_operator('EXEC_DEFAULT')
    

    and here is the output:

    deb@ 0.001 > execute()   warm_restart = False
    deb@ 0.003 > execute() finish
    deb@ 0.507 > modal() start
    deb@ 0.507 > step()
    deb@ 0.507 > processor init
    deb@ 0.507 > processor iteration step 0
    deb@ 0.508 > processor reloading the file
    deb@ 0.510 > on cancel. Instance is dead.
    Read blend: D:\mch\MyDocuments\scripts\blender\modal_operator_reloading_blend\da
    ta\runner_zombie_operator.blend
    deb@ 0.527 > load_post_callback start
    deb@ 0.527 > load_post_callback finish
    deb@ 0.528 > processor after reloading the file
    deb@ 0.529 > modal() finish ({'RUNNING_MODAL'})
    deb@ 0.531 > scene_update_callback start
    deb@ 0.000 > execute()   warm_restart = True
    deb@ 0.001 > execute() finish
    deb@ 0.001 > scene_update_callback finish
    deb@ 0.505 > modal() start
    deb@ 0.505 > step()
    deb@ 0.506 > processor iteration step 1
    deb@ 0.506 > processor reloading the file
    deb@ 0.508 > on cancel. Instance is dead.
    Read blend: D:\mch\MyDocuments\scripts\blender\modal_operator_reloading_blend\da
    ta\runner_zombie_operator.blend
    deb@ 0.524 > load_post_callback start
    deb@ 0.525 > load_post_callback finish
    deb@ 0.526 > processor after reloading the file
    deb@ 0.526 > modal() finish ({'RUNNING_MODAL'})
    deb@ 0.527 > scene_update_callback start
    deb@ 0.000 > execute()   warm_restart = True
    deb@ 0.001 > execute() finish
    deb@ 0.001 > scene_update_callback finish
    deb@ 0.505 > modal() start
    deb@ 0.506 > step()
    deb@ 0.506 > processor iteration step 2
    deb@ 0.506 > modal() finish ({'FINISHED'})