From d94ae52e14041ba647dc33ee800b9f4bb060d310 Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Mon, 14 Sep 2026 20:36:59 -0400 Subject: [PATCH] Fix store crash when filtering after load (loader thread cleanup) StorePage cover/date loaders were never removed from tracking once their thread finished, and _populate/_begin_scan dropped the whole list while threads were still running. That garbage-collected QObjects whose QThread had already been deleted, leaving a dangling thread affinity that crashed Qt (SIGSEGV) when searching the store after it fully loaded. Loaders now remove themselves and deleteLater on their finished signal, and the tracking list is kept until each thread winds down. --- app/gui.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/app/gui.py b/app/gui.py index 85f9528..2ae9c0f 100644 --- a/app/gui.py +++ b/app/gui.py @@ -2044,7 +2044,6 @@ class StorePage(QWidget): self._cover_attempted.clear() for loader in self._loaders: loader.cancel() - self._loaders = [] self.grid.clear() self.table.setRowCount(0) self.status.setText("Loading…") @@ -2220,10 +2219,12 @@ class StorePage(QWidget): self._cover_attempted.clear() self._placeholder = QIcon(_placeholder_pixmap(self.ICON_W, self.ICON_H)) - # Cancel any in-flight cover loaders (stale generation). + # Cancel any in-flight cover loaders (stale generation). Each loader + # removes itself from ``_loaders`` via its own ``finished`` handler once + # its thread winds down — dropping the list here would garbage-collect + # live QObjects and crash Qt. for loader in self._loaders: loader.cancel() - self._loaders = [] self.grid.clear() self.grid.setUpdatesEnabled(False) @@ -2401,6 +2402,7 @@ class StorePage(QWidget): thread.started.connect(loader.run) loader.loaded.connect(self._on_date_loaded) loader.finished.connect(self._on_date_batch_done) + loader.finished.connect(self._on_loader_finished) loader.finished.connect(thread.quit) thread.finished.connect(self._on_thread_finished) self._threads.append(thread) @@ -2425,6 +2427,21 @@ class StorePage(QWidget): if cell is not None: cell.setText(date) + def _on_loader_finished(self): + """Drop a finished cover/date loader from tracking. + + Loaders stay in ``_loaders`` until their thread finishes so their + QObject wrapper is never garbage-collected while its thread is still + winding down (that left a dangling thread affinity and crashed Qt on + filter/refresh). + """ + loader = self.sender() + if loader is None: + return + if loader in self._loaders: + self._loaders.remove(loader) + loader.deleteLater() + def _start_cover_loader(self, gen, tasks): if self._closing or not tasks: return @@ -2434,6 +2451,7 @@ class StorePage(QWidget): thread.started.connect(loader.run) loader.loaded.connect(self._on_cover_loaded) loader.finished.connect(self._on_cover_batch_done) + loader.finished.connect(self._on_loader_finished) loader.finished.connect(thread.quit) thread.finished.connect(self._on_thread_finished) self._threads.append(thread)