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.
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'})