Harden shutdown: no segfault closing mid-load (thread cleanup + graceful Ctrl+C)
This commit is contained in:
+51
-7
@@ -7,6 +7,7 @@ Run with::
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -158,7 +159,9 @@ class CoverLoader(QObject):
|
|||||||
else:
|
else:
|
||||||
self.failed.emit(self.gen, i, self.items[i]["name"])
|
self.failed.emit(self.gen, i, self.items[i]["name"])
|
||||||
finally:
|
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()
|
self.finished.emit()
|
||||||
|
|
||||||
|
|
||||||
@@ -480,7 +483,7 @@ class CoverShowcase(QWidget):
|
|||||||
return QIcon(pm)
|
return QIcon(pm)
|
||||||
|
|
||||||
def _on_loaded(self, gen, index, image):
|
def _on_loaded(self, gen, index, image):
|
||||||
if gen != self._generation:
|
if self._closing or gen != self._generation:
|
||||||
return
|
return
|
||||||
item = self.list.item(index)
|
item = self.list.item(index)
|
||||||
if item is not None:
|
if item is not None:
|
||||||
@@ -502,7 +505,7 @@ class CoverShowcase(QWidget):
|
|||||||
self.play_requested.emit(data)
|
self.play_requested.emit(data)
|
||||||
|
|
||||||
def _on_failed(self, gen, index, name):
|
def _on_failed(self, gen, index, name):
|
||||||
if gen != self._generation:
|
if self._closing or gen != self._generation:
|
||||||
return
|
return
|
||||||
if index not in self.missing:
|
if index not in self.missing:
|
||||||
self.missing.append(index)
|
self.missing.append(index)
|
||||||
@@ -510,6 +513,8 @@ class CoverShowcase(QWidget):
|
|||||||
self._update_status()
|
self._update_status()
|
||||||
|
|
||||||
def _on_loading_done(self):
|
def _on_loading_done(self):
|
||||||
|
if self._closing:
|
||||||
|
return
|
||||||
sender = self.sender()
|
sender = self.sender()
|
||||||
if sender is not None and getattr(sender, "gen", None) != self._generation:
|
if sender is not None and getattr(sender, "gen", None) != self._generation:
|
||||||
return
|
return
|
||||||
@@ -517,6 +522,8 @@ class CoverShowcase(QWidget):
|
|||||||
self._update_status()
|
self._update_status()
|
||||||
|
|
||||||
def _update_status(self):
|
def _update_status(self):
|
||||||
|
if self._closing:
|
||||||
|
return
|
||||||
if self._loading:
|
if self._loading:
|
||||||
self.status.setText("Loading covers…")
|
self.status.setText("Loading covers…")
|
||||||
self.missing_changed.emit(0)
|
self.missing_changed.emit(0)
|
||||||
@@ -1541,7 +1548,9 @@ class StoreCoverLoader(QObject):
|
|||||||
if not img.isNull():
|
if not img.isNull():
|
||||||
self.loaded.emit(self.gen, row, _fit_image(img, self.icon_w, self.icon_h))
|
self.loaded.emit(self.gen, row, _fit_image(img, self.icon_w, self.icon_h))
|
||||||
finally:
|
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()
|
self.finished.emit()
|
||||||
|
|
||||||
|
|
||||||
@@ -2353,7 +2362,7 @@ class StorePage(QWidget):
|
|||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def _on_date_loaded(self, gen, row, date):
|
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
|
return
|
||||||
if 0 <= row < len(self._filtered):
|
if 0 <= row < len(self._filtered):
|
||||||
self._filtered[row]["release_date"] = date
|
self._filtered[row]["release_date"] = date
|
||||||
@@ -2376,7 +2385,7 @@ class StorePage(QWidget):
|
|||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def _on_cover_loaded(self, gen, row, image):
|
def _on_cover_loaded(self, gen, row, image):
|
||||||
if gen != self._generation:
|
if self._closing or gen != self._generation:
|
||||||
return
|
return
|
||||||
item = self.grid.item(row)
|
item = self.grid.item(row)
|
||||||
if item is not None:
|
if item is not None:
|
||||||
@@ -4130,6 +4139,7 @@ class Ps1Page(QWidget):
|
|||||||
self._preview_gen = 0
|
self._preview_gen = 0
|
||||||
self._preview_threads = []
|
self._preview_threads = []
|
||||||
self._preview_workers = []
|
self._preview_workers = []
|
||||||
|
self._closing = False
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
bar = QHBoxLayout()
|
bar = QHBoxLayout()
|
||||||
@@ -4228,7 +4238,7 @@ class Ps1Page(QWidget):
|
|||||||
self._preview_workers.append(worker)
|
self._preview_workers.append(worker)
|
||||||
|
|
||||||
def _on_preview_result(self, gen, game_id, icon0, pic0, pic1):
|
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
|
return
|
||||||
self.preview_icon0.setText("")
|
self.preview_icon0.setText("")
|
||||||
self.preview_pic0.setText("")
|
self.preview_pic0.setText("")
|
||||||
@@ -4276,6 +4286,8 @@ class Ps1Page(QWidget):
|
|||||||
self._thread.start()
|
self._thread.start()
|
||||||
|
|
||||||
def _on_results(self, entries):
|
def _on_results(self, entries):
|
||||||
|
if self._closing:
|
||||||
|
return
|
||||||
# Only show discs that still need converting; converted games already
|
# Only show discs that still need converting; converted games already
|
||||||
# appear in the Library tab.
|
# appear in the Library tab.
|
||||||
entries = [e for e in entries if e["kind"] == "disc" and e.get("status") != "converted"]
|
entries = [e for e in entries if e["kind"] == "disc" and e.get("status") != "converted"]
|
||||||
@@ -4291,8 +4303,23 @@ class Ps1Page(QWidget):
|
|||||||
self.table.selectRow(0)
|
self.table.selectRow(0)
|
||||||
|
|
||||||
def _on_failed(self, message):
|
def _on_failed(self, message):
|
||||||
|
if not self._closing:
|
||||||
self.status.setText(f"PS1 scan failed: {message}")
|
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):
|
def _convert_selected(self):
|
||||||
row = self.table.currentRow()
|
row = self.table.currentRow()
|
||||||
if row < 0 or row >= len(self._entries):
|
if row < 0 or row >= len(self._entries):
|
||||||
@@ -5685,6 +5712,8 @@ class MainWindow(QMainWindow):
|
|||||||
self.psp_page.shutdown()
|
self.psp_page.shutdown()
|
||||||
if hasattr(self, "saves_page"):
|
if hasattr(self, "saves_page"):
|
||||||
self.saves_page.shutdown()
|
self.saves_page.shutdown()
|
||||||
|
if hasattr(self, "ps1_page"):
|
||||||
|
self.ps1_page.shutdown()
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
@@ -5718,6 +5747,21 @@ def main():
|
|||||||
window.show()
|
window.show()
|
||||||
log.info("main window ready")
|
log.info("main window ready")
|
||||||
threading.Thread(target=_start_bundle_check, args=(window,), daemon=True).start()
|
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()
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user