Root cause: QThread C++ destructor ran while underlying OS thread was still terminating. The pattern of checking isRunning() and using short timeouts (wait(3000/5000)) left a window where the Python reference was dropped and Shiboken destroyed the C++ object before the thread fully exited. Fixes: - Remove all self.finished.connect(self.deleteLater) — unsafe, deleteLater from within run() or from worker thread races with thread termination - _cleanup_worker: always call wait() (no timeout) before nulling the Python reference — guarantees OS thread is fully dead - Add _is_worker_running() helper with try/except RuntimeError guard to safely check stale C++ objects - DeviceMonitor._poll/stop/wait: use wait() with no timeout - closeEvent: guard isRunning() with try/except RuntimeError
71 lines
1.9 KiB
Python
71 lines
1.9 KiB
Python
import logging
|
|
from typing import Set
|
|
|
|
from PyQt6.QtCore import QObject, QTimer, QThread, pyqtSignal
|
|
|
|
from ipod_device import IPodDevice
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class _DetectWorker(QThread):
|
|
finished = pyqtSignal(object)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
|
|
def run(self):
|
|
try:
|
|
devices = IPodDevice().detect_devices()
|
|
self.finished.emit(devices)
|
|
except Exception:
|
|
logger.exception("Device detection failed")
|
|
self.finished.emit([])
|
|
|
|
|
|
class DeviceMonitor(QObject):
|
|
|
|
state_changed = pyqtSignal(list)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.timer = QTimer(self)
|
|
self.timer.timeout.connect(self._poll)
|
|
self._device_ids: Set[str] = set()
|
|
self._poll_worker = None
|
|
|
|
def start(self, interval_ms: int = 3000):
|
|
self._device_ids.clear()
|
|
self._poll()
|
|
self.timer.start(interval_ms)
|
|
|
|
def stop(self):
|
|
self.timer.stop()
|
|
if self._poll_worker:
|
|
self._poll_worker.quit()
|
|
self._poll_worker.wait()
|
|
self._poll_worker = None
|
|
self._device_ids.clear()
|
|
|
|
def _poll(self):
|
|
if self._poll_worker:
|
|
try:
|
|
if self._poll_worker.isRunning():
|
|
return
|
|
except RuntimeError:
|
|
self._poll_worker = None
|
|
worker = _DetectWorker()
|
|
worker.finished.connect(self._on_devices_detected)
|
|
worker.start()
|
|
self._poll_worker = worker
|
|
|
|
def _on_devices_detected(self, devices):
|
|
if self._poll_worker:
|
|
self._poll_worker.quit()
|
|
self._poll_worker.wait()
|
|
self._poll_worker = None
|
|
current_ids = {d["id"] for d in devices}
|
|
if current_ids != self._device_ids:
|
|
self._device_ids = current_ids
|
|
self.state_changed.emit(devices)
|