290 lines
14 KiB
Python
290 lines
14 KiB
Python
# 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()
|
||
|