Docker-OSX/main_app.py

492 lines
29 KiB
Python

# 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())