two
This commit is contained in:
322
backend.py
Normal file
322
backend.py
Normal file
@@ -0,0 +1,322 @@
|
||||
# backend.py
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import json
|
||||
import time # 用于模拟耗时操作,实际可移除
|
||||
|
||||
def run_command(command, description="执行命令", cwd=None, shell=False):
|
||||
"""
|
||||
运行一个系统命令,并捕获其输出和错误。
|
||||
:param command: 要执行的命令列表 (例如 ["sudo", "lsblk"])
|
||||
:param description: 命令的描述,用于日志
|
||||
:param cwd: 更改工作目录
|
||||
:param shell: 是否使用shell执行命令 (通常不推荐,除非命令需要shell特性)
|
||||
:return: (True/False, stdout, stderr) 表示成功、标准输出、标准错误
|
||||
"""
|
||||
try:
|
||||
print(f"[Backend] {description}: {' '.join(command)}")
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True, # 如果命令返回非零退出码,将抛出CalledProcessError
|
||||
cwd=cwd,
|
||||
shell=shell
|
||||
)
|
||||
print(f"[Backend] 命令成功: {description}")
|
||||
return True, result.stdout, result.stderr
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"[Backend] 命令失败: {description}")
|
||||
print(f"[Backend] 错误输出: {e.stderr}")
|
||||
return False, e.stdout, e.stderr
|
||||
except FileNotFoundError:
|
||||
print(f"[Backend] 命令未找到: {' '.join(command)}")
|
||||
return False, "", f"命令未找到: {command[0]}"
|
||||
except Exception as e:
|
||||
print(f"[Backend] 发生未知错误: {e}")
|
||||
return False, "", str(e)
|
||||
|
||||
def scan_partitions():
|
||||
"""
|
||||
扫描系统中的所有磁盘和分区,并返回结构化的信息。
|
||||
:return: (success, disks, partitions, efi_partitions, error_message)
|
||||
disks: list of {"name": "/dev/sda"}
|
||||
partitions: list of {"name": "/dev/sda1", "fstype": "ext4", "size": "100G", "mountpoint": "/"}
|
||||
efi_partitions: list of {"name": "/dev/sda1", "fstype": "vfat", "size": "512M"}
|
||||
"""
|
||||
success, stdout, stderr = run_command(
|
||||
["sudo", "lsblk", "-J", "-o", "NAME,FSTYPE,SIZE,MOUNTPOINT,UUID,PARTLABEL,LABEL,TYPE"],
|
||||
"扫描分区"
|
||||
)
|
||||
|
||||
if not success:
|
||||
return False, [], [], [], stderr
|
||||
|
||||
try:
|
||||
data = json.loads(stdout)
|
||||
all_disks = []
|
||||
all_partitions = []
|
||||
all_efi_partitions = []
|
||||
|
||||
for block_device in data.get("blockdevices", []):
|
||||
dev_name = f"/dev/{block_device['name']}"
|
||||
dev_type = block_device.get("type")
|
||||
|
||||
if dev_type == "disk":
|
||||
all_disks.append({"name": dev_name})
|
||||
elif dev_type == "part":
|
||||
# 过滤掉Live系统自身的分区(基于常见的Live环境挂载点)
|
||||
mountpoint = block_device.get("mountpoint")
|
||||
if mountpoint and (mountpoint == "/" or mountpoint.startswith("/run/media") or mountpoint.startswith("/cdrom")):
|
||||
continue
|
||||
|
||||
part_info = {
|
||||
"name": dev_name,
|
||||
"fstype": block_device.get("fstype", "unknown"),
|
||||
"size": block_device.get("size", "unknown"),
|
||||
"mountpoint": mountpoint,
|
||||
"uuid": block_device.get("uuid"),
|
||||
"partlabel": block_device.get("partlabel"),
|
||||
"label": block_device.get("label")
|
||||
}
|
||||
all_partitions.append(part_info)
|
||||
|
||||
# 识别EFI系统分区 (FAT32文件系统, 且通常有特定标签或名称)
|
||||
if part_info["fstype"] == "vfat" and (part_info["partlabel"] == "EFI System Partition" or part_info["label"] == "EFI"):
|
||||
all_efi_partitions.append(part_info)
|
||||
|
||||
return True, all_disks, all_partitions, all_efi_partitions, ""
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
return False, [], [], [], f"解析lsblk输出失败: {e}"
|
||||
except Exception as e:
|
||||
return False, [], [], [], f"处理分区数据时发生未知错误: {e}"
|
||||
|
||||
def mount_target_system(root_partition, boot_partition=None, efi_partition=None):
|
||||
"""
|
||||
挂载目标系统的根分区、/boot分区和EFI分区到临时目录。
|
||||
:param root_partition: 目标系统的根分区设备路径 (例如 /dev/sda1)
|
||||
:param boot_partition: 目标系统的独立 /boot 分区设备路径 (可选)
|
||||
:param efi_partition: 目标系统的EFI系统分区设备路径 (可选,仅UEFI)
|
||||
:return: (True/False, mount_point, error_message)
|
||||
"""
|
||||
mount_point = "/mnt_grub_repair_" + str(int(time.time())) # 确保唯一性
|
||||
if not os.path.exists(mount_point):
|
||||
os.makedirs(mount_point)
|
||||
|
||||
# 1. 挂载根分区
|
||||
success, _, stderr = run_command(["sudo", "mount", root_partition, mount_point], f"挂载根分区 {root_partition}")
|
||||
if not success:
|
||||
return False, "", f"挂载根分区失败: {stderr}"
|
||||
|
||||
# 2. 如果有独立 /boot 分区
|
||||
if boot_partition:
|
||||
boot_mount_point = os.path.join(mount_point, "boot")
|
||||
if not os.path.exists(boot_mount_point):
|
||||
os.makedirs(boot_mount_point)
|
||||
success, _, stderr = run_command(["sudo", "mount", boot_partition, boot_mount_point], f"挂载 /boot 分区 {boot_partition}")
|
||||
if not success:
|
||||
unmount_target_system(mount_point) # 挂载失败,尝试清理
|
||||
return False, "", f"挂载 /boot 分区失败: {stderr}"
|
||||
|
||||
# 3. 如果有EFI分区 (UEFI系统)
|
||||
if efi_partition:
|
||||
efi_mount_point = os.path.join(mount_point, "boot/efi")
|
||||
if not os.path.exists(efi_mount_point):
|
||||
os.makedirs(efi_mount_point)
|
||||
success, _, stderr = run_command(["sudo", "mount", efi_partition, efi_mount_point], f"挂载 EFI 分区 {efi_partition}")
|
||||
if not success:
|
||||
unmount_target_system(mount_point) # 挂载失败,尝试清理
|
||||
return False, "", f"挂载 EFI 分区失败: {stderr}"
|
||||
|
||||
# 4. 绑定必要的伪文件系统
|
||||
bind_paths = ["/dev", "/dev/pts", "/proc", "/sys", "/run"]
|
||||
for path in bind_paths:
|
||||
target_path = os.path.join(mount_point, path.lstrip('/'))
|
||||
if not os.path.exists(target_path):
|
||||
os.makedirs(target_path)
|
||||
success, _, stderr = run_command(["sudo", "mount", "--bind", path, target_path], f"绑定 {path} 到 {target_path}")
|
||||
if not success:
|
||||
unmount_target_system(mount_point) # 绑定失败,尝试清理
|
||||
return False, "", f"绑定 {path} 失败: {stderr}"
|
||||
|
||||
return True, mount_point, ""
|
||||
|
||||
def detect_distro_type(mount_point):
|
||||
"""
|
||||
尝试检测目标系统发行版类型。
|
||||
:param mount_point: 目标系统根分区的挂载点
|
||||
:return: "arch", "centos", "debian", "ubuntu", "unknown"
|
||||
"""
|
||||
os_release_path = os.path.join(mount_point, "etc/os-release")
|
||||
if not os.path.exists(os_release_path):
|
||||
return "unknown"
|
||||
|
||||
try:
|
||||
with open(os_release_path, "r") as f:
|
||||
content = f.read()
|
||||
if "ID=arch" in content:
|
||||
return "arch"
|
||||
elif "ID=centos" in content or "ID=rhel" in content:
|
||||
return "centos"
|
||||
elif "ID=debian" in content:
|
||||
return "debian"
|
||||
elif "ID=ubuntu" in content:
|
||||
return "ubuntu"
|
||||
else:
|
||||
return "unknown"
|
||||
except Exception as e:
|
||||
print(f"[Backend] 无法读取os-release文件: {e}")
|
||||
return "unknown"
|
||||
|
||||
def chroot_and_repair_grub(mount_point, target_disk, is_uefi=False, distro_type="unknown"):
|
||||
"""
|
||||
Chroot到目标系统并执行GRUB修复命令。
|
||||
:param mount_point: 目标系统根分区的挂载点
|
||||
:param target_disk: GRUB要安装到的物理磁盘 (例如 /dev/sda)
|
||||
:param is_uefi: 目标系统是否使用UEFI启动
|
||||
:param distro_type: 目标系统发行版类型 (用于选择正确的GRUB命令)
|
||||
:return: (True/False, error_message)
|
||||
"""
|
||||
chroot_cmd_prefix = ["sudo", "chroot", mount_point]
|
||||
|
||||
# 1. 安装GRUB到目标磁盘
|
||||
if is_uefi:
|
||||
# UEFI系统,需要确保EFI分区已挂载到 /boot/efi
|
||||
# grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=GRUB
|
||||
# 注意:某些系统可能需要指定 --removable 选项,以便在某些固件上更容易启动
|
||||
# 统一使用 --bootloader-id=GRUB,如果已存在会更新
|
||||
success, _, stderr = run_command(
|
||||
chroot_cmd_prefix + ["grub-install", "--target=x86_64-efi", "--efi-directory=/boot/efi", "--bootloader-id=GRUB"],
|
||||
"安装UEFI GRUB"
|
||||
)
|
||||
else:
|
||||
# BIOS系统,安装到MBR
|
||||
success, _, stderr = run_command(
|
||||
chroot_cmd_prefix + ["grub-install", target_disk],
|
||||
"安装BIOS GRUB"
|
||||
)
|
||||
if not success:
|
||||
return False, f"GRUB安装失败: {stderr}"
|
||||
|
||||
# 2. 更新GRUB配置文件
|
||||
grub_update_cmd = []
|
||||
if distro_type in ["debian", "ubuntu"]:
|
||||
grub_update_cmd = ["update-grub"] # 这是一个脚本,实际是调用grub-mkconfig
|
||||
elif distro_type == "arch":
|
||||
grub_update_cmd = ["grub-mkconfig", "-o", "/boot/grub/grub.cfg"]
|
||||
elif distro_type == "centos":
|
||||
grub_update_cmd = ["grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"] # CentOS 8/9
|
||||
else:
|
||||
# 尝试通用命令
|
||||
grub_update_cmd = ["grub-mkconfig", "-o", "/boot/grub/grub.cfg"]
|
||||
|
||||
success, _, stderr = run_command(
|
||||
chroot_cmd_prefix + grub_update_cmd,
|
||||
"更新GRUB配置文件"
|
||||
)
|
||||
if not success:
|
||||
return False, f"GRUB配置文件更新失败: {stderr}"
|
||||
|
||||
return True, ""
|
||||
|
||||
def unmount_target_system(mount_point):
|
||||
"""
|
||||
卸载所有挂载的分区。
|
||||
:param mount_point: 目标系统根分区的挂载点
|
||||
:return: (True/False, error_message)
|
||||
"""
|
||||
print(f"[Backend] 正在卸载分区 from {mount_point}...")
|
||||
success = True
|
||||
error_msg = ""
|
||||
|
||||
# 逆序卸载绑定
|
||||
bind_paths = ["/run", "/sys", "/proc", "/dev/pts", "/dev"]
|
||||
for path in reversed(bind_paths): # 逆序
|
||||
target_path = os.path.join(mount_point, path.lstrip('/'))
|
||||
if os.path.ismount(target_path): # 检查是否真的挂载了
|
||||
s, _, stderr = run_command(["sudo", "umount", target_path], f"卸载绑定 {target_path}")
|
||||
if not s:
|
||||
success = False
|
||||
error_msg += f"卸载绑定 {target_path} 失败: {stderr}\n"
|
||||
|
||||
# 卸载 /boot/efi (如果存在)
|
||||
efi_mount_point = os.path.join(mount_point, "boot/efi")
|
||||
if os.path.ismount(efi_mount_point):
|
||||
s, _, stderr = run_command(["sudo", "umount", efi_mount_point], f"卸载 {efi_mount_point}")
|
||||
if not s:
|
||||
success = False
|
||||
error_msg += f"卸载 {efi_mount_point} 失败: {stderr}\n"
|
||||
|
||||
# 卸载 /boot (如果存在)
|
||||
boot_mount_point = os.path.join(mount_point, "boot")
|
||||
if os.path.ismount(boot_mount_point):
|
||||
s, _, stderr = run_command(["sudo", "umount", boot_mount_point], f"卸载 {boot_mount_point}")
|
||||
if not s:
|
||||
success = False
|
||||
error_msg += f"卸载 {boot_mount_point} 失败: {stderr}\n"
|
||||
|
||||
# 卸载根分区
|
||||
if os.path.ismount(mount_point):
|
||||
s, _, stderr = run_command(["sudo", "umount", mount_point], f"卸载根分区 {mount_point}")
|
||||
if not s:
|
||||
success = False
|
||||
error_msg += f"卸载根分区 {mount_point} 失败: {stderr}\n"
|
||||
|
||||
# 清理临时挂载点目录
|
||||
if os.path.exists(mount_point) and not os.path.ismount(mount_point):
|
||||
try:
|
||||
os.rmdir(mount_point)
|
||||
print(f"[Backend] 清理临时目录 {mount_point}")
|
||||
except OSError as e:
|
||||
print(f"[Backend] 无法删除临时目录 {mount_point}: {e}")
|
||||
error_msg += f"无法删除临时目录 {mount_point}: {e}\n"
|
||||
|
||||
|
||||
return success, error_msg
|
||||
|
||||
# 示例:如果直接运行backend.py,可以进行一些测试
|
||||
if __name__ == "__main__":
|
||||
print("--- 运行后端测试 ---")
|
||||
|
||||
print("\n--- 扫描分区测试 ---")
|
||||
success, disks, partitions, efi_partitions, err = scan_partitions()
|
||||
if success:
|
||||
print("磁盘:", [d['name'] for d in disks])
|
||||
print("分区:", [p['name'] for p in partitions])
|
||||
print("EFI分区:", [p['name'] for p in efi_partitions])
|
||||
else:
|
||||
print("扫描分区失败:", err)
|
||||
|
||||
# 假设有一些分区可供测试,实际运行时需要用户输入
|
||||
# 例如:
|
||||
# test_root_partition = "/dev/sdaX"
|
||||
# test_target_disk = "/dev/sda"
|
||||
# test_efi_partition = "/dev/sdY"
|
||||
#
|
||||
# if test_root_partition and test_target_disk:
|
||||
# print(f"\n--- 挂载系统测试 (模拟 {test_root_partition}) ---")
|
||||
# mount_ok, mnt_pt, mount_err = mount_target_system(test_root_partition, efi_partition=test_efi_partition)
|
||||
# if mount_ok:
|
||||
# print(f"系统挂载成功到: {mnt_pt}")
|
||||
# distro = detect_distro_type(mnt_pt)
|
||||
# print(f"检测到发行版: {distro}")
|
||||
#
|
||||
# print("\n--- 修复GRUB测试 ---")
|
||||
# repair_ok, repair_err = chroot_and_repair_grub(mnt_pt, test_target_disk, is_uefi=True, distro_type=distro)
|
||||
# if repair_ok:
|
||||
# print("GRUB修复成功!")
|
||||
# else:
|
||||
# print("GRUB修复失败:", repair_err)
|
||||
#
|
||||
# print("\n--- 卸载系统测试 ---")
|
||||
# unmount_ok, unmount_err = unmount_target_system(mnt_pt)
|
||||
# if unmount_ok:
|
||||
# print("系统卸载成功!")
|
||||
# else:
|
||||
# print("系统卸载失败:", unmount_err)
|
||||
# else:
|
||||
# print("系统挂载失败:", mount_err)
|
||||
# else:
|
||||
# print("\n请在代码中设置 test_root_partition 和 test_target_disk 进行完整测试。")
|
||||
289
frontend.py
Normal file
289
frontend.py
Normal 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()
|
||||
|
||||
Reference in New Issue
Block a user