Fix QThread lifecycle crash: 'Destroyed while thread is still running'

- Add _cleanup_worker() helper: calls wait() then deleteLater() before
  dropping the Python reference. This ensures the C++ thread is fully
  cleaned up before the QObject is destroyed.
- Replace all 'self.worker_thread = None' with _cleanup_worker()
- Add running thread guards to all handlers: prevent starting a new
  WorkerThread while one is already running
- Add timeout to wait() calls (3000ms) to avoid hanging on close
- WorkerThread.stop() now calls self.wait() to ensure clean shutdown
This commit is contained in:
Maksim Totmin 2026-05-31 14:58:13 +07:00
parent 4c41cba428
commit 56a88cf420

View File

@ -201,6 +201,7 @@ class WorkerThread(QThread):
def stop(self): def stop(self):
self.is_running = False self.is_running = False
self.wait()
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
@ -222,6 +223,18 @@ class MainWindow(QMainWindow):
self._scan_library() self._scan_library()
self._on_refresh_devices_clicked() self._on_refresh_devices_clicked()
def _cleanup_worker(self):
"""Safely clean up the worker thread after it finishes.
Calls wait() to ensure the C++ thread is fully cleaned up before
dropping the Python reference. This prevents the Qt crash:
'QThread: Destroyed while thread is still running'
"""
if self.worker_thread:
self.worker_thread.wait(3000)
self.worker_thread.deleteLater()
self.worker_thread = None
def _setup_ui(self): def _setup_ui(self):
main_widget = QWidget() main_widget = QWidget()
main_layout = QVBoxLayout(main_widget) main_layout = QVBoxLayout(main_widget)
@ -632,6 +645,10 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "Error", "Please enter a valid URL") QMessageBox.warning(self, "Error", "Please enter a valid URL")
return return
if self.worker_thread and self.worker_thread.isRunning():
QMessageBox.information(self, "Info", "Please wait for the current task to finish")
return
self.download_button.setEnabled(False) self.download_button.setEnabled(False)
self.download_status.setText("Downloading...") self.download_status.setText("Downloading...")
self.download_progress.setValue(0) self.download_progress.setValue(0)
@ -669,7 +686,7 @@ class MainWindow(QMainWindow):
self.downloaded_tracks = [] self.downloaded_tracks = []
self.track_list.clear() self.track_list.clear()
self.worker_thread = None self._cleanup_worker()
def _on_library_convert_clicked(self): def _on_library_convert_clicked(self):
source_files = self._get_selected_source_files() source_files = self._get_selected_source_files()
@ -682,6 +699,10 @@ class MainWindow(QMainWindow):
) )
return return
if self.worker_thread and self.worker_thread.isRunning():
QMessageBox.information(self, "Info", "Please wait for the current task to finish")
return
output_dir = self.output_dir_input.text().strip() output_dir = self.output_dir_input.text().strip()
if not output_dir: if not output_dir:
QMessageBox.warning(self, "Error", "Please set an output directory") QMessageBox.warning(self, "Error", "Please set an output directory")
@ -718,7 +739,7 @@ class MainWindow(QMainWindow):
self.library_status.setText(f"Error: {message}") self.library_status.setText(f"Error: {message}")
QMessageBox.warning(self, "Conversion Error", message) QMessageBox.warning(self, "Conversion Error", message)
self.worker_thread = None self._cleanup_worker()
def _on_library_transfer_clicked(self): def _on_library_transfer_clicked(self):
selected_tracks = self._get_selected_ready_tracks() selected_tracks = self._get_selected_ready_tracks()
@ -730,6 +751,10 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "Error", "No iPod device mounted. Go to iPod tab and mount first.") QMessageBox.warning(self, "Error", "No iPod device mounted. Go to iPod tab and mount first.")
return return
if self.worker_thread and self.worker_thread.isRunning():
QMessageBox.information(self, "Info", "Please wait for the current task to finish")
return
self.library_transfer_btn.setEnabled(False) self.library_transfer_btn.setEnabled(False)
self.library_status.setText("Transferring...") self.library_status.setText("Transferring...")
self.library_progress.setValue(0) self.library_progress.setValue(0)
@ -759,9 +784,12 @@ class MainWindow(QMainWindow):
self.library_status.setText(f"Error: {message}") self.library_status.setText(f"Error: {message}")
QMessageBox.warning(self, "Transfer Error", message) QMessageBox.warning(self, "Transfer Error", message)
self.worker_thread = None self._cleanup_worker()
def _on_refresh_devices_clicked(self): def _on_refresh_devices_clicked(self):
if self.worker_thread and self.worker_thread.isRunning():
return
self.device_combo.clear() self.device_combo.clear()
self.device_info_label.setText("Detecting devices...") self.device_info_label.setText("Detecting devices...")
@ -807,7 +835,7 @@ class MainWindow(QMainWindow):
self._set_device_not_found() self._set_device_not_found()
self.statusBar().showMessage(message) self.statusBar().showMessage(message)
self.worker_thread = None self._cleanup_worker()
def _set_device_mounted(self, device: dict, mount_point: str): def _set_device_mounted(self, device: dict, mount_point: str):
self.current_mount_point = mount_point self.current_mount_point = mount_point
@ -1010,7 +1038,8 @@ class MainWindow(QMainWindow):
def closeEvent(self, event): def closeEvent(self, event):
if self.worker_thread and self.worker_thread.isRunning(): if self.worker_thread and self.worker_thread.isRunning():
self.worker_thread.stop() self.worker_thread.stop()
self.worker_thread.wait() self.worker_thread.wait(5000)
self.worker_thread.deleteLater()
event.accept() event.accept()