Files
BootRepairTool/frontend.py
2026-02-10 15:00:20 +08:00

290 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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