From be55400ec658f8db8a18e8080d5c334113f10e7e Mon Sep 17 00:00:00 2001 From: Stephen Boddy Date: Sat, 11 Jul 2026 06:40:17 +0100 Subject: [PATCH] Remove stale autosave file on clean Blender quit Previously the autosaved copy was only ever overwritten, never removed, so a deliberate quit (whether the user saved or chose "don't save") still nagged with a recovery prompt on next startup. Registers an atexit cleanup that removes the active IFC's autosave file(s) on a graceful interpreter shutdown. atexit never runs on an actual crash, so a genuine crash still leaves the recovery file in place as before. The cleanup reads a cached plain-string path kept up to date by reset_timer(), rather than looking it up live via bpy.context - by the time atexit fires, Blender's C++ side is torn down far enough that even a read-only bpy.context.scene access aborts the process (std::bad_optional_access) instead of raising a catchable exception. Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Sonnet 5 --- src/bonsai/bonsai/tool/autosave.py | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/bonsai/bonsai/tool/autosave.py b/src/bonsai/bonsai/tool/autosave.py index 7fb4a04e33..db7e554443 100644 --- a/src/bonsai/bonsai/tool/autosave.py +++ b/src/bonsai/bonsai/tool/autosave.py @@ -20,6 +20,7 @@ from __future__ import annotations +import atexit import logging import os from pathlib import Path @@ -36,6 +37,9 @@ AUTOSAVING_SUFFIX = "_autosaving.ifc" AUTOSAVED_SUFFIX = "_autosaved.ifc" _timer_callback: Union[Callable[[], None], None] = None +# See cleanup_stale_autosave() for why this is a cached plain string rather +# than looked up live. +_active_ifc_path_cache: Union[str, None] = None class Autosave: @@ -59,6 +63,12 @@ class Autosave: return None return path + @classmethod + def _update_active_ifc_path_cache(cls) -> None: + global _active_ifc_path_cache + ifc_path = cls.get_active_ifc_path() + _active_ifc_path_cache = ifc_path.as_posix() if ifc_path is not None else None + @classmethod def is_enabled(cls) -> bool: return bool(tool.Blender.get_addon_preferences().autosave_enabled) @@ -82,6 +92,7 @@ class Autosave: @classmethod def reset_timer(cls) -> None: cls.cancel_timer() + cls._update_active_ifc_path_cache() if not cls.is_eligible(): return @@ -147,3 +158,31 @@ class Autosave: if autosaved_path.stat().st_mtime > path.stat().st_mtime: return autosaved_path.as_posix().replace("\\", "/") return None + + @classmethod + def cleanup_stale_autosave(cls) -> None: + """Remove the active IFC's autosave file(s) on a graceful shutdown. + + Registered via `atexit`, which only runs on a normal interpreter + shutdown - never on an actual crash. So a deliberate quit (whether + the user saved or chose "don't save") clears the recovery file and + won't prompt on next startup, while a genuine crash leaves it in + place for recovery, since no atexit callbacks fire then. + + Deliberately reads only `_active_ifc_path_cache` - a plain string + kept up to date by `reset_timer()` - rather than touching `bpy` here. + By the time `atexit` fires, Blender's own C++ side is torn down far + enough that even reading `bpy.context.scene` aborts the process + (std::bad_optional_access) instead of raising a catchable exception. + """ + if _active_ifc_path_cache is None: + return + try: + _, autosaving_path, autosaved_path = cls.get_paths(_active_ifc_path_cache) + autosaving_path.unlink(missing_ok=True) + autosaved_path.unlink(missing_ok=True) + except Exception: + pass + + +atexit.register(Autosave.cleanup_stale_autosave)