This commit is contained in:
zj
2026-02-10 15:00:20 +08:00
parent ce31c8b127
commit 70d8ac41b4
3 changed files with 614 additions and 1 deletions

289
frontend.py Normal file
View File

@@ -0,0 +1,289 @@
# frontend.py
import tkinter as tk
from tkinter import ttk, messagebox
import threading
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("<<ComboboxSelected>>", 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:
self.log_message(f"已选择根分区: {self.selected_root_partition_info['name']}", "info")
# 尝试根据根分区所在的磁盘自动选择目标磁盘
# /dev/sda1 -> /dev/sda
disk_name_from_root = "/".join(self.selected_root_partition_info['name'].split('/')[:3])
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):
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")
messagebox.showerror("修复失败", f"无法挂载目标系统分区。请检查分区选择和权限。\n错误: {mount_err}")
self.start_button.config(state="normal")
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")
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")
messagebox.showwarning("卸载警告", f"部分分区可能未能正确卸载。请手动检查并卸载。\n错误: {unmount_err}")
else:
self.log_message("所有分区已成功卸载。", "success")
self.log_message("--- GRUB 修复过程结束 ---", "info")
# 最终结果提示
if repair_ok and unmount_ok:
messagebox.showinfo("修复成功", "GRUB引导修复已完成请重启系统以验证。")
elif repair_ok and not unmount_ok:
messagebox.showwarning("修复完成,但有警告", "GRUB引导修复已完成但部分分区卸载失败。请重启系统以验证。")
else:
messagebox.showerror("修复失败", "GRUB引导修复过程中发生错误。请检查日志并尝试手动修复。")
self.start_button.config(state="normal")
if __name__ == "__main__":
root = tk.Tk()
app = GrubRepairApp(root)
root.mainloop()