Fix scrape crash: don't run GUI slots on worker threads
Connecting a signal to a Python lambda has no QObject receiver, so PySide6 treats it as a direct connection and runs the lambda on the emitting (worker) thread. The scrape dialog connected its results signals to lambdas, so _on_results/_on_scraped executed on the worker thread and called accept()/done() there -> QThread::wait on itself and GUI widget mutation off the GUI thread -> 'QThread: Destroyed while thread is still running' abort. Connect to bound methods (queued to the GUI thread) and carry the generation on the worker (read via sender().gen). Also replaced the thread.finished lambdas in load_library/metadata with bound-method cleanup keyed by a thread._worker attribute.
This commit is contained in:
+42
-21
@@ -969,10 +969,11 @@ class ScrapeSearchWorker(QObject):
|
|||||||
results = Signal(list) # candidates with thumb_bytes added
|
results = Signal(list) # candidates with thumb_bytes added
|
||||||
finished = Signal()
|
finished = Signal()
|
||||||
|
|
||||||
def __init__(self, system, query):
|
def __init__(self, system, query, gen):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.system = system
|
self.system = system
|
||||||
self.query = query
|
self.query = query
|
||||||
|
self.gen = gen
|
||||||
self._cancel = threading.Event()
|
self._cancel = threading.Event()
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
@@ -1003,11 +1004,12 @@ class ScrapeFetchWorker(QObject):
|
|||||||
result = Signal(dict) # {"meta": {...}, "cover": bytes|None}
|
result = Signal(dict) # {"meta": {...}, "cover": bytes|None}
|
||||||
finished = Signal()
|
finished = Signal()
|
||||||
|
|
||||||
def __init__(self, system, rid, candidate):
|
def __init__(self, system, rid, candidate, gen):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.system = system
|
self.system = system
|
||||||
self.rid = rid
|
self.rid = rid
|
||||||
self.candidate = candidate
|
self.candidate = candidate
|
||||||
|
self.gen = gen
|
||||||
self._cancel = threading.Event()
|
self._cancel = threading.Event()
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
@@ -1122,17 +1124,17 @@ class ScrapeDialog(QDialog):
|
|||||||
self.list.clear()
|
self.list.clear()
|
||||||
self.candidates = []
|
self.candidates = []
|
||||||
thread = QThread(self)
|
thread = QThread(self)
|
||||||
worker = ScrapeSearchWorker(self.system, query)
|
worker = ScrapeSearchWorker(self.system, query, gen)
|
||||||
worker.moveToThread(thread)
|
worker.moveToThread(thread)
|
||||||
thread.started.connect(worker.run)
|
thread.started.connect(worker.run)
|
||||||
worker.results.connect(lambda r, g=gen: self._on_results(r, g))
|
worker.results.connect(self._on_results)
|
||||||
worker.finished.connect(thread.quit)
|
worker.finished.connect(thread.quit)
|
||||||
self._threads.append(thread)
|
self._threads.append(thread)
|
||||||
self._workers.append(worker)
|
self._workers.append(worker)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def _on_results(self, results, gen):
|
def _on_results(self, results):
|
||||||
if gen != self._search_gen:
|
if getattr(self.sender(), "gen", None) != self._search_gen:
|
||||||
return
|
return
|
||||||
self.search_btn.setEnabled(True)
|
self.search_btn.setEnabled(True)
|
||||||
self.candidates = results
|
self.candidates = results
|
||||||
@@ -1172,17 +1174,17 @@ class ScrapeDialog(QDialog):
|
|||||||
self.search_btn.setEnabled(False)
|
self.search_btn.setEnabled(False)
|
||||||
self.status.setText("Scraping…")
|
self.status.setText("Scraping…")
|
||||||
thread = QThread(self)
|
thread = QThread(self)
|
||||||
worker = ScrapeFetchWorker(self.system, self.rid, candidate)
|
worker = ScrapeFetchWorker(self.system, self.rid, candidate, gen)
|
||||||
worker.moveToThread(thread)
|
worker.moveToThread(thread)
|
||||||
thread.started.connect(worker.run)
|
thread.started.connect(worker.run)
|
||||||
worker.result.connect(lambda p, g=gen: self._on_scraped(p, g))
|
worker.result.connect(self._on_scraped)
|
||||||
worker.finished.connect(thread.quit)
|
worker.finished.connect(thread.quit)
|
||||||
self._threads.append(thread)
|
self._threads.append(thread)
|
||||||
self._workers.append(worker)
|
self._workers.append(worker)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def _on_scraped(self, payload, gen):
|
def _on_scraped(self, payload):
|
||||||
if gen != self._search_gen:
|
if getattr(self.sender(), "gen", None) != self._search_gen:
|
||||||
return
|
return
|
||||||
meta = payload.get("meta") or {}
|
meta = payload.get("meta") or {}
|
||||||
cover = payload.get("cover")
|
cover = payload.get("cover")
|
||||||
@@ -1198,7 +1200,15 @@ class ScrapeDialog(QDialog):
|
|||||||
self.search_btn.setEnabled(True)
|
self.search_btn.setEnabled(True)
|
||||||
self.status.setText("Could not fetch anything for that selection.")
|
self.status.setText("Could not fetch anything for that selection.")
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def _shutdown_threads(self):
|
||||||
|
"""Cancel workers and wait for their threads to fully finish.
|
||||||
|
|
||||||
|
Must run before the dialog is destroyed — QThread children abort the
|
||||||
|
process with "QThread: Destroyed while thread is still running" if they
|
||||||
|
are deleted while their worker thread is alive. ``done()`` covers
|
||||||
|
accept()/reject() (including the accept() in ``_on_scraped``); the
|
||||||
|
close event covers the window-manager close path.
|
||||||
|
"""
|
||||||
for w in self._workers:
|
for w in self._workers:
|
||||||
w.cancel()
|
w.cancel()
|
||||||
for t in self._threads:
|
for t in self._threads:
|
||||||
@@ -1207,8 +1217,15 @@ class ScrapeDialog(QDialog):
|
|||||||
t.wait()
|
t.wait()
|
||||||
self._workers = []
|
self._workers = []
|
||||||
self._threads = []
|
self._threads = []
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
self._shutdown_threads()
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
def done(self, result):
|
||||||
|
self._shutdown_threads()
|
||||||
|
super().done(result)
|
||||||
|
|
||||||
|
|
||||||
class SettingsPage(QWidget):
|
class SettingsPage(QWidget):
|
||||||
saved = Signal()
|
saved = Signal()
|
||||||
@@ -4856,12 +4873,11 @@ class MainWindow(QMainWindow):
|
|||||||
thread = QThread(self)
|
thread = QThread(self)
|
||||||
worker = LibraryWorker(self._ps1_dir, self._ps1_output_dir, self._nps_dir)
|
worker = LibraryWorker(self._ps1_dir, self._ps1_output_dir, self._nps_dir)
|
||||||
worker.moveToThread(thread)
|
worker.moveToThread(thread)
|
||||||
|
thread._worker = worker
|
||||||
thread.started.connect(worker.run)
|
thread.started.connect(worker.run)
|
||||||
worker.result.connect(self._on_library_loaded)
|
worker.result.connect(self._on_library_loaded)
|
||||||
worker.finished.connect(thread.quit)
|
worker.finished.connect(thread.quit)
|
||||||
worker.finished.connect(worker.deleteLater)
|
thread.finished.connect(self._forget_library_thread)
|
||||||
thread.finished.connect(thread.deleteLater)
|
|
||||||
thread.finished.connect(lambda t=thread, w=worker: self._forget_library_thread(t, w))
|
|
||||||
self._library_threads.append(thread)
|
self._library_threads.append(thread)
|
||||||
self._library_workers.append(worker)
|
self._library_workers.append(worker)
|
||||||
thread.start()
|
thread.start()
|
||||||
@@ -4875,11 +4891,14 @@ class MainWindow(QMainWindow):
|
|||||||
self.store_page.set_library_items(self._library_cache)
|
self.store_page.set_library_items(self._library_cache)
|
||||||
self.statusBar().showMessage("")
|
self.statusBar().showMessage("")
|
||||||
|
|
||||||
def _forget_library_thread(self, thread, worker):
|
def _forget_library_thread(self):
|
||||||
|
thread = self.sender()
|
||||||
|
worker = getattr(thread, "_worker", None)
|
||||||
if thread in self._library_threads:
|
if thread in self._library_threads:
|
||||||
self._library_threads.remove(thread)
|
self._library_threads.remove(thread)
|
||||||
if worker in self._library_workers:
|
if worker is not None and worker in self._library_workers:
|
||||||
self._library_workers.remove(worker)
|
self._library_workers.remove(worker)
|
||||||
|
thread.deleteLater()
|
||||||
|
|
||||||
def _apply_library_filter(self):
|
def _apply_library_filter(self):
|
||||||
if not hasattr(self, "_library_cache"):
|
if not hasattr(self, "_library_cache"):
|
||||||
@@ -5012,21 +5031,23 @@ class MainWindow(QMainWindow):
|
|||||||
thread = QThread(self)
|
thread = QThread(self)
|
||||||
worker = MetadataWorker(title, gen, system, rid)
|
worker = MetadataWorker(title, gen, system, rid)
|
||||||
worker.moveToThread(thread)
|
worker.moveToThread(thread)
|
||||||
|
thread._worker = worker
|
||||||
thread.started.connect(worker.run)
|
thread.started.connect(worker.run)
|
||||||
worker.result.connect(self._on_metadata)
|
worker.result.connect(self._on_metadata)
|
||||||
worker.finished.connect(worker.deleteLater)
|
|
||||||
worker.finished.connect(thread.quit)
|
worker.finished.connect(thread.quit)
|
||||||
thread.finished.connect(thread.deleteLater)
|
thread.finished.connect(self._forget_meta_thread)
|
||||||
thread.finished.connect(lambda t=thread, w=worker: self._forget_meta_thread(t, w))
|
|
||||||
self._meta_threads.append(thread)
|
self._meta_threads.append(thread)
|
||||||
self._meta_workers.append(worker)
|
self._meta_workers.append(worker)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def _forget_meta_thread(self, thread, worker):
|
def _forget_meta_thread(self):
|
||||||
|
thread = self.sender()
|
||||||
|
worker = getattr(thread, "_worker", None)
|
||||||
if thread in self._meta_threads:
|
if thread in self._meta_threads:
|
||||||
self._meta_threads.remove(thread)
|
self._meta_threads.remove(thread)
|
||||||
if worker in self._meta_workers:
|
if worker is not None and worker in self._meta_workers:
|
||||||
self._meta_workers.remove(worker)
|
self._meta_workers.remove(worker)
|
||||||
|
thread.deleteLater()
|
||||||
|
|
||||||
def _on_metadata(self, gen, meta):
|
def _on_metadata(self, gen, meta):
|
||||||
if gen != self._meta_gen:
|
if gen != self._meta_gen:
|
||||||
|
|||||||
Reference in New Issue
Block a user