From bb355dac3132b516ea5c39684dafcfb54ae38fed Mon Sep 17 00:00:00 2001 From: Cowork Supervisor Date: Sun, 12 Jul 2026 12:53:12 -0400 Subject: [PATCH] fix: keep UpdateCheckWorker alive until its thread finishes (#32) Closing the About dialog mid-check GC'd the dialog and the running QThread with it -> 'QThread: Destroyed while thread is still running'. A class-level keepalive set now holds each worker until finished; stale results to a destroyed receiver are dropped by Qt. Closes #32 Co-Authored-By: Claude Fable 5 --- bcc.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bcc.py b/bcc.py index 5886a28..1779909 100644 --- a/bcc.py +++ b/bcc.py @@ -13,6 +13,7 @@ from __future__ import annotations import sys import time from pathlib import Path +from typing import ClassVar from PySide6.QtCore import QRect, QSettings, QSize, Qt, QThread, QTimer, QUrl, Signal from PySide6.QtGui import ( @@ -172,10 +173,30 @@ class UpdateCheckWorker(QThread): running binary. Fails quiet — emits None on any network problem — so it's safe to fire unattended from a silent startup check as well as from the About dialog's "Check for updates" button. + + Lifetime: the class keeps every instance alive in `_live` until its + thread has finished. Without this, a caller whose own reference dies + early (the About dialog is a temporary — closing it mid-check used to + GC the dialog and the running QThread with it) crashes the process with + "QThread: Destroyed while thread is still running". Callers may drop + their reference at any time; signal connections to a destroyed receiver + are disconnected by Qt, so a late result is simply discarded. """ done = Signal(object) # dict | None + _live: ClassVar[set[UpdateCheckWorker]] = set() + + def __init__(self): + super().__init__() + UpdateCheckWorker._live.add(self) + self.finished.connect(self._release_keepalive) + + def _release_keepalive(self): + # Delivered on the main thread after run() has returned; only now is + # it safe for the last reference to drop. + UpdateCheckWorker._live.discard(self) + def run(self): self.done.emit(core.fetch_latest_release())