# main_app.py import sys import subprocess import os import psutil import platform import ctypes import json import re import traceback # For better error logging import shutil # For shutil.which from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QPushButton, QTextEdit, QMessageBox, QMenuBar, QFileDialog, QGroupBox, QLineEdit, QProgressBar, QCheckBox ) from PyQt6.QtGui import QAction, QIcon from PyQt6.QtCore import pyqtSignal, pyqtSlot, QObject, QThread, QTimer, Qt from constants import APP_NAME, DEVELOPER_NAME, BUSINESS_NAME, MACOS_VERSIONS # DOCKER_IMAGE_BASE and Docker-related utils are no longer primary for this flow. # utils.py might be refactored or parts removed later. # Platform specific USB writers USBWriterLinux = None USBWriterMacOS = None USBWriterWindows = None if platform.system() == "Linux": try: from usb_writer_linux import USBWriterLinux except ImportError as e: print(f"Could not import USBWriterLinux: {e}") elif platform.system() == "Darwin": try: from usb_writer_macos import USBWriterMacOS except ImportError as e: print(f"Could not import USBWriterMacOS: {e}") elif platform.system() == "Windows": try: from usb_writer_windows import USBWriterWindows except ImportError as e: print(f"Could not import USBWriterWindows: {e}") GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "scripts", "gibMacOS", "gibMacOS.py") if not os.path.exists(GIBMACOS_SCRIPT_PATH): GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "gibMacOS.py") class WorkerSignals(QObject): progress = pyqtSignal(str) finished = pyqtSignal(str) error = pyqtSignal(str) progress_value = pyqtSignal(int) class GibMacOSWorker(QObject): signals = WorkerSignals() def __init__(self, version_key: str, download_path: str, catalog_key: str = "publicrelease"): super().__init__() self.version_key = version_key self.download_path = download_path self.catalog_key = catalog_key self.process = None self._is_running = True @pyqtSlot() def run(self): try: script_to_run = "" if os.path.exists(GIBMACOS_SCRIPT_PATH): script_to_run = GIBMACOS_SCRIPT_PATH elif shutil.which("gibMacOS.py"): # Check if it's in PATH script_to_run = "gibMacOS.py" elif os.path.exists(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "gibMacOS.py")): # Check alongside main_app.py script_to_run = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "gibMacOS.py") else: self.signals.error.emit(f"gibMacOS.py not found at expected locations or in PATH.") return version_for_gib = MACOS_VERSIONS.get(self.version_key, self.version_key) os.makedirs(self.download_path, exist_ok=True) command = [sys.executable, script_to_run, "-n", "-c", self.catalog_key, "-v", version_for_gib, "-d", self.download_path] self.signals.progress.emit(f"Downloading macOS '{self.version_key}' (as '{version_for_gib}') installer assets...\nCommand: {' '.join(command)}\nOutput will be in: {self.download_path}\n") self.process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True, creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0 ) if self.process.stdout: for line in iter(self.process.stdout.readline, ''): if not self._is_running: self.signals.progress.emit("macOS download process stopping at user request.\n") break line_strip = line.strip() self.signals.progress.emit(line_strip) progress_match = re.search(r"(\d+)%", line_strip) if progress_match: try: self.signals.progress_value.emit(int(progress_match.group(1))) except ValueError: pass self.process.stdout.close() return_code = self.process.wait() if not self._is_running and return_code != 0: self.signals.finished.emit(f"macOS download cancelled or stopped early (exit code {return_code}).") return if return_code == 0: self.signals.finished.emit(f"macOS '{self.version_key}' installer assets downloaded to '{self.download_path}'.") else: self.signals.error.emit(f"Failed to download macOS '{self.version_key}' (gibMacOS exit code {return_code}). Check logs.") except FileNotFoundError: self.signals.error.emit(f"Error: Python or gibMacOS.py script not found. Ensure Python is in PATH and gibMacOS script is correctly located.") except Exception as e: self.signals.error.emit(f"An error occurred during macOS download: {str(e)}\n{traceback.format_exc()}") finally: self._is_running = False def stop(self): self._is_running = False if self.process and self.process.poll() is None: self.signals.progress.emit("Attempting to stop macOS download (may not be effective for active downloads)...\n") try: self.process.terminate(); self.process.wait(timeout=2) except subprocess.TimeoutExpired: self.process.kill() self.signals.progress.emit("macOS download process termination requested.\n") class USBWriterWorker(QObject): signals = WorkerSignals() def __init__(self, device: str, macos_download_path: str, enhance_plist: bool, target_macos_version: str): super().__init__() self.device = device self.macos_download_path = macos_download_path self.enhance_plist = enhance_plist self.target_macos_version = target_macos_version self.writer_instance = None @pyqtSlot() def run(self): current_os = platform.system() try: writer_cls = None if current_os == "Linux": writer_cls = USBWriterLinux elif current_os == "Darwin": writer_cls = USBWriterMacOS elif current_os == "Windows": writer_cls = USBWriterWindows if writer_cls is None: self.signals.error.emit(f"{current_os} USB writer module not available or OS not supported."); return # Platform writers' __init__ will need to be updated for macos_download_path # This assumes usb_writer_*.py __init__ signatures are now: # __init__(self, device, macos_download_path, progress_callback, enhance_plist_enabled, target_macos_version) self.writer_instance = writer_cls( device=self.device, macos_download_path=self.macos_download_path, progress_callback=lambda msg: self.signals.progress.emit(msg), enhance_plist_enabled=self.enhance_plist, target_macos_version=self.target_macos_version ) if self.writer_instance.format_and_write(): self.signals.finished.emit("USB writing process completed successfully.") else: self.signals.error.emit("USB writing process failed. Check output for details.") except Exception as e: self.signals.error.emit(f"USB writing preparation error: {str(e)}\n{traceback.format_exc()}") class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(APP_NAME) self.setGeometry(100, 100, 800, 700) # Adjusted height self.active_worker_thread = None self.macos_download_path = None self.current_worker_instance = None self.spinner_chars = ["|", "/", "-", "\\"]; self.spinner_index = 0 self.spinner_timer = QTimer(self); self.spinner_timer.timeout.connect(self._update_spinner_status) self.base_status_message = "Ready." self._setup_ui() self.status_bar = self.statusBar() # self.status_bar.addPermanentWidget(self.progress_bar) # Progress bar now in main layout self.status_bar.showMessage(self.base_status_message, 5000) self.refresh_usb_drives() def _setup_ui(self): menubar = self.menuBar(); file_menu = menubar.addMenu("&File"); help_menu = menubar.addMenu("&Help") exit_action = QAction("&Exit", self); exit_action.triggered.connect(self.close); file_menu.addAction(exit_action) about_action = QAction("&About", self); about_action.triggered.connect(self.show_about_dialog); help_menu.addAction(about_action) central_widget = QWidget(); self.setCentralWidget(central_widget); main_layout = QVBoxLayout(central_widget) # Step 1: Download macOS download_group = QGroupBox("Step 1: Download macOS Installer Assets") download_layout = QVBoxLayout() selection_layout = QHBoxLayout(); self.version_label = QLabel("Select macOS Version:"); self.version_combo = QComboBox() self.version_combo.addItems(MACOS_VERSIONS.keys()); selection_layout.addWidget(self.version_label); selection_layout.addWidget(self.version_combo) download_layout.addLayout(selection_layout) self.download_macos_button = QPushButton("Download macOS Installer Assets") self.download_macos_button.clicked.connect(self.start_macos_download_flow) download_layout.addWidget(self.download_macos_button) self.cancel_operation_button = QPushButton("Cancel Current Operation") self.cancel_operation_button.clicked.connect(self.stop_current_operation) self.cancel_operation_button.setEnabled(False) download_layout.addWidget(self.cancel_operation_button) download_group.setLayout(download_layout) main_layout.addWidget(download_group) # Step 2: USB Drive Selection & Writing usb_group = QGroupBox("Step 2: Create Bootable USB Installer") self.usb_layout = QVBoxLayout() self.usb_drive_label = QLabel("Available USB Drives:"); self.usb_layout.addWidget(self.usb_drive_label) usb_selection_layout = QHBoxLayout(); self.usb_drive_combo = QComboBox(); self.usb_drive_combo.currentIndexChanged.connect(self.update_all_button_states) usb_selection_layout.addWidget(self.usb_drive_combo); self.refresh_usb_button = QPushButton("Refresh List"); self.refresh_usb_button.clicked.connect(self.refresh_usb_drives) usb_selection_layout.addWidget(self.refresh_usb_button); self.usb_layout.addLayout(usb_selection_layout) self.windows_usb_guidance_label = QLabel("For Windows: Select USB disk from dropdown (WMI). Manual input below if empty/unreliable.") self.windows_disk_id_input = QLineEdit(); self.windows_disk_id_input.setPlaceholderText("Disk No. (e.g., 1)"); self.windows_disk_id_input.textChanged.connect(self.update_all_button_states) if platform.system() == "Windows": self.usb_layout.addWidget(self.windows_usb_guidance_label); self.usb_layout.addWidget(self.windows_disk_id_input); self.windows_usb_guidance_label.setVisible(True); self.windows_disk_id_input.setVisible(True) else: self.windows_usb_guidance_label.setVisible(False); self.windows_disk_id_input.setVisible(False) self.enhance_plist_checkbox = QCheckBox("Try to auto-enhance config.plist for this system's hardware (Experimental, Linux Host Only for detection)") self.enhance_plist_checkbox.setChecked(False); self.usb_layout.addWidget(self.enhance_plist_checkbox) warning_label = QLabel("WARNING: USB drive will be ERASED!"); warning_label.setStyleSheet("color: red; font-weight: bold;"); self.usb_layout.addWidget(warning_label) self.write_to_usb_button = QPushButton("Create macOS Installer USB"); self.write_to_usb_button.clicked.connect(self.handle_write_to_usb) self.write_to_usb_button.setEnabled(False); self.usb_layout.addWidget(self.write_to_usb_button); usb_group.setLayout(self.usb_layout); main_layout.addWidget(usb_group) self.progress_bar = QProgressBar(self); self.progress_bar.setRange(0, 0); self.progress_bar.setVisible(False); main_layout.addWidget(self.progress_bar) self.output_area = QTextEdit(); self.output_area.setReadOnly(True); main_layout.addWidget(self.output_area) self.update_all_button_states() def show_about_dialog(self): QMessageBox.about(self, f"About {APP_NAME}", f"Version: 1.0.0 (Installer Flow)\nDeveloper: {DEVELOPER_NAME}\nBusiness: {BUSINESS_NAME}\n\nThis tool helps create bootable macOS USB drives using gibMacOS and OpenCore.") def _set_ui_busy(self, busy_status: bool, message: str = "Processing..."): self.progress_bar.setVisible(busy_status) if busy_status: self.base_status_message = message if not self.spinner_timer.isActive(): self.spinner_timer.start(150) self._update_spinner_status() self.progress_bar.setRange(0,0) else: self.spinner_timer.stop() self.status_bar.showMessage(message or "Ready.", 7000) self.update_all_button_states() def _update_spinner_status(self): if self.spinner_timer.isActive(): char = self.spinner_chars[self.spinner_index % len(self.spinner_chars)] active_worker_provides_progress = False if self.active_worker_thread and self.active_worker_thread.isRunning(): active_worker_provides_progress = getattr(self.active_worker_thread, "provides_progress", False) if active_worker_provides_progress and self.progress_bar.maximum() == 100: # Determinate self.status_bar.showMessage(f"{char} {self.base_status_message} ({self.progress_bar.value()}%)") else: if self.progress_bar.maximum() != 0: self.progress_bar.setRange(0,0) self.status_bar.showMessage(f"{char} {self.base_status_message}") self.spinner_index = (self.spinner_index + 1) % len(self.spinner_chars) elif not (self.active_worker_thread and self.active_worker_thread.isRunning()): self.spinner_timer.stop() def update_all_button_states(self): is_worker_active = self.active_worker_thread is not None and self.active_worker_thread.isRunning() self.download_macos_button.setEnabled(not is_worker_active) self.version_combo.setEnabled(not is_worker_active) self.cancel_operation_button.setEnabled(is_worker_active and self.current_worker_instance is not None) self.refresh_usb_button.setEnabled(not is_worker_active) self.usb_drive_combo.setEnabled(not is_worker_active) if platform.system() == "Windows": self.windows_disk_id_input.setEnabled(not is_worker_active) self.enhance_plist_checkbox.setEnabled(not is_worker_active) # Write to USB button logic macos_assets_ready = bool(self.macos_download_path and os.path.isdir(self.macos_download_path)) usb_identified = False current_os = platform.system(); writer_module = None if current_os == "Linux": writer_module = USBWriterLinux; usb_identified = bool(self.usb_drive_combo.currentData()) elif current_os == "Darwin": writer_module = USBWriterMacOS; usb_identified = bool(self.usb_drive_combo.currentData()) elif current_os == "Windows": writer_module = USBWriterWindows usb_identified = bool(self.usb_drive_combo.currentData()) or bool(self.windows_disk_id_input.text().strip()) self.write_to_usb_button.setEnabled(not is_worker_active and macos_assets_ready and usb_identified and writer_module is not None) tooltip = "" if writer_module is None: tooltip = f"USB Writing not supported on {current_os} or module missing." elif not macos_assets_ready: tooltip = "Download macOS installer assets first (Step 1)." elif not usb_identified: tooltip = "Select or identify a target USB drive." else: tooltip = "" self.write_to_usb_button.setToolTip(tooltip) def _start_worker(self, worker_instance, on_finished_slot, on_error_slot, worker_name="worker", provides_progress=False): if self.active_worker_thread and self.active_worker_thread.isRunning(): QMessageBox.warning(self, "Busy", "Another operation is in progress."); return False self._set_ui_busy(True, f"Starting {worker_name.replace('_', ' ')}...") self.current_worker_instance = worker_instance if provides_progress: self.progress_bar.setRange(0,100) worker_instance.signals.progress_value.connect(self.update_progress_bar_value) else: self.progress_bar.setRange(0,0) self.active_worker_thread = QThread(); self.active_worker_thread.setObjectName(worker_name + "_thread") setattr(self.active_worker_thread, "provides_progress", provides_progress) worker_instance.moveToThread(self.active_worker_thread) worker_instance.signals.progress.connect(self.update_output) worker_instance.signals.finished.connect(lambda msg, wn=worker_name, slot=on_finished_slot: self._handle_worker_finished(msg, wn, slot)) worker_instance.signals.error.connect(lambda err, wn=worker_name, slot=on_error_slot: self._handle_worker_error(err, wn, slot)) self.active_worker_thread.finished.connect(self.active_worker_thread.deleteLater) self.active_worker_thread.started.connect(worker_instance.run) self.active_worker_thread.start() return True @pyqtSlot(int) def update_progress_bar_value(self, value): if self.progress_bar.maximum() == 0: self.progress_bar.setRange(0,100) self.progress_bar.setValue(value) # Spinner update will happen on its timer, it can check progress_bar.value() def _handle_worker_finished(self, message, worker_name, specific_finished_slot): final_msg = f"{worker_name.replace('_', ' ').capitalize()} completed." self.current_worker_instance = None # Clear current worker self.active_worker_thread = None if specific_finished_slot: specific_finished_slot(message) self._set_ui_busy(False, final_msg) def _handle_worker_error(self, error_message, worker_name, specific_error_slot): final_msg = f"{worker_name.replace('_', ' ').capitalize()} failed." self.current_worker_instance = None # Clear current worker self.active_worker_thread = None if specific_error_slot: specific_error_slot(error_message) self._set_ui_busy(False, final_msg) def start_macos_download_flow(self): self.output_area.clear(); selected_version_name = self.version_combo.currentText() gibmacos_version_arg = MACOS_VERSIONS.get(selected_version_name, selected_version_name) chosen_path = QFileDialog.getExistingDirectory(self, "Select Directory to Download macOS Installer Assets") if not chosen_path: self.output_area.append("Download directory selection cancelled."); return self.macos_download_path = chosen_path worker = GibMacOSWorker(gibmacos_version_arg, self.macos_download_path) if not self._start_worker(worker, self.macos_download_finished, self.macos_download_error, "macos_download", f"Downloading macOS {selected_version_name} assets...", provides_progress=True): # Assuming GibMacOSWorker will emit progress_value self._set_ui_busy(False, "Failed to start macOS download operation.") @pyqtSlot(str) def macos_download_finished(self, message): QMessageBox.information(self, "Download Complete", message) # self.macos_download_path is set. UI update handled by generic handler. @pyqtSlot(str) def macos_download_error(self, error_message): QMessageBox.critical(self, "Download Error", error_message) self.macos_download_path = None # UI reset by generic handler. def stop_current_operation(self): if self.current_worker_instance and hasattr(self.current_worker_instance, 'stop'): self.output_area.append(f" --- Attempting to stop {self.active_worker_thread.objectName().replace('_thread','')} ---") self.current_worker_instance.stop() else: self.output_area.append(" --- No active stoppable operation or stop method not implemented for current worker. ---") def handle_error(self, message): self.output_area.append(f"ERROR: {message}"); QMessageBox.critical(self, "Error", message) self._set_ui_busy(False, "Error occurred.") def check_admin_privileges(self) -> bool: # ... (same) try: if platform.system() == "Windows": return ctypes.windll.shell32.IsUserAnAdmin() != 0 else: return os.geteuid() == 0 except Exception as e: self.output_area.append(f"Could not check admin privileges: {e}"); return False def refresh_usb_drives(self): # ... (same logic as before) self.usb_drive_combo.clear(); current_selection_text = getattr(self, '_current_usb_selection_text', None) self.output_area.append(" Scanning for disk devices...") if platform.system() == "Windows": self.usb_drive_label.setText("Available USB Disks (Windows - via WMI/PowerShell):") self.windows_usb_guidance_label.setVisible(True); self.windows_disk_id_input.setVisible(False); powershell_command = "Get-WmiObject Win32_DiskDrive | Where-Object {$_.InterfaceType -eq 'USB'} | Select-Object DeviceID, Index, Model, @{Name='SizeGB';Expression={[math]::Round($_.Size / 1GB, 2)}} | ConvertTo-Json" try: process = subprocess.run(["powershell", "-Command", powershell_command], capture_output=True, text=True, check=True, creationflags=subprocess.CREATE_NO_WINDOW) disks_data = json.loads(process.stdout); disks_json = disks_data if isinstance(disks_data, list) else [disks_data] if disks_data else [] if disks_json: for disk in disks_json: if disk.get('DeviceID') is None or disk.get('Index') is None: continue disk_text = f"Disk {disk['Index']}: {disk.get('Model','N/A')} ({disk.get('SizeGB','N/A')} GB) - {disk['DeviceID']}" self.usb_drive_combo.addItem(disk_text, userData=str(disk['Index'])) self.output_area.append(f"Found {len(disks_json)} USB disk(s) via WMI."); if current_selection_text: for i in range(self.usb_drive_combo.count()): if self.usb_drive_combo.itemText(i) == current_selection_text: self.usb_drive_combo.setCurrentIndex(i); break else: self.output_area.append("No USB disks found via WMI/PowerShell. Manual input field shown as fallback."); self.windows_disk_id_input.setVisible(True) except Exception as e: self.output_area.append(f"Error scanning Windows USBs with PowerShell: {e}"); self.windows_disk_id_input.setVisible(True) else: self.usb_drive_label.setText("Available USB Drives (for Linux/macOS):") self.windows_usb_guidance_label.setVisible(False); self.windows_disk_id_input.setVisible(False) try: partitions = psutil.disk_partitions(all=False); potential_usbs = [] for p in partitions: is_removable = 'removable' in p.opts; is_likely_usb = False if platform.system() == "Darwin" and p.device.startswith("/dev/disk") and 'external' in p.opts.lower() and 'physical' in p.opts.lower(): is_likely_usb = True elif platform.system() == "Linux" and ((p.mountpoint and ("/media/" in p.mountpoint or "/run/media/" in p.mountpoint)) or (p.device.startswith("/dev/sd") and not p.device.endswith("da"))): is_likely_usb = True if is_removable or is_likely_usb: try: usage = psutil.disk_usage(p.mountpoint); size_gb = usage.total / (1024**3) except Exception: continue if size_gb < 0.1 : continue drive_text = f"{p.device} @ {p.mountpoint} ({p.fstype}, {size_gb:.2f} GB)" potential_usbs.append((drive_text, p.device)) if potential_usbs: idx_to_select = -1 for i, (text, device_path) in enumerate(potential_usbs): self.usb_drive_combo.addItem(text, userData=device_path); if text == current_selection_text: idx_to_select = i if idx_to_select != -1: self.usb_drive_combo.setCurrentIndex(idx_to_select) self.output_area.append(f"Found {len(potential_usbs)} potential USB drive(s). Please verify carefully.") else: self.output_area.append("No suitable USB drives found for Linux/macOS.") except ImportError: self.output_area.append("psutil library not found.") except Exception as e: self.output_area.append(f"Error scanning for USB drives: {e}") self.update_all_button_states() def handle_write_to_usb(self): if not self.check_admin_privileges(): QMessageBox.warning(self, "Privileges Required", "This operation requires Administrator/root privileges."); return if not self.macos_download_path or not os.path.isdir(self.macos_download_path): QMessageBox.warning(self, "Missing macOS Assets", "Download macOS installer assets first."); return current_os = platform.system(); usb_writer_module = None; target_device_id_for_worker = None if current_os == "Windows": target_device_id_for_worker = self.usb_drive_combo.currentData() or self.windows_disk_id_input.text().strip(); usb_writer_module = USBWriterWindows else: target_device_id_for_worker = self.usb_drive_combo.currentData(); usb_writer_module = USBWriterLinux if current_os == "Linux" else USBWriterMacOS if current_os == "Darwin" else None if not usb_writer_module: QMessageBox.warning(self, "Unsupported Platform", f"USB writing not supported for {current_os}."); return if not target_device_id_for_worker: QMessageBox.warning(self, "No USB Selected/Identified", f"Please select/identify target USB."); return if current_os == "Windows" and target_device_id_for_worker.isdigit(): target_device_id_for_worker = f"disk {target_device_id_for_worker}" enhance_plist_state = self.enhance_plist_checkbox.isChecked() target_macos_name = self.version_combo.currentText() reply = QMessageBox.warning(self, "Confirm Write Operation", f"WARNING: ALL DATA ON TARGET '{target_device_id_for_worker}' WILL BE ERASED. Proceed?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Cancel) if reply == QMessageBox.StandardButton.Cancel: self.output_area.append(" USB write cancelled."); return # USBWriterWorker now needs different args # The platform specific writers (USBWriterLinux etc) will need to be updated to accept macos_download_path # and use it to find BaseSystem.dmg, EFI/OC etc. instead of opencore_qcow2_path, macos_qcow2_path usb_worker_adapted = USBWriterWorker( device=target_device_id_for_worker, macos_download_path=self.macos_download_path, enhance_plist=enhance_plist_state, target_macos_version=target_macos_name ) if not self._start_worker(usb_worker_adapted, self.usb_write_finished, self.usb_write_error, "usb_write_worker", busy_message=f"Creating USB for {target_device_id_for_worker}...", provides_progress=False): # USB writing can be long, but progress parsing is per-platform script. self._set_ui_busy(False, "Failed to start USB write operation.") @pyqtSlot(str) def usb_write_finished(self, message): QMessageBox.information(self, "USB Write Complete", message) @pyqtSlot(str) def usb_write_error(self, error_message): QMessageBox.critical(self, "USB Write Error", error_message) def closeEvent(self, event): # ... (same logic) self._current_usb_selection_text = self.usb_drive_combo.currentText() if self.active_worker_thread and self.active_worker_thread.isRunning(): reply = QMessageBox.question(self, 'Confirm Exit', "An operation is running. Exit anyway?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No) if reply == QMessageBox.StandardButton.Yes: if self.current_worker_instance and hasattr(self.current_worker_instance, 'stop'): self.current_worker_instance.stop() else: self.active_worker_thread.quit() self.active_worker_thread.wait(1000); event.accept() else: event.ignore(); return else: event.accept() if __name__ == "__main__": import traceback # Ensure traceback is available for GibMacOSWorker import shutil # Ensure shutil is available for GibMacOSWorker path check app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec())