616 lines
26 KiB
Python
616 lines
26 KiB
Python
# frontend.py
|
||
# 参考 Calamares 引导安装模块优化
|
||
|
||
import tkinter as tk
|
||
from tkinter import ttk, messagebox, scrolledtext
|
||
import threading
|
||
import os
|
||
import re
|
||
import backend
|
||
|
||
# ==============================================================================
|
||
# X11 字体渲染问题修复
|
||
# 设置 tkinter 使用安全字体,避免 "BadLength" 错误
|
||
# ==============================================================================
|
||
def setup_safe_fonts():
|
||
"""配置安全的字体设置,解决 Arch Linux 等系统上的 X11 渲染问题"""
|
||
try:
|
||
# 设置 tkinter 使用安全字体
|
||
# 避免使用可能导致 X11 RENDER 扩展问题的复杂字体
|
||
default_fonts = {
|
||
'TkDefaultFont': ('DejaVu Sans', 10),
|
||
'TkTextFont': ('DejaVu Sans', 10),
|
||
'TkFixedFont': ('DejaVu Sans Mono', 10),
|
||
'TkMenuFont': ('DejaVu Sans', 10),
|
||
'TkHeadingFont': ('DejaVu Sans', 12, 'bold'),
|
||
}
|
||
|
||
for font_name, font_config in default_fonts.items():
|
||
try:
|
||
tk.font.nametofont(font_name).configure(family=font_config[0], size=font_config[1])
|
||
except:
|
||
pass
|
||
except:
|
||
pass
|
||
|
||
# 尝试导入字体模块
|
||
try:
|
||
import tkinter.font as tkfont
|
||
except ImportError:
|
||
import tkFont as tkfont
|
||
|
||
|
||
class GrubRepairApp:
|
||
def __init__(self, master):
|
||
self.master = master
|
||
|
||
# 设置安全字体
|
||
setup_safe_fonts()
|
||
|
||
master.title("Linux GRUB 引导修复工具 v2.0")
|
||
master.geometry("643x750")
|
||
master.resizable(True, True)
|
||
|
||
# 设置窗口最小大小
|
||
master.minsize(643, 600)
|
||
|
||
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.btrfs_subvolume = None
|
||
|
||
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):
|
||
"""创建 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)
|
||
|
||
# 根分区
|
||
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=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))
|
||
|
||
# /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=2, column=1, padx=5, pady=5, sticky="ew")
|
||
|
||
# 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=3, column=1, padx=5, pady=5, sticky="ew")
|
||
|
||
# ===== 目标磁盘选择 =====
|
||
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="目标磁盘 *:").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=5, sticky="ew")
|
||
|
||
# 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()
|
||
|
||
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")
|
||
|
||
# 如果 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")
|
||
|
||
# ===== 操作按钮 =====
|
||
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)
|
||
|
||
# ===== 日志区域 =====
|
||
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 = 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_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 _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: str, level: str = "info"):
|
||
"""添加日志到文本框"""
|
||
self.log_text.config(state="normal")
|
||
|
||
# 添加时间戳
|
||
import datetime
|
||
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
||
log_line = f"[{timestamp}] {message}\n"
|
||
|
||
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
|
||
|
||
# 清空旧数据
|
||
self.all_disks_data = {}
|
||
self.all_partitions_data = {}
|
||
self.all_efi_partitions_data = {}
|
||
|
||
# 更新磁盘列表
|
||
disk_display_names = []
|
||
for d in disks:
|
||
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:
|
||
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
|
||
|
||
# 更新 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
|
||
|
||
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)
|
||
|
||
if self.selected_root_partition_info:
|
||
root_part_name = self.selected_root_partition_info['name']
|
||
self.log_message(f"已选择根分区: {root_part_name}", "info")
|
||
|
||
# 检测是否为 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:
|
||
self.target_disk_combo.set("")
|
||
self.selected_target_disk_info = None
|
||
self.log_message("检测到 LVM 逻辑卷,请手动选择目标磁盘", "warning")
|
||
else:
|
||
# 根据分区路径推断磁盘
|
||
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
|
||
else:
|
||
self.selected_root_partition_info = None
|
||
self.target_disk_combo.set("")
|
||
self.selected_target_disk_info = None
|
||
self.btrfs_frame.grid_remove()
|
||
|
||
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")
|
||
|
||
# 如果启用 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("=" * 60, "step")
|
||
self.log_message("GRUB 引导修复过程开始", "step")
|
||
self.log_message("=" * 60, "step")
|
||
|
||
# 启动修复线程
|
||
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'] 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("正在挂载目标系统分区...", "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,
|
||
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}"))
|
||
return
|
||
|
||
# 2. 检测发行版
|
||
self.log_message("正在检测目标系统发行版...", "step")
|
||
distro_type = backend.detect_distro_type(self.mount_point)
|
||
self.log_message(f"检测到发行版: {distro_type}", "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")
|
||
|
||
# 检查是否有独立的 /boot 分区
|
||
has_separate_boot = self.selected_boot_partition_info is not None
|
||
|
||
repair_ok, repair_err = backend.chroot_and_repair_grub(
|
||
self.mount_point,
|
||
target_disk_path or "", # UEFI 模式下可能为空
|
||
self.is_uefi_mode,
|
||
distro_type,
|
||
install_hybrid=install_hybrid,
|
||
use_fallback=use_fallback,
|
||
efi_bootloader_id=bootloader_id,
|
||
has_separate_boot=has_separate_boot
|
||
)
|
||
|
||
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("正在卸载目标系统分区...", "step")
|
||
unmount_ok, unmount_err = backend.unmount_target_system(self.mount_point)
|
||
if not unmount_ok:
|
||
self.log_message(f"卸载分区失败: {unmount_err}", "warning")
|
||
else:
|
||
self.log_message("所有分区已成功卸载", "success")
|
||
|
||
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 引导修复已完成!\n\n"
|
||
"请重启系统以验证修复结果。\n"
|
||
"如果仍无法启动,请检查BIOS设置中的启动顺序。"
|
||
))
|
||
elif repair_ok and not unmount_ok:
|
||
self.master.after(0, lambda: messagebox.showwarning(
|
||
"修复完成,但有警告",
|
||
"GRUB 引导修复已完成,但部分分区卸载失败。\n"
|
||
"建议重启系统,这会自动清理挂载点。"
|
||
))
|
||
else:
|
||
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()
|