How to survive revert_mainfile() in memory?

Question:

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:mchMyDocumentsscriptsblendermodal_operator_reloading_blendda
tarunner_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:mchMyDocumentsscriptsblendermodal_operator_reloading_blendda
tarunner_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 filesBlender Foundationblender-2.79.0blender.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.

Asked By: Mechanic

||

Answers:

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:mchMyDocumentsscriptsblendermodal_operator_reloading_blendda
tarunner_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:mchMyDocumentsscriptsblendermodal_operator_reloading_blendda
tarunner_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'})
Answered By: Mechanic
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.