This commit is contained in:
zj
2026-02-11 04:37:13 +08:00
parent 6645d8dea3
commit a859956597
5 changed files with 2476 additions and 409 deletions

View File

@@ -1,124 +1,268 @@
# frontend.py
# 参考 Calamares 引导安装模块优化
import tkinter as tk
from tkinter import ttk, messagebox
from tkinter import ttk, messagebox, scrolledtext
import threading
import os
import re
import backend # 导入后端逻辑
import backend
class GrubRepairApp:
def __init__(self, master):
self.master = master
master.title("Linux GRUB 引导修复工具")
master.geometry("850x650") # 调整窗口大小
master.resizable(True, True) # 允许窗口大小调整
master.title("Linux GRUB 引导修复工具 v2.0")
master.geometry("900x750")
master.resizable(True, True)
# 设置窗口最小大小
master.minsize(800, 600)
self.mount_point = None
self.selected_root_partition_info = 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.btrfs_subvolume = None
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.all_disks_data = {}
self.all_partitions_data = {}
self.all_efi_partitions_data = {}
# 系统信息
self.system_info = {}
self._detect_system_info()
self.create_widgets()
self.scan_partitions_gui()
# 设置后端日志回调
backend.set_log_callback(self._backend_log_callback)
def _detect_system_info(self):
"""检测系统信息"""
self.system_info["efi_bits"] = backend.get_efi_word_size()
self.system_info["cpu_arch"] = __import__('platform').machine()
self.system_info["is_live"] = backend.is_live_environment()
sb_supported, sb_enabled = backend.check_secure_boot_status()
self.system_info["sb_supported"] = sb_supported
self.system_info["sb_enabled"] = sb_enabled
# 检测 Live 环境 UEFI 状态
self.system_info["live_is_uefi"] = os.path.exists("/sys/firmware/efi")
def create_widgets(self):
# Frame for partition selection
self.partition_frame = ttk.LabelFrame(self.master, text="1. 选择目标系统分区", padding="10")
"""创建 GUI 组件"""
# 创建主滚动区域
main_canvas = tk.Canvas(self.master)
scrollbar = ttk.Scrollbar(self.master, orient="vertical", command=main_canvas.yview)
self.main_frame = ttk.Frame(main_canvas)
self.main_frame.bind(
"<Configure>",
lambda e: main_canvas.configure(scrollregion=main_canvas.bbox("all"))
)
main_canvas.create_window((0, 0), window=self.main_frame, anchor="nw")
main_canvas.configure(yscrollcommand=scrollbar.set)
main_canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# 鼠标滚轮支持
def on_mousewheel(event):
main_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
main_canvas.bind_all("<MouseWheel>", on_mousewheel)
# ===== 系统信息区域 =====
self.info_frame = ttk.LabelFrame(self.main_frame, text="系统信息", padding="10")
self.info_frame.pack(padx=10, pady=5, fill="x")
info_text = f"""
架构: {self.system_info.get('cpu_arch', 'Unknown')} |
EFI位数: {self.system_info.get('efi_bits', 'Unknown')}位 |
Live环境: {'' if self.system_info.get('is_live') else ''} |
Secure Boot: {'已启用' if self.system_info.get('sb_enabled') else '已禁用/不支持'}
"""
ttk.Label(self.info_frame, text=info_text.strip()).pack(anchor="w")
# ===== 分区选择区域 =====
self.partition_frame = ttk.LabelFrame(self.main_frame, text="1. 选择目标系统分区", padding="10")
self.partition_frame.pack(padx=10, pady=5, fill="x")
self.partition_frame.columnconfigure(1, weight=1) # 让 Combobox 填充宽度
self.partition_frame.columnconfigure(1, weight=1)
ttk.Label(self.partition_frame, text="根分区 (/)").grid(row=0, column=0, padx=5, pady=2, sticky="w")
# 根分区
ttk.Label(self.partition_frame, text="根分区 (/) *").grid(row=0, column=0, padx=5, pady=5, 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.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
self.root_partition_combo.bind("<<ComboboxSelected>>", self.on_root_partition_selected)
# btrfs 子卷选择(初始隐藏)
self.btrfs_frame = ttk.Frame(self.partition_frame)
self.btrfs_frame.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=2)
self.btrfs_frame.grid_remove() # 初始隐藏
ttk.Label(self.btrfs_frame, text="Btrfs 子卷:").pack(side="left", padx=(0, 5))
self.btrfs_subvolume_combo = ttk.Combobox(self.btrfs_frame, state="readonly", width=30)
self.btrfs_subvolume_combo.pack(side="left", fill="x", expand=True)
ttk.Button(self.btrfs_frame, text="刷新", command=self._refresh_btrfs_subvolumes).pack(side="left", padx=(5, 0))
ttk.Label(self.partition_frame, text="独立 /boot 分区 (可选)").grid(row=1, column=0, padx=5, pady=2, sticky="w")
# /boot 分区
ttk.Label(self.partition_frame, text="独立 /boot 分区:").grid(row=2, column=0, padx=5, pady=5, 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")
self.boot_partition_combo.grid(row=2, column=1, padx=5, pady=5, sticky="ew")
ttk.Label(self.partition_frame, text="EFI 系统分区 (ESP, 仅UEFI)").grid(row=2, column=0, padx=5, pady=2, sticky="w")
# EFI 分区
ttk.Label(self.partition_frame, text="EFI 系统分区 (ESP)").grid(row=3, column=0, padx=5, pady=5, 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")
self.efi_partition_combo.grid(row=3, column=1, padx=5, pady=5, sticky="ew")
# Frame for target disk selection
self.disk_frame = ttk.LabelFrame(self.master, text="2. 选择GRUB安装目标磁盘", padding="10")
# ===== 目标磁盘选择 =====
self.disk_frame = ttk.LabelFrame(self.main_frame, 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")
ttk.Label(self.disk_frame, text="目标磁盘 *").grid(row=0, column=0, padx=5, pady=5, 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.target_disk_combo.grid(row=0, column=1, padx=5, pady=5, 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")
# UEFI 模式选择
self.uefi_mode_var = tk.BooleanVar(value=self.system_info.get("live_is_uefi", False))
self.is_uefi_mode = self.uefi_mode_var.get()
# 尝试自动检测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)")
uefi_text = f"使用 UEFI 模式 (Live环境检测为{'UEFI' if self.system_info.get('live_is_uefi') else 'BIOS'})"
self.uefi_check = ttk.Checkbutton(
self.disk_frame,
text=uefi_text,
variable=self.uefi_mode_var,
command=self.on_uefi_mode_changed
)
self.uefi_check.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="w")
self.uefi_mode_var.trace_add("write", self.on_uefi_check_changed)
# 如果 Live 环境是 UEFI提示用户
if self.system_info.get("live_is_uefi"):
ttk.Label(
self.disk_frame,
text="⚠️ Live 环境以 UEFI 模式启动,建议保持 UEFI 模式选中",
foreground="orange"
).grid(row=2, column=0, columnspan=2, padx=5, sticky="w")
# ===== 高级选项 =====
self.advanced_frame = ttk.LabelFrame(self.main_frame, text="3. 高级选项", padding="10")
self.advanced_frame.pack(padx=10, pady=5, fill="x")
# 混合启动模式
self.hybrid_mode_var = tk.BooleanVar(value=False)
self.hybrid_check = ttk.Checkbutton(
self.advanced_frame,
text="混合启动模式(同时安装 BIOS 和 UEFI 引导)",
variable=self.hybrid_mode_var
)
self.hybrid_check.grid(row=0, column=0, columnspan=2, padx=5, pady=2, sticky="w")
# EFI Fallback
self.fallback_var = tk.BooleanVar(value=True)
self.fallback_check = ttk.Checkbutton(
self.advanced_frame,
text="安装 EFI Fallback推荐提高兼容性",
variable=self.fallback_var
)
self.fallback_check.grid(row=1, column=0, columnspan=2, padx=5, pady=2, sticky="w")
# EFI Bootloader ID
ttk.Label(self.advanced_frame, text="EFI 启动项名称:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
self.bootloader_id_var = tk.StringVar(value="GRUB")
self.bootloader_id_entry = ttk.Entry(self.advanced_frame, textvariable=self.bootloader_id_var, width=20)
self.bootloader_id_entry.grid(row=2, column=1, padx=5, pady=5, sticky="w")
# 额外选项
self.force_install_var = tk.BooleanVar(value=True)
ttk.Checkbutton(
self.advanced_frame,
text="强制安装(--force",
variable=self.force_install_var
).grid(row=3, column=0, columnspan=2, padx=5, pady=2, sticky="w")
# 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")
# ===== 操作按钮 =====
self.button_frame = ttk.Frame(self.main_frame)
self.button_frame.pack(padx=10, pady=10, fill="x")
self.start_button = ttk.Button(
self.button_frame,
text="🔧 开始修复 GRUB",
command=self.start_repair_thread
)
self.start_button.pack(side="left", padx=(0, 5))
self.rescan_button = ttk.Button(
self.button_frame,
text="🔄 重新扫描分区",
command=self.scan_partitions_gui
)
self.rescan_button.pack(side="left", padx=5)
# Progress and Log
self.log_frame = ttk.LabelFrame(self.master, text="修复日志", padding="10")
# ===== 日志区域 =====
self.log_frame = ttk.LabelFrame(self.main_frame, 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 = scrolledtext.ScrolledText(
self.log_frame,
wrap="word",
height=15,
state="disabled",
font=("Consolas", 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)
# 配置日志标签颜色
self.log_text.tag_config("debug", foreground="gray")
self.log_text.tag_config("info", foreground="black")
self.log_text.tag_config("warning", foreground="orange")
self.log_text.tag_config("error", foreground="red")
self.log_text.tag_config("success", foreground="green")
self.log_text.tag_config("step", foreground="blue", font=("Consolas", 10, "bold"))
def log_message(self, message, level="info"):
"""
线程安全的日志记录函数。
"""
def _backend_log_callback(self, message: str, level):
"""后端日志回调函数"""
level_str = level.value if hasattr(level, 'value') else str(level).lower()
self._append_log(message, level_str)
def log_message(self, message: str, level: str = "info"):
"""线程安全的日志记录"""
self.master.after(0, self._append_log, message, level)
def _append_log(self, message, level):
def _append_log(self, message: str, level: str = "info"):
"""添加日志到文本框"""
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")
# 添加时间戳
import datetime
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
log_line = f"[{timestamp}] {message}\n"
self.log_text.insert(tk.END, message + "\n", tag)
self.log_text.insert(tk.END, log_line, level)
self.log_text.see(tk.END)
self.log_text.config(state="disabled")
def scan_partitions_gui(self):
"""扫描分区"""
self.log_message("正在扫描分区...", "info")
self.rescan_button.config(state="disabled")
# 清空现有数据
self.root_partition_combo.set('')
self.boot_partition_combo.set('')
self.efi_partition_combo.set('')
self.target_disk_combo.set('')
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。")
self.rescan_button.config(state="normal")
return
# 清空旧数据
@@ -126,31 +270,46 @@ class GrubRepairApp:
self.all_partitions_data = {}
self.all_efi_partitions_data = {}
# 更新磁盘列表
disk_display_names = []
for d in disks:
display_name = d["name"]
display_name = f"{d['name']} ({d.get('size', 'unknown')})"
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']})"
extra_info = []
if p.get('is_lvm'):
extra_info.append("LVM")
if p.get('is_btrfs'):
extra_info.append("Btrfs")
if p.get('is_luks'):
extra_info.append("LUKS")
extra_str = f" [{', '.join(extra_info)}]" if extra_info else ""
display_name = f"{p['name']} ({p['fstype']} - {p['size']}){extra_str}"
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
self.boot_partition_combo['values'] = [""] + partition_display_names
# 更新 EFI 分区列表
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.efi_partition_combo['values'] = [""] + efi_partition_display_names
self.log_message("分区扫描完成。请选择目标系统分区", "info")
def on_root_partition_selected(self, event):
self.log_message(f"扫描完成: 发现 {len(disks)} 个磁盘, {len(partitions)}分区", "success")
self.rescan_button.config(state="normal")
def on_root_partition_selected(self, event=None):
"""根分区选择事件"""
selected_display = self.root_partition_combo.get()
self.selected_root_partition_info = self.all_partitions_data.get(selected_display)
@@ -158,152 +317,260 @@ class GrubRepairApp:
root_part_name = self.selected_root_partition_info['name']
self.log_message(f"已选择根分区: {root_part_name}", "info")
# 检是否是 LVM 逻辑卷
# 检是否为 btrfs
if self.selected_root_partition_info.get('fstype') == 'btrfs':
self.log_message("检测到 Btrfs 文件系统", "info")
self.btrfs_frame.grid() # 显示子卷选择
self._refresh_btrfs_subvolumes()
else:
self.btrfs_frame.grid_remove() # 隐藏子卷选择
self.btrfs_subvolume = None
# 自动选择目标磁盘
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")
self.log_message("检测到 LVM 逻辑卷,请手动选择目标磁盘", "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")
# 根据分区路径推断磁盘
disk_name_from_root = self._extract_disk_from_partition(root_part_name)
# 在磁盘列表中查找匹配项
for disk_display in self.target_disk_combo['values']:
if disk_name_from_root in disk_display:
self.target_disk_combo.set(disk_display)
self.selected_target_disk_info = self.all_disks_data.get(disk_display)
self.log_message(f"自动选择目标磁盘: {self.selected_target_disk_info['name']}", "info")
break
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
self.btrfs_frame.grid_remove()
def on_uefi_check_changed(self, *args):
def _extract_disk_from_partition(self, partition: str) -> str:
"""从分区路径提取磁盘路径"""
# /dev/sda1 -> /dev/sda
# /dev/nvme0n1p1 -> /dev/nvme0n1
# /dev/mmcblk0p1 -> /dev/mmcblk0
# /dev/mapper/vg-lv -> 保持原样LVM
if partition.startswith("/dev/mapper/"):
return partition # LVM 无法自动推断
# 移除分区号
import re
patterns = [
r'^(/dev/nvme\d+n\d+)p\d+$', # NVMe
r'^(/dev/mmcblk\d+)p\d+$', # eMMC
r'^(/dev/[a-zA-Z]+)\d+$', # SATA/SCSI
]
for pattern in patterns:
match = re.match(pattern, partition)
if match:
return match.group(1)
return partition
def _refresh_btrfs_subvolumes(self):
"""刷新 btrfs 子卷列表"""
if not self.selected_root_partition_info:
return
partition = self.selected_root_partition_info['name']
subvolumes = backend.get_btrfs_root_subvolume(partition)
# 尝试获取所有子卷
try:
import subprocess
result = subprocess.run(
["sudo", "btrfs", "subvolume", "list", partition],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
all_subvols = ["(默认 - 不指定)"]
for line in result.stdout.strip().split('\n'):
match = re.search(r'path\s+(\S+)', line)
if match:
all_subvols.append(match.group(1))
self.btrfs_subvolume_combo['values'] = all_subvols
if len(all_subvols) > 1:
self.btrfs_subvolume_combo.set(all_subvols[0])
except Exception as e:
self.log_message(f"获取 btrfs 子卷失败: {e}", "warning")
def on_uefi_mode_changed(self):
"""UEFI 模式切换事件"""
self.is_uefi_mode = self.uefi_mode_var.get()
self.log_message(f"UEFI模式选择: {'启用' if self.is_uefi_mode else '禁用'}", "info")
self.log_message(f"UEFI 模式: {'启用' if self.is_uefi_mode else '禁用'}", "info")
# 如果启用 UEFI建议必须选择 EFI 分区
if self.is_uefi_mode:
self.log_message("提示: UEFI 模式建议指定 EFI 系统分区", "info")
def start_repair_thread(self):
# 禁用按钮防止重复点击
"""启动修复线程"""
# 获取用户选择
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()) or None
self.selected_efi_partition_info = self.all_efi_partitions_data.get(self.efi_partition_combo.get()) or None
self.selected_target_disk_info = self.all_disks_data.get(self.target_disk_combo.get())
self.is_uefi_mode = self.uefi_mode_var.get()
# 获取 btrfs 子卷
btrfs_subvol = self.btrfs_subvolume_combo.get() if hasattr(self, 'btrfs_subvolume_combo') else ""
if btrfs_subvol and btrfs_subvol != "(默认 - 不指定)":
self.btrfs_subvolume = btrfs_subvol
else:
self.btrfs_subvolume = None
# 验证输入
if not self.selected_root_partition_info:
messagebox.showerror("错误", "请选择目标系统的根分区!")
return
if not self.selected_target_disk_info and not self.is_uefi_mode:
messagebox.showerror("错误", "BIOS 模式下必须选择 GRUB 安装的目标磁盘!")
return
# UEFI 模式下检查 EFI 分区
if self.is_uefi_mode and not self.selected_efi_partition_info:
response = messagebox.askyesno(
"警告",
"您选择了 UEFI 模式,但未选择 EFI 系统分区 (ESP)。\n"
"这可能导致修复失败。是否继续?"
)
if not response:
return
# 禁用按钮
self.start_button.config(state="disabled")
self.rescan_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
self.log_message("=" * 60, "step")
self.log_message("GRUB 引导修复过程开始", "step")
self.log_message("=" * 60, "step")
# 启动新线程进行修复避免UI卡死
repair_thread = threading.Thread(target=self._run_repair_process)
# 启动修复线程
repair_thread = threading.Thread(target=self._run_repair_process, daemon=True)
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")
target_disk_path = self.selected_target_disk_info['name'] if self.selected_target_disk_info else None
self.log_message(f"根分区: {root_part_path}", "info")
self.log_message(f"/boot 分区: {boot_part_path or ''}", "info")
self.log_message(f"EFI 分区: {efi_part_path or ''}", "info")
self.log_message(f"目标磁盘: {target_disk_path or 'N/A (UEFI only)'}", "info")
self.log_message(f"UEFI 模式: {'' if self.is_uefi_mode else ''}", "info")
self.log_message(f"Btrfs 子卷: {self.btrfs_subvolume or ''}", "info")
# 1. 挂载目标系统
self.log_message("正在挂载目标系统分区...", "info")
self.log_message("正在挂载目标系统分区...", "step")
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分区
efi_part_path if self.is_uefi_mode else None,
self.btrfs_subvolume
)
if not mount_ok:
self.log_message(f"挂载失败,修复中止: {mount_err}", "error")
self.master.after(0, lambda: messagebox.showerror("修复失败", f"无法挂载目标系统分区。请检查分区选择和权限。\n错误: {mount_err}"))
self.log_message(f"挂载失败: {mount_err}", "error")
self.master.after(0, lambda: messagebox.showerror("修复失败", f"无法挂载目标系统:\n{mount_err}"))
return
# 2. 尝试识别发行版类型
self.log_message("正在检测目标系统发行版...", "info")
# 2. 检测发行版
self.log_message("正在检测目标系统发行版...", "step")
distro_type = backend.detect_distro_type(self.mount_point)
self.log_message(f"检测到目标系统发行版: {distro_type}", "info")
self.log_message(f"检测到发行版: {distro_type}", "info")
# 3. Chroot并修复GRUB
self.log_message("正在进入Chroot环境并修复GRUB...", "info")
# 3. Chroot 并修复 GRUB
self.log_message("正在进入 Chroot 环境并修复 GRUB...", "step")
install_hybrid = self.hybrid_mode_var.get()
use_fallback = self.fallback_var.get()
bootloader_id = self.bootloader_id_var.get() or "GRUB"
self.log_message(f"混合模式: {'' if install_hybrid else ''}", "info")
self.log_message(f"EFI Fallback: {'' if use_fallback else ''}", "info")
self.log_message(f"启动项名称: {bootloader_id}", "info")
repair_ok, repair_err = backend.chroot_and_repair_grub(
self.mount_point,
target_disk_path,
target_disk_path or "", # UEFI 模式下可能为空
self.is_uefi_mode,
distro_type
distro_type,
install_hybrid=install_hybrid,
use_fallback=use_fallback,
efi_bootloader_id=bootloader_id
)
if not repair_ok:
self.log_message(f"GRUB修复失败: {repair_err}", "error")
self.master.after(0, lambda: messagebox.showerror("修复失败", f"GRUB修复过程中发生错误。\n错误: {repair_err}"))
# 即使失败,也要尝试卸载
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")
self.log_message("GRUB 修复命令执行成功!", "success")
# 4. 卸载分区
self.log_message("正在卸载目标系统分区...", "info")
self.log_message("正在卸载目标系统分区...", "step")
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}"))
self.log_message(f"卸载分区失败: {unmount_err}", "warning")
else:
self.log_message("所有分区已成功卸载", "success")
self.log_message("所有分区已成功卸载", "success")
self.log_message("--- GRUB 修复过程结束 ---", "info")
self.log_message("=" * 60, "step")
self.log_message("GRUB 修复过程结束", "step")
self.log_message("=" * 60, "step")
# 最终结果提示
# 最终结果
if repair_ok and unmount_ok:
self.master.after(0, lambda: messagebox.showinfo("修复成功", "GRUB引导修复已完成请重启系统以验证。"))
self.master.after(0, lambda: messagebox.showinfo(
"修复成功",
"GRUB 引导修复已完成!\n\n"
"请重启系统以验证修复结果。\n"
"如果仍无法启动请检查BIOS设置中的启动顺序。"
))
elif repair_ok and not unmount_ok:
self.master.after(0, lambda: messagebox.showwarning("修复完成,但有警告", "GRUB引导修复已完成但部分分区卸载失败。请重启系统以验证。"))
self.master.after(0, lambda: messagebox.showwarning(
"修复完成,但有警告",
"GRUB 引导修复已完成,但部分分区卸载失败。\n"
"建议重启系统,这会自动清理挂载点。"
))
else:
self.master.after(0, lambda: messagebox.showerror("修复失败", "GRUB引导修复过程中发生错误。请检查日志并尝试手动修复。"))
self.master.after(0, lambda: messagebox.showerror(
"修复失败",
"GRUB 引导修复过程中发生错误。\n"
"请检查日志并尝试手动修复。"
))
except Exception as e:
self.log_message(f"发生未预期的错误: {str(e)}", "error")
import traceback
self.log_message(traceback.format_exc(), "debug")
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"))
self.master.after(0, lambda: self.rescan_button.config(state="normal"))
if __name__ == "__main__":
root = tk.Tk()
app = GrubRepairApp(root)
root.mainloop()