mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Merge pull request #8568 from sboddy/fix-autosave-recovery-segfault
Fix segfault on autosave recovery dialog Cancel
This commit is contained in:
@@ -1044,13 +1044,19 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
|||||||
|
|
||||||
return tooltip
|
return tooltip
|
||||||
|
|
||||||
def check_autosave_recovery(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"] | None:
|
def check_autosave_recovery(self, context: bpy.types.Context) -> bool:
|
||||||
if self.skip_autosave_recovery:
|
if self.skip_autosave_recovery:
|
||||||
return None
|
return False
|
||||||
autosaved_filepath = tool.Autosave.get_newer_autosaved_path(self.get_filepath_abs())
|
autosaved_filepath = tool.Autosave.get_newer_autosaved_path(self.get_filepath_abs())
|
||||||
if not autosaved_filepath:
|
if not autosaved_filepath:
|
||||||
return None
|
return False
|
||||||
return bpy.ops.bim.load_autosaved_recovery_popup(
|
# Fire-and-forget: don't propagate this popup's own RUNNING_MODAL
|
||||||
|
# return value up as if *this* operator were running modally too -
|
||||||
|
# we never call modal_handler_add() on ourselves, so the window
|
||||||
|
# manager would be left tracking a modal operator with no handler,
|
||||||
|
# corrupting its operator bookkeeping until it crashes later when
|
||||||
|
# the (real) popup modal handler is closed.
|
||||||
|
bpy.ops.bim.load_autosaved_recovery_popup(
|
||||||
"INVOKE_DEFAULT",
|
"INVOKE_DEFAULT",
|
||||||
original_filepath=str(self.get_filepath_abs()),
|
original_filepath=str(self.get_filepath_abs()),
|
||||||
autosaved_filepath=autosaved_filepath,
|
autosaved_filepath=autosaved_filepath,
|
||||||
@@ -1059,10 +1065,11 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
|||||||
should_start_fresh_session=self.should_start_fresh_session,
|
should_start_fresh_session=self.should_start_fresh_session,
|
||||||
import_without_ifc_data=self.import_without_ifc_data,
|
import_without_ifc_data=self.import_without_ifc_data,
|
||||||
)
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
if recovery := self.check_autosave_recovery(context):
|
if self.check_autosave_recovery(context):
|
||||||
return recovery
|
return {"FINISHED"}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
tool.Blender.get_addon_preferences().save_metadata_blend_file
|
tool.Blender.get_addon_preferences().save_metadata_blend_file
|
||||||
@@ -1177,8 +1184,8 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
|||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
if self.filepath:
|
if self.filepath:
|
||||||
if recovery := self.check_autosave_recovery(context):
|
if self.check_autosave_recovery(context):
|
||||||
return recovery
|
return {"FINISHED"}
|
||||||
return self.execute(context)
|
return self.execute(context)
|
||||||
return ImportHelper.invoke(self, context, event)
|
return ImportHelper.invoke(self, context, event)
|
||||||
|
|
||||||
@@ -2182,8 +2189,8 @@ class LoadAutosavedRecoveryPopup(bpy.types.Operator):
|
|||||||
self, width=420, title="Recover Autosaved File", confirm_text="Yes"
|
self, width=420, title="Recover Autosaved File", confirm_text="Yes"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _load(self, filepath: str, skip_recent: bool) -> set["rna_enums.OperatorReturnItems"]:
|
def _load_kwargs(self, filepath: str, skip_recent: bool) -> dict:
|
||||||
return bpy.ops.bim.load_project(
|
return dict(
|
||||||
filepath=filepath,
|
filepath=filepath,
|
||||||
skip_autosave_recovery=True, # Prevent infinite loop
|
skip_autosave_recovery=True, # Prevent infinite loop
|
||||||
is_advanced=self.is_advanced,
|
is_advanced=self.is_advanced,
|
||||||
@@ -2193,16 +2200,42 @@ class LoadAutosavedRecoveryPopup(bpy.types.Operator):
|
|||||||
skip_recent=skip_recent,
|
skip_recent=skip_recent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _defer(callback) -> None:
|
||||||
|
def on_timer() -> None:
|
||||||
|
callback()
|
||||||
|
return None
|
||||||
|
|
||||||
|
# bim.load_project (with should_start_fresh_session, our default)
|
||||||
|
# calls wm.read_homefile(), which tears down the window
|
||||||
|
# manager/screens/regions. Calling that synchronously from this
|
||||||
|
# dialog's execute()/cancel() - themselves invoked from deep inside
|
||||||
|
# Blender's modal handling for this popup's button click - frees
|
||||||
|
# data that the still-on-stack caller dereferences once we return,
|
||||||
|
# segfaulting Blender. Deferring by one timer tick runs the reload
|
||||||
|
# after the popup's own modal handling has fully unwound. The
|
||||||
|
# callback only closes over plain values (not `self`), since the
|
||||||
|
# operator instance itself may no longer be valid by the time the
|
||||||
|
# timer fires.
|
||||||
|
bpy.app.timers.register(on_timer, first_interval=0.0)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
result = self._load(self.autosaved_filepath, skip_recent=True)
|
kwargs = self._load_kwargs(self.autosaved_filepath, skip_recent=True)
|
||||||
# Re-point tracking at the original path so future saves write back
|
original_filepath = self.original_filepath
|
||||||
# to it, not "_autosaved.ifc".
|
|
||||||
tool.Ifc.set_path(self.original_filepath)
|
def load_and_repoint() -> None:
|
||||||
return result
|
bpy.ops.bim.load_project(**kwargs)
|
||||||
|
# Re-point tracking at the original path so future saves write
|
||||||
|
# back to it, not "_autosaved.ifc".
|
||||||
|
tool.Ifc.set_path(original_filepath)
|
||||||
|
|
||||||
|
self._defer(load_and_repoint)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
def cancel(self, context):
|
def cancel(self, context):
|
||||||
# Also reached via Escape or a click outside the dialog, not just Cancel.
|
# Also reached via Escape or a click outside the dialog, not just Cancel.
|
||||||
self._load(self.original_filepath, skip_recent=False)
|
kwargs = self._load_kwargs(self.original_filepath, skip_recent=False)
|
||||||
|
self._defer(lambda: bpy.ops.bim.load_project(**kwargs))
|
||||||
|
|
||||||
|
|
||||||
class AutosavePrompt(bpy.types.Operator):
|
class AutosavePrompt(bpy.types.Operator):
|
||||||
|
|||||||
@@ -96,9 +96,16 @@ class Autosave:
|
|||||||
if not cls.is_eligible():
|
if not cls.is_eligible():
|
||||||
return
|
return
|
||||||
|
|
||||||
def on_timer() -> None:
|
def on_timer() -> Union[float, None]:
|
||||||
cls._on_timer_expired()
|
cls._on_timer_expired()
|
||||||
return None
|
# Reschedule by returning the next interval rather than calling
|
||||||
|
# reset_timer(), which would unregister this timer from within
|
||||||
|
# its own callback. Blender frees the timer's internal registry
|
||||||
|
# entry on that manual unregister, then frees it again when the
|
||||||
|
# callback returns - a double free that corrupts the heap and
|
||||||
|
# crashes Blender shortly after (e.g. when the prompt dialog
|
||||||
|
# spawned below is next interacted with).
|
||||||
|
return cls.get_interval_seconds() if cls.is_eligible() else None
|
||||||
|
|
||||||
global _timer_callback
|
global _timer_callback
|
||||||
_timer_callback = on_timer
|
_timer_callback = on_timer
|
||||||
@@ -120,7 +127,6 @@ class Autosave:
|
|||||||
cls.perform_backup(bpy.context)
|
cls.perform_backup(bpy.context)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
print(f"Bonsai: autosave backup failed: {error}")
|
print(f"Bonsai: autosave backup failed: {error}")
|
||||||
cls.reset_timer()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def perform_backup(cls, context: bpy.types.Context) -> None:
|
def perform_backup(cls, context: bpy.types.Context) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user