diff --git a/app/gui.py b/app/gui.py index 3d3850c..385d6fd 100644 --- a/app/gui.py +++ b/app/gui.py @@ -7,6 +7,7 @@ Run with:: import datetime import json import logging +import signal import sys import threading from pathlib import Path @@ -158,7 +159,9 @@ class CoverLoader(QObject): else: self.failed.emit(self.gen, i, self.items[i]["name"]) finally: - pool.shutdown(wait=True, cancel_futures=True) + # Return promptly on cancel/exit instead of blocking the GUI thread + # on in-flight network timeouts during shutdown. + pool.shutdown(wait=not self._cancel.is_set(), cancel_futures=True) self.finished.emit() @@ -480,7 +483,7 @@ class CoverShowcase(QWidget): return QIcon(pm) def _on_loaded(self, gen, index, image): - if gen != self._generation: + if self._closing or gen != self._generation: return item = self.list.item(index) if item is not None: @@ -502,7 +505,7 @@ class CoverShowcase(QWidget): self.play_requested.emit(data) def _on_failed(self, gen, index, name): - if gen != self._generation: + if self._closing or gen != self._generation: return if index not in self.missing: self.missing.append(index) @@ -510,6 +513,8 @@ class CoverShowcase(QWidget): self._update_status() def _on_loading_done(self): + if self._closing: + return sender = self.sender() if sender is not None and getattr(sender, "gen", None) != self._generation: return @@ -517,6 +522,8 @@ class CoverShowcase(QWidget): self._update_status() def _update_status(self): + if self._closing: + return if self._loading: self.status.setText("Loading covers…") self.missing_changed.emit(0) @@ -1541,7 +1548,9 @@ class StoreCoverLoader(QObject): if not img.isNull(): self.loaded.emit(self.gen, row, _fit_image(img, self.icon_w, self.icon_h)) finally: - pool.shutdown(wait=True, cancel_futures=True) + # Return promptly on cancel/exit instead of blocking the GUI thread + # on in-flight network timeouts during shutdown. + pool.shutdown(wait=not self._cancel.is_set(), cancel_futures=True) self.finished.emit() @@ -2353,7 +2362,7 @@ class StorePage(QWidget): thread.start() def _on_date_loaded(self, gen, row, date): - if gen != self._generation or not date: + if self._closing or gen != self._generation or not date: return if 0 <= row < len(self._filtered): self._filtered[row]["release_date"] = date @@ -2376,7 +2385,7 @@ class StorePage(QWidget): thread.start() def _on_cover_loaded(self, gen, row, image): - if gen != self._generation: + if self._closing or gen != self._generation: return item = self.grid.item(row) if item is not None: @@ -4130,6 +4139,7 @@ class Ps1Page(QWidget): self._preview_gen = 0 self._preview_threads = [] self._preview_workers = [] + self._closing = False layout = QVBoxLayout(self) bar = QHBoxLayout() @@ -4228,7 +4238,7 @@ class Ps1Page(QWidget): self._preview_workers.append(worker) def _on_preview_result(self, gen, game_id, icon0, pic0, pic1): - if gen != self._preview_gen: + if self._closing or gen != self._preview_gen: return self.preview_icon0.setText("") self.preview_pic0.setText("") @@ -4276,6 +4286,8 @@ class Ps1Page(QWidget): self._thread.start() def _on_results(self, entries): + if self._closing: + return # Only show discs that still need converting; converted games already # appear in the Library tab. entries = [e for e in entries if e["kind"] == "disc" and e.get("status") != "converted"] @@ -4291,7 +4303,22 @@ class Ps1Page(QWidget): self.table.selectRow(0) def _on_failed(self, message): - self.status.setText(f"PS1 scan failed: {message}") + if not self._closing: + self.status.setText(f"PS1 scan failed: {message}") + + def shutdown(self): + self._closing = True + if self._thread is not None and _thread_running(self._thread): + self._thread.quit() + self._thread.wait() + for t in self._preview_threads: + if _thread_running(t): + t.quit() + t.wait() + self._preview_threads = [] + self._preview_workers = [] + self._thread = None + self._worker = None def _convert_selected(self): row = self.table.currentRow() @@ -5685,6 +5712,8 @@ class MainWindow(QMainWindow): self.psp_page.shutdown() if hasattr(self, "saves_page"): self.saves_page.shutdown() + if hasattr(self, "ps1_page"): + self.ps1_page.shutdown() event.accept() @@ -5718,6 +5747,21 @@ def main(): window.show() log.info("main window ready") threading.Thread(target=_start_bundle_check, args=(window,), daemon=True).start() + + # Ctrl+C should close the window gracefully (running closeEvent's thread + # cleanup) rather than tearing Qt down mid-thread, which segfaults. + interrupt = {"pending": False} + + def _sigint_handler(signum, frame): + interrupt["pending"] = True + + signal.signal(signal.SIGINT, _sigint_handler) + sigint_timer = QTimer() + sigint_timer.setInterval(100) + sigint_timer.timeout.connect( + lambda: window.close() if interrupt["pending"] else None) + sigint_timer.start() + return app.exec()