fix: keep UpdateCheckWorker alive until its thread finishes (#32)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10) (pull_request) Successful in 8s
CI / Tests (py3.12) (pull_request) Successful in 8s

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 <noreply@anthropic.com>
This commit is contained in:
Cowork Supervisor
2026-07-12 12:53:12 -04:00
parent 06326e5e9d
commit bb355dac31
+21
View File
@@ -13,6 +13,7 @@ from __future__ import annotations
import sys import sys
import time import time
from pathlib import Path from pathlib import Path
from typing import ClassVar
from PySide6.QtCore import QRect, QSettings, QSize, Qt, QThread, QTimer, QUrl, Signal from PySide6.QtCore import QRect, QSettings, QSize, Qt, QThread, QTimer, QUrl, Signal
from PySide6.QtGui import ( from PySide6.QtGui import (
@@ -172,10 +173,30 @@ class UpdateCheckWorker(QThread):
running binary. Fails quiet — emits None on any network problem — so 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 it's safe to fire unattended from a silent startup check as well as from
the About dialog's "Check for updates" button. 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 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): def run(self):
self.done.emit(core.fetch_latest_release()) self.done.emit(core.fetch_latest_release())