# frontend.py import tkinter as tk from tkinter import ttk, messagebox import threading import os import re import backend # 导入后端逻辑 class GrubRepairApp: def __init__(self, master): self.master = master master.title("Linux GRUB 引导修复工具") master.geometry("850x650") # 调整窗口大小 master.resizable(True, True) # 允许窗口大小调整 self.mount_point = None self.selected_root_partition_info = None # 存储字典信息 self.selected_boot_partition_info = None self.selected_efi_partition_info = None self.selected_target_disk_info = None self.is_uefi_mode = False self.all_disks_data = {} # {display_name: {"name": "/dev/sda"}} self.all_partitions_data = {} # {display_name: {"name": "/dev/sda1", ...}} self.all_efi_partitions_data = {} # {display_name: {"name": "/dev/sda1", ...}} self.create_widgets() self.scan_partitions_gui() def create_widgets(self): # Frame for partition selection self.partition_frame = ttk.LabelFrame(self.master, text="1. 选择目标系统分区", padding="10") self.partition_frame.pack(padx=10, pady=5, fill="x") self.partition_frame.columnconfigure(1, weight=1) # 让 Combobox 填充宽度 ttk.Label(self.partition_frame, text="根分区 (/):").grid(row=0, column=0, padx=5, pady=2, sticky="w") self.root_partition_combo = ttk.Combobox(self.partition_frame, state="readonly") self.root_partition_combo.grid(row=0, column=1, padx=5, pady=2, sticky="ew") self.root_partition_combo.bind("<>", self.on_root_partition_selected) ttk.Label(self.partition_frame, text="独立 /boot 分区 (可选):").grid(row=1, column=0, padx=5, pady=2, sticky="w") self.boot_partition_combo = ttk.Combobox(self.partition_frame, state="readonly") self.boot_partition_combo.grid(row=1, column=1, padx=5, pady=2, sticky="ew") ttk.Label(self.partition_frame, text="EFI 系统分区 (ESP, 仅UEFI):").grid(row=2, column=0, padx=5, pady=2, sticky="w") self.efi_partition_combo = ttk.Combobox(self.partition_frame, state="readonly") self.efi_partition_combo.grid(row=2, column=1, padx=5, pady=2, sticky="ew") # Frame for target disk selection self.disk_frame = ttk.LabelFrame(self.master, text="2. 选择GRUB安装目标磁盘", padding="10") self.disk_frame.pack(padx=10, pady=5, fill="x") self.disk_frame.columnconfigure(1, weight=1) ttk.Label(self.disk_frame, text="目标磁盘 (例如 /dev/sda):").grid(row=0, column=0, padx=5, pady=2, sticky="w") self.target_disk_combo = ttk.Combobox(self.disk_frame, state="readonly") self.target_disk_combo.grid(row=0, column=1, padx=5, pady=2, sticky="ew") self.uefi_mode_var = tk.BooleanVar(value=False) self.uefi_check = ttk.Checkbutton(self.disk_frame, text="目标系统使用UEFI启动 (Live环境检测为UEFI)", variable=self.uefi_mode_var) self.uefi_check.grid(row=1, column=0, columnspan=2, padx=5, pady=2, sticky="w") # 尝试自动检测Live环境是否为UEFI if os.path.exists("/sys/firmware/efi"): self.uefi_mode_var.set(True) self.uefi_check.config(state="disabled") # 如果Live是UEFI,通常目标系统也是,且不建议更改 self.is_uefi_mode = True # 直接设置 else: self.uefi_check.config(state="normal") self.is_uefi_mode = False # 直接设置 self.uefi_check.config(text="目标系统使用UEFI启动 (Live环境检测为BIOS)") self.uefi_mode_var.trace_add("write", self.on_uefi_check_changed) # Start Repair Button self.start_button = ttk.Button(self.master, text="3. 开始修复 GRUB", command=self.start_repair_thread) self.start_button.pack(padx=10, pady=10, fill="x") # Progress and Log self.log_frame = ttk.LabelFrame(self.master, text="修复日志", padding="10") self.log_frame.pack(padx=10, pady=5, fill="both", expand=True) self.log_text = tk.Text(self.log_frame, wrap="word", height=15, state="disabled", font=("Monospace", 10)) self.log_text.pack(padx=5, pady=5, fill="both", expand=True) self.log_scrollbar = ttk.Scrollbar(self.log_text, command=self.log_text.yview) self.log_scrollbar.pack(side="right", fill="y") self.log_text.config(yscrollcommand=self.log_scrollbar.set) def log_message(self, message, level="info"): """ 线程安全的日志记录函数。 """ self.master.after(0, self._append_log, message, level) def _append_log(self, message, level): self.log_text.config(state="normal") tag = "info" if level == "error": tag = "error" self.log_text.tag_config("error", foreground="red") elif level == "warning": tag = "warning" self.log_text.tag_config("warning", foreground="orange") elif level == "success": tag = "success" self.log_text.tag_config("success", foreground="green") self.log_text.insert(tk.END, message + "\n", tag) self.log_text.see(tk.END) self.log_text.config(state="disabled") def scan_partitions_gui(self): self.log_message("正在扫描分区...", "info") success, disks, partitions, efi_partitions, err = backend.scan_partitions() if not success: self.log_message(f"分区扫描失败: {err}", "error") messagebox.showerror("错误", f"无法扫描分区: {err}\n请确保您有sudo权限并已安装lsblk。") return # 清空旧数据 self.all_disks_data = {} self.all_partitions_data = {} self.all_efi_partitions_data = {} disk_display_names = [] for d in disks: display_name = d["name"] self.all_disks_data[display_name] = d disk_display_names.append(display_name) self.target_disk_combo['values'] = disk_display_names partition_display_names = [] for p in partitions: display_name = f"{p['name']} ({p['fstype']} - {p['size']})" self.all_partitions_data[display_name] = p partition_display_names.append(display_name) self.root_partition_combo['values'] = partition_display_names self.boot_partition_combo['values'] = [""] + partition_display_names # 允许不选择独立/boot efi_partition_display_names = [] for p in efi_partitions: display_name = f"{p['name']} ({p['fstype']} - {p['size']})" self.all_efi_partitions_data[display_name] = p efi_partition_display_names.append(display_name) self.efi_partition_combo['values'] = [""] + efi_partition_display_names # 允许不选择EFI分区 self.log_message("分区扫描完成。请选择目标系统分区。", "info") def on_root_partition_selected(self, event): selected_display = self.root_partition_combo.get() self.selected_root_partition_info = self.all_partitions_data.get(selected_display) if self.selected_root_partition_info: root_part_name = self.selected_root_partition_info['name'] self.log_message(f"已选择根分区: {root_part_name}", "info") # 检查是否是 LVM 逻辑卷 is_lvm = self.selected_root_partition_info.get('is_lvm', False) if is_lvm: # LVM 逻辑卷无法直接从路径推断父磁盘,提示用户手动选择 self.target_disk_combo.set("") self.selected_target_disk_info = None self.log_message("检测到 LVM 逻辑卷,请手动选择 GRUB 安装的目标磁盘。", "warning") else: # 尝试根据根分区所在的磁盘自动选择目标磁盘 # /dev/sda1 -> /dev/sda # /dev/nvme0n1p1 -> /dev/nvme0n1 # /dev/mmcblk0p1 -> /dev/mmcblk0 disk_name_from_root = re.sub(r'(\d+)?p?\d+$', '', root_part_name) if disk_name_from_root in self.target_disk_combo['values']: self.target_disk_combo.set(disk_name_from_root) self.selected_target_disk_info = self.all_disks_data.get(disk_name_from_root) self.log_message(f"自动选择目标磁盘: {self.selected_target_disk_info['name']}", "info") else: self.target_disk_combo.set("") self.selected_target_disk_info = None self.log_message("无法自动选择目标磁盘,请手动选择。", "warning") else: self.selected_root_partition_info = None self.target_disk_combo.set("") self.selected_target_disk_info = None def on_uefi_check_changed(self, *args): self.is_uefi_mode = self.uefi_mode_var.get() self.log_message(f"UEFI模式选择: {'启用' if self.is_uefi_mode else '禁用'}", "info") def start_repair_thread(self): # 禁用按钮防止重复点击 self.start_button.config(state="disabled") # 清空日志 self.log_text.config(state="normal") self.log_text.delete(1.0, tk.END) self.log_text.config(state="disabled") self.log_message("--- GRUB 修复过程开始 ---", "info") # 获取用户选择 self.selected_root_partition_info = self.all_partitions_data.get(self.root_partition_combo.get()) self.selected_boot_partition_info = self.all_partitions_data.get(self.boot_partition_combo.get()) self.selected_efi_partition_info = self.all_efi_partitions_data.get(self.efi_partition_combo.get()) self.selected_target_disk_info = self.all_disks_data.get(self.target_disk_combo.get()) self.is_uefi_mode = self.uefi_mode_var.get() if not self.selected_root_partition_info: messagebox.showerror("错误", "请选择目标系统的根分区!") self.log_message("未选择根分区,修复中止。", "error") self.start_button.config(state="normal") return if not self.selected_target_disk_info: messagebox.showerror("错误", "请选择GRUB要安装到的目标磁盘!") self.log_message("未选择目标磁盘,修复中止。", "error") self.start_button.config(state="normal") return # 再次确认UEFI模式下是否选择了EFI分区 if self.is_uefi_mode and not self.selected_efi_partition_info: response = messagebox.askyesno("警告", "您选择了UEFI模式,但未选择EFI系统分区 (ESP)。这可能导致修复失败。是否继续?") if not response: self.log_message("用户取消,未选择EFI分区。", "warning") self.start_button.config(state="normal") return # 启动新线程进行修复,避免UI卡死 repair_thread = threading.Thread(target=self._run_repair_process) repair_thread.start() def _run_repair_process(self): try: root_part_path = self.selected_root_partition_info['name'] boot_part_path = self.selected_boot_partition_info['name'] if self.selected_boot_partition_info else None efi_part_path = self.selected_efi_partition_info['name'] if self.selected_efi_partition_info else None target_disk_path = self.selected_target_disk_info['name'] self.log_message(f"选择的根分区: {root_part_path}", "info") self.log_message(f"选择的独立 /boot 分区: {boot_part_path if boot_part_path else '无'}", "info") self.log_message(f"选择的EFI分区: {efi_part_path if efi_part_path else '无'}", "info") self.log_message(f"GRUB安装目标磁盘: {target_disk_path}", "info") self.log_message(f"UEFI模式: {'启用' if self.is_uefi_mode else '禁用'}", "info") # 1. 挂载目标系统 self.log_message("正在挂载目标系统分区...", "info") mount_ok, self.mount_point, mount_err = backend.mount_target_system( root_part_path, boot_part_path, efi_part_path if self.is_uefi_mode else None # 只有UEFI模式才挂载EFI分区 ) if not mount_ok: self.log_message(f"挂载失败,修复中止: {mount_err}", "error") self.master.after(0, lambda: messagebox.showerror("修复失败", f"无法挂载目标系统分区。请检查分区选择和权限。\n错误: {mount_err}")) return # 2. 尝试识别发行版类型 self.log_message("正在检测目标系统发行版...", "info") distro_type = backend.detect_distro_type(self.mount_point) self.log_message(f"检测到目标系统发行版: {distro_type}", "info") # 3. Chroot并修复GRUB self.log_message("正在进入Chroot环境并修复GRUB...", "info") repair_ok, repair_err = backend.chroot_and_repair_grub( self.mount_point, target_disk_path, self.is_uefi_mode, distro_type ) if not repair_ok: self.log_message(f"GRUB修复失败: {repair_err}", "error") self.master.after(0, lambda: messagebox.showerror("修复失败", f"GRUB修复过程中发生错误。\n错误: {repair_err}")) # 即使失败,也要尝试卸载 else: self.log_message("GRUB修复命令执行成功!", "success") # 4. 卸载分区 self.log_message("正在卸载目标系统分区...", "info") unmount_ok, unmount_err = backend.unmount_target_system(self.mount_point) if not unmount_ok: self.log_message(f"卸载分区失败: {unmount_err}", "error") self.master.after(0, lambda: messagebox.showwarning("卸载警告", f"部分分区可能未能正确卸载。请手动检查并卸载。\n错误: {unmount_err}")) else: self.log_message("所有分区已成功卸载。", "success") self.log_message("--- GRUB 修复过程结束 ---", "info") # 最终结果提示 if repair_ok and unmount_ok: self.master.after(0, lambda: messagebox.showinfo("修复成功", "GRUB引导修复已完成!请重启系统以验证。")) elif repair_ok and not unmount_ok: self.master.after(0, lambda: messagebox.showwarning("修复完成,但有警告", "GRUB引导修复已完成,但部分分区卸载失败。请重启系统以验证。")) else: self.master.after(0, lambda: messagebox.showerror("修复失败", "GRUB引导修复过程中发生错误。请检查日志并尝试手动修复。")) except Exception as e: self.log_message(f"发生未预期的错误: {str(e)}", "error") self.master.after(0, lambda err=str(e): messagebox.showerror("错误", f"修复过程中发生错误:\n{err}")) finally: # 确保按钮状态被恢复 self.master.after(0, lambda: self.start_button.config(state="normal")) if __name__ == "__main__": root = tk.Tk() app = GrubRepairApp(root) root.mainloop()