From 202ef0bbef47c79f2e272778a9c4bfa079b54253 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jan 2024 15:07:19 -0500 Subject: [PATCH 1/3] Attempt to fix the crash issue with Houdini --- python/app/dialog.py | 13 +++++-- python/app/utils.py | 60 +++++++++++++++++++++++++++++++++ python/app/widget_all_fields.py | 2 +- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/python/app/dialog.py b/python/app/dialog.py index f37fa55a..5af7d5c5 100644 --- a/python/app/dialog.py +++ b/python/app/dialog.py @@ -38,6 +38,7 @@ from .note_updater import NoteUpdater from .widget_all_fields import AllFieldsWidget from .work_area_dialog import WorkAreaDialog +from .utils import monitor_qobject_lifetime shotgun_model = sgtk.platform.import_framework( "tk-framework-shotgunutils", "shotgun_model" @@ -135,8 +136,10 @@ def __init__(self, parent=None): # create a background task manager self._task_manager = task_manager.BackgroundTaskManager( - self, start_processing=True, max_threads=2 + self, max_threads=2 ) + monitor_qobject_lifetime(self._task_manager, "Main task manager") + self._task_manager.start_processing() # register the data fetcher with the global schema manager shotgun_globals.register_bg_task_manager(self._task_manager) @@ -278,6 +281,10 @@ def closeEvent(self, event): # close splash self._overlay.hide() + # destroy the current Qt Model + self._overlay.destroy() + self.destroy() + # okay to close dialog event.accept() @@ -869,7 +876,7 @@ def _change_work_area(self, entity_type, entity_id): } self._app.log_debug( - "Creating new task:\n%s" % pprint.pprint(sg_data) + "Creating new task:\n%s" % pprint.pformat(sg_data) ) task_data = self._app.shotgun.create("Task", sg_data) (entity_type, entity_id) = (task_data["type"], task_data["id"]) @@ -1208,6 +1215,8 @@ def setup_entity_model_view(self, entity_data): entity_data["model"] = ModelClass( entity_data["entity_type"], entity_data["view"], self._task_manager ) + monitor_qobject_lifetime(entity_data["model"]) + # create proxy for sorting entity_data["sort_proxy"] = FilterItemProxyModel(self) diff --git a/python/app/utils.py b/python/app/utils.py index ab6c53db..db489aec 100644 --- a/python/app/utils.py +++ b/python/app/utils.py @@ -216,3 +216,63 @@ def create_human_readable_timestamp(datetime_obj): time_str = datetime_obj.strftime("%H:%M") return (time_str, full_time_str) + + +# storage for any tracked qobjects +_g_monitored_qobjects = {} + + +def monitor_qobject_lifetime(obj, name=""): + """ + Debug method to help track the lifetime of a QObject derived instance. Hooks into + the instances destroyed signal to report when the QObject has been destroyed. + + :param obj: The QObject instance to monitor + :param name: An optional name to be appended to the debug output, useful for identifying + a specific instance of a class. + """ + msg = type(obj).__name__ + if name: + msg = "%s [%s]" % (msg, name) + + app = sgtk.platform.current_bundle() + + global _g_monitored_qobjects + uid = len(_g_monitored_qobjects) + _g_monitored_qobjects[uid] = msg + obj.destroyed.connect(lambda m=msg, u=uid: _on_qobject_destroyed(m, u)) + + +def _on_qobject_destroyed(name, uid): + """ + Slot triggered whenever a monitored qobject is destroyed - reports to debug that the object + was destroyed. + + :param name: Name of the instance that was destroyed + :param uid: Unique id of the QObject used to look it up in the monitored list + """ + app = sgtk.platform.current_bundle() + app.log_debug("%s destroyed" % name) + global _g_monitored_qobjects + if uid in _g_monitored_qobjects: + del _g_monitored_qobjects[uid] + + +def report_non_destroyed_qobjects(clear_list=True): + """ + Report any monitored QObjects that have not yet been destroyed. Care should be taken to + account for QObjects that are pending destruction via deleteLater signals that may be + pending. + + :param clear_list: If true then the list of monitored QObjects will be cleared after + this function has reported them. + """ + app = sgtk.platform.current_bundle() + global _g_monitored_qobjects + app.log_debug( + "%d monitored QObjects have not been destroyed!" % len(_g_monitored_qobjects) + ) + for msg in _g_monitored_qobjects.values(): + app.log_debug(" - %s" % msg) + if clear_list: + _g_monitored_qobjects = {} diff --git a/python/app/widget_all_fields.py b/python/app/widget_all_fields.py index 0ed6ab90..3aa9f69a 100644 --- a/python/app/widget_all_fields.py +++ b/python/app/widget_all_fields.py @@ -85,7 +85,7 @@ def clear(self): # set it's parent to None so that it is removed from the widget hierarchy x.setParent(None) # mark it to be deleted when event processing returns to the main loop - x.deleteLater() + x.destroy() self._widgets = [] From d013711eddd1695c64be53c76e0d0ab68a12d3e9 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jan 2024 15:20:41 -0500 Subject: [PATCH 2/3] Format code with black --- python/app/dialog.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/python/app/dialog.py b/python/app/dialog.py index 5af7d5c5..5113a589 100644 --- a/python/app/dialog.py +++ b/python/app/dialog.py @@ -135,9 +135,7 @@ def __init__(self, parent=None): self._action_manager.refresh_request.connect(self.refresh) # create a background task manager - self._task_manager = task_manager.BackgroundTaskManager( - self, max_threads=2 - ) + self._task_manager = task_manager.BackgroundTaskManager(self, max_threads=2) monitor_qobject_lifetime(self._task_manager, "Main task manager") self._task_manager.start_processing() @@ -261,7 +259,6 @@ def closeEvent(self, event): self._overlay.repaint() try: - # register the data fetcher with the global schema manager shotgun_globals.unregister_bg_task_manager(self._task_manager) @@ -841,7 +838,6 @@ def _change_work_area(self, entity_type, entity_id): res = dialog.exec_() if res == QtGui.QDialog.Accepted: - if dialog.is_new_task: # user wants to create a new task @@ -1217,7 +1213,6 @@ def setup_entity_model_view(self, entity_data): ) monitor_qobject_lifetime(entity_data["model"]) - # create proxy for sorting entity_data["sort_proxy"] = FilterItemProxyModel(self) entity_data["sort_proxy"].setSourceModel(entity_data["model"]) @@ -1298,7 +1293,6 @@ def _sort_menu_setup(self, task_tab_data): self._sort_menu_actions() def _field_filters(self): - # ---- define a few simple filter methods for use by the menu def field_filter(field): From 0ca540c801e333546e45d59f47b4b9d965e45a19 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Wed, 17 Jan 2024 08:03:52 -0500 Subject: [PATCH 3/3] Use EAFP instead of LBYL --- python/app/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/app/utils.py b/python/app/utils.py index db489aec..7f935f26 100644 --- a/python/app/utils.py +++ b/python/app/utils.py @@ -254,8 +254,10 @@ def _on_qobject_destroyed(name, uid): app = sgtk.platform.current_bundle() app.log_debug("%s destroyed" % name) global _g_monitored_qobjects - if uid in _g_monitored_qobjects: + try: del _g_monitored_qobjects[uid] + except KeyError: + pass def report_non_destroyed_qobjects(clear_list=True): @@ -275,4 +277,4 @@ def report_non_destroyed_qobjects(clear_list=True): for msg in _g_monitored_qobjects.values(): app.log_debug(" - %s" % msg) if clear_list: - _g_monitored_qobjects = {} + _g_monitored_qobjects.clear()