This commit is contained in:
zj
2026-02-11 02:38:52 +08:00
parent 70d8ac41b4
commit cdb32839d5
6 changed files with 525 additions and 151 deletions

126
AGENTS.md Normal file
View File

@@ -0,0 +1,126 @@
# BootRepairTool - 项目说明
## 项目概述
**BootRepairTool** 是一个用于修复 Linux 系统 GRUB 引导的图形化工具。适用于 Live USB/CD 环境,帮助用户修复损坏的 GRUB 引导加载器。
## 项目结构
```
BootRepairTool/
├── backend.py # 后端逻辑 - 分区扫描、挂载、GRUB修复
├── frontend.py # 前端界面 - tkinter GUI
├── README.md # 项目简介
└── AGENTS.md # 本文件
```
## 技术栈
- **语言**: Python 3
- **GUI 框架**: tkinter
- **运行环境**: Linux Live USB/CD (需要 root/sudo 权限)
## 核心功能
### 1. 分区扫描 (`backend.py:scan_partitions`)
- 使用 `lsblk -J` 获取磁盘和分区信息
- 识别 EFI 系统分区 (ESP)
- 过滤 Live 系统自身的分区
### 2. 系统挂载 (`backend.py:mount_target_system`)
- 挂载根分区 (/)
- 挂载独立 /boot 分区(可选)
- 挂载 EFI 分区UEFI 模式)
- 绑定伪文件系统 (/dev, /proc, /sys, /run)
### 3. 发行版检测 (`backend.py:detect_distro_type`)
- 支持: Arch, CentOS/RHEL, Debian, Ubuntu
- 通过读取 `/etc/os-release` 识别
### 4. GRUB 修复 (`backend.py:chroot_and_repair_grub`)
- **BIOS 模式**: `grub-install /dev/sdX`
- **UEFI 模式**: `grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=GRUB`
- 更新 GRUB 配置:
- Debian/Ubuntu: `update-grub`
- Arch: `grub-mkconfig -o /boot/grub/grub.cfg`
- CentOS: `grub2-mkconfig -o /boot/grub2/grub.cfg`
### 5. 卸载清理 (`backend.py:unmount_target_system`)
- 逆序卸载绑定的文件系统
- 卸载 EFI/boot/根分区
- 清理临时挂载点
## 前端界面 (`frontend.py`)
### 主要组件
1. **分区选择区域**
- 根分区 (/) - 必选
- 独立 /boot 分区 - 可选
- EFI 系统分区 (ESP) - UEFI 模式下推荐
2. **目标磁盘选择**
- 物理磁盘选择 (如 /dev/sda)
- UEFI 模式复选框(自动检测 Live 环境)
3. **日志窗口**
- 彩色日志输出(信息/警告/错误/成功)
- 自动滚动
### 工作流程
```
扫描分区 → 用户选择分区 → 点击修复 → 挂载系统 →
检测发行版 → chroot修复GRUB → 卸载分区 → 完成
```
### 线程安全
修复过程在独立线程中运行,避免 UI 卡死。使用 `master.after()` 进行线程安全的日志更新。
## 代码规范
### 后端函数返回值
所有后端函数统一返回 `(success: bool, ..., error_message: str)` 格式:
```python
# 示例
success, stdout, stderr = run_command([...])
success, disks, partitions, efi_parts, err = scan_partitions()
```
### 错误处理
- 命令执行失败时自动清理已挂载的分区
- 日志记录使用 `[Backend]` 前缀
- GUI 使用弹窗显示关键错误
## 使用限制
1. **需要 root 权限**: 所有磁盘操作需要 `sudo`
2. **Live 环境**: 设计为在 Live USB/CD 中运行
3. **单线程修复**: 同时只能进行一个修复任务
4. **UEFI 自动检测**: 根据 Live 环境自动设置 UEFI 模式
## 测试
直接运行 `backend.py` 可进行基础测试(仅扫描分区):
```bash
sudo python backend.py
```
完整修复测试需要取消注释 `__main__` 块中的测试代码并设置实际分区。
## 潜在改进点
1. **添加 requirements.txt**: 当前无第三方依赖
2. **国际化**: 当前仅支持中文界面
3. **配置文件**: 支持保存/加载常用配置
4. **日志持久化**: 将日志保存到文件
5. **多语言发行版支持**: 扩展 detect_distro_type
6. **Btrfs/LVM 支持**: 当前仅支持标准分区
## 注意事项
- **数据风险**: 操作磁盘有数据丢失风险,建议备份
- **EFI 分区警告**: UEFI 模式下未选择 ESP 会弹出警告
- **临时挂载点**: 使用 `/mnt_grub_repair_<timestamp>` 格式
- **依赖命令**: 需要系统中存在 `lsblk`, `mount`, `umount`, `grub-install`, `grub-mkconfig`/`update-grub`

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,16 +3,51 @@
import subprocess
import os
import json
import time # 用于模拟耗时操作,实际可移除
import time
import re
import shutil
from typing import Tuple, List, Dict, Optional
def run_command(command, description="执行命令", cwd=None, shell=False):
# 常量配置
COMMAND_TIMEOUT = 300 # 命令执行超时时间(秒)
DEFAULT_GRUB_CONFIG_PATHS = [
"/boot/grub/grub.cfg",
"/boot/grub2/grub.cfg",
]
def validate_device_path(path: str) -> bool:
"""
验证设备路径格式是否合法(防止命令注入)。
:param path: 设备路径(如 /dev/sda1, /dev/mapper/cl-root
:return: 是否合法
"""
if not path:
return False
# 允许的格式:
# - /dev/sda, /dev/sda1
# - /dev/nvme0n1, /dev/nvme0n1p1
# - /dev/mmcblk0, /dev/mmcblk0p1
# - /dev/mapper/xxx (LVM逻辑卷)
# - /dev/dm-0 等
patterns = [
r'^/dev/[a-zA-Z0-9_-]+$',
r'^/dev/mapper/[a-zA-Z0-9_-]+$',
]
return any(re.match(pattern, path) is not None for pattern in patterns)
def run_command(command: List[str], description: str = "执行命令",
cwd: Optional[str] = None, shell: bool = False,
timeout: int = COMMAND_TIMEOUT) -> Tuple[bool, str, str]:
"""
运行一个系统命令,并捕获其输出和错误。
:param command: 要执行的命令列表 (例如 ["sudo", "lsblk"])
:param description: 命令的描述,用于日志
:param cwd: 更改工作目录
:param shell: 是否使用shell执行命令 (通常不推荐除非命令需要shell特性)
:return: (True/False, stdout, stderr) 表示成功、标准输出、标准错误
:param shell: 是否使用shell执行命令
:param timeout: 命令超时时间(秒)
:return: (success, stdout, stderr) 表示成功、标准输出、标准错误
"""
try:
print(f"[Backend] {description}: {' '.join(command)}")
@@ -20,9 +55,10 @@ def run_command(command, description="执行命令", cwd=None, shell=False):
command,
capture_output=True,
text=True,
check=True, # 如果命令返回非零退出码将抛出CalledProcessError
check=True,
cwd=cwd,
shell=shell
shell=shell,
timeout=timeout
)
print(f"[Backend] 命令成功: {description}")
return True, result.stdout, result.stderr
@@ -30,6 +66,9 @@ def run_command(command, description="执行命令", cwd=None, shell=False):
print(f"[Backend] 命令失败: {description}")
print(f"[Backend] 错误输出: {e.stderr}")
return False, e.stdout, e.stderr
except subprocess.TimeoutExpired:
print(f"[Backend] 命令超时: {description}")
return False, "", f"命令执行超时(超过 {timeout} 秒)"
except FileNotFoundError:
print(f"[Backend] 命令未找到: {' '.join(command)}")
return False, "", f"命令未找到: {command[0]}"
@@ -37,16 +76,103 @@ def run_command(command, description="执行命令", cwd=None, shell=False):
print(f"[Backend] 发生未知错误: {e}")
return False, "", str(e)
def scan_partitions():
def _process_partition(block_device: Dict, all_disks: List, all_partitions: List,
all_efi_partitions: List) -> None:
"""
处理单个块设备递归处理子分区、LVM逻辑卷等。
"""
dev_name = f"/dev/{block_device.get('name', '')}"
dev_type = block_device.get("type")
if dev_type == "disk":
all_disks.append({"name": dev_name})
# 递归处理子分区
for child in block_device.get("children", []):
_process_partition(child, all_disks, all_partitions, all_efi_partitions)
elif dev_type == "part":
# 过滤掉Live系统自身的分区
mountpoint = block_device.get("mountpoint")
if mountpoint and (mountpoint == "/" or
mountpoint.startswith("/run/media") or
mountpoint.startswith("/cdrom") or
mountpoint.startswith("/live")):
# 继续处理子设备如LVM但自己不被标记为可用分区
for child in block_device.get("children", []):
_process_partition(child, all_disks, all_partitions, all_efi_partitions)
return
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"),
"parttype": (block_device.get("parttype") or "").upper() # EFI分区类型GUID
}
all_partitions.append(part_info)
# 识别EFI系统分区 (FAT32文件系统, 且满足以下条件之一)
is_vfat = part_info["fstype"] == "vfat"
has_efi_label = part_info["partlabel"] and "EFI" in part_info["partlabel"].upper()
has_efi_name = part_info["label"] and "EFI" in part_info["label"].upper()
is_efi_type = part_info["parttype"] == "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
if is_vfat and (has_efi_label or has_efi_name or is_efi_type):
all_efi_partitions.append(part_info)
# 递归处理子设备如LVM逻辑卷
for child in block_device.get("children", []):
_process_partition(child, all_disks, all_partitions, all_efi_partitions)
elif dev_type in ["lvm", "dm"]:
# 处理LVM逻辑卷和Device Mapper设备
mountpoint = block_device.get("mountpoint")
# 过滤Live系统自身的挂载点
if mountpoint and (mountpoint == "/" or
mountpoint.startswith("/run/media") or
mountpoint.startswith("/cdrom") or
mountpoint.startswith("/live")):
pass # 仍然添加到分区列表,但标记一下
# 尝试获取mapper路径如 /dev/mapper/cl-root
lv_name = block_device.get("name", "")
# 如果名称中包含'-'可能是mapper设备
if "-" in lv_name and not lv_name.startswith("dm-"):
mapper_path = f"/dev/mapper/{lv_name}"
else:
mapper_path = dev_name
part_info = {
"name": mapper_path,
"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"),
"parttype": (block_device.get("parttype") or "").upper(),
"is_lvm": True,
"dm_name": lv_name
}
all_partitions.append(part_info)
# 递归处理可能的子设备
for child in block_device.get("children", []):
_process_partition(child, all_disks, all_partitions, all_efi_partitions)
def scan_partitions() -> Tuple[bool, List, List, List, str]:
"""
扫描系统中的所有磁盘和分区,并返回结构化的信息。
: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"],
["sudo", "lsblk", "-J", "-o", "NAME,FSTYPE,SIZE,MOUNTPOINT,UUID,PARTLABEL,LABEL,TYPE,PARTTYPE"],
"扫描分区"
)
@@ -60,31 +186,7 @@ def scan_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)
_process_partition(block_device, all_disks, all_partitions, all_efi_partitions)
return True, all_disks, all_partitions, all_efi_partitions, ""
@@ -93,21 +195,77 @@ def scan_partitions():
except Exception as e:
return False, [], [], [], f"处理分区数据时发生未知错误: {e}"
def mount_target_system(root_partition, boot_partition=None, efi_partition=None):
def _cleanup_partial_mount(mount_point: str, mounted_boot: bool, mounted_efi: bool,
bind_paths_mounted: List[str]) -> None:
"""
清理部分挂载的资源(用于挂载失败时的回滚)。
"""
print(f"[Backend] 清理部分挂载资源: {mount_point}")
# 逆序卸载已绑定的路径
for target_path in reversed(bind_paths_mounted):
if os.path.ismount(target_path):
run_command(["sudo", "umount", target_path], f"清理卸载绑定 {target_path}")
# 卸载EFI分区
if mounted_efi:
efi_mount_point = os.path.join(mount_point, "boot/efi")
if os.path.ismount(efi_mount_point):
run_command(["sudo", "umount", efi_mount_point], "清理卸载EFI分区")
# 卸载/boot分区
if mounted_boot:
boot_mount_point = os.path.join(mount_point, "boot")
if os.path.ismount(boot_mount_point):
run_command(["sudo", "umount", boot_mount_point], "清理卸载/boot分区")
# 卸载根分区
if os.path.ismount(mount_point):
run_command(["sudo", "umount", mount_point], "清理卸载根分区")
# 删除临时目录
if os.path.exists(mount_point) and not os.path.ismount(mount_point):
try:
os.rmdir(mount_point)
except OSError:
pass
def mount_target_system(root_partition: str, boot_partition: Optional[str] = None,
efi_partition: Optional[str] = None) -> Tuple[bool, str, str]:
"""
挂载目标系统的根分区、/boot分区和EFI分区到临时目录。
:param root_partition: 目标系统的根分区设备路径 (例如 /dev/sda1)
:param root_partition: 目标系统的根分区设备路径
: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)
# 验证输入
if not validate_device_path(root_partition):
return False, "", f"无效的根分区路径: {root_partition}"
if boot_partition and not validate_device_path(boot_partition):
return False, "", f"无效的/boot分区路径: {boot_partition}"
if efi_partition and not validate_device_path(efi_partition):
return False, "", f"无效的EFI分区路径: {efi_partition}"
# 创建临时挂载点
mount_point = "/mnt_grub_repair_" + str(int(time.time()))
try:
os.makedirs(mount_point, exist_ok=False)
except Exception as e:
return False, "", f"创建挂载点失败: {e}"
# 跟踪已挂载的资源,用于失败时清理
bind_paths_mounted = []
mounted_boot = False
mounted_efi = False
# 1. 挂载根分区
success, _, stderr = run_command(["sudo", "mount", root_partition, mount_point], f"挂载根分区 {root_partition}")
success, _, stderr = run_command(["sudo", "mount", root_partition, mount_point],
f"挂载根分区 {root_partition}")
if not success:
os.rmdir(mount_point)
return False, "", f"挂载根分区失败: {stderr}"
# 2. 如果有独立 /boot 分区
@@ -115,20 +273,24 @@ def mount_target_system(root_partition, boot_partition=None, efi_partition=None)
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}")
success, _, stderr = run_command(["sudo", "mount", boot_partition, boot_mount_point],
f"挂载 /boot 分区 {boot_partition}")
if not success:
unmount_target_system(mount_point) # 挂载失败,尝试清理
_cleanup_partial_mount(mount_point, False, False, [])
return False, "", f"挂载 /boot 分区失败: {stderr}"
mounted_boot = True
# 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}")
success, _, stderr = run_command(["sudo", "mount", efi_partition, efi_mount_point],
f"挂载 EFI 分区 {efi_partition}")
if not success:
unmount_target_system(mount_point) # 挂载失败,尝试清理
_cleanup_partial_mount(mount_point, mounted_boot, False, [])
return False, "", f"挂载 EFI 分区失败: {stderr}"
mounted_efi = True
# 4. 绑定必要的伪文件系统
bind_paths = ["/dev", "/dev/pts", "/proc", "/sys", "/run"]
@@ -136,63 +298,108 @@ def mount_target_system(root_partition, boot_partition=None, efi_partition=None)
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}")
success, _, stderr = run_command(["sudo", "mount", "--bind", path, target_path],
f"绑定 {path}{target_path}")
if not success:
unmount_target_system(mount_point) # 绑定失败,尝试清理
_cleanup_partial_mount(mount_point, mounted_boot, mounted_efi, bind_paths_mounted)
return False, "", f"绑定 {path} 失败: {stderr}"
bind_paths_mounted.append(target_path)
return True, mount_point, ""
def detect_distro_type(mount_point):
def detect_distro_type(mount_point: str) -> str:
"""
尝试检测目标系统发行版类型。
:param mount_point: 目标系统根分区的挂载点
:return: "arch", "centos", "debian", "ubuntu", "unknown"
:return: "arch", "centos", "debian", "ubuntu", "fedora", "opensuse", "unknown"
"""
os_release_path = os.path.join(mount_point, "etc/os-release")
if not os.path.exists(os_release_path):
# 尝试 /etc/issue 作为备选
issue_path = os.path.join(mount_point, "etc/issue")
if os.path.exists(issue_path):
try:
with open(issue_path, "r") as f:
content = f.read().lower()
if "ubuntu" in content:
return "ubuntu"
elif "debian" in content:
return "debian"
elif "centos" in content or "rhel" in content:
return "centos"
elif "fedora" in content:
return "fedora"
except Exception:
pass
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"
# 解析 ID 和 ID_LIKE 字段
id_match = re.search(r'^ID=(.+)$', content, re.MULTILINE)
id_like_match = re.search(r'^ID_LIKE=(.+)$', content, re.MULTILINE)
distro_id = id_match.group(1).strip('"\'') if id_match else ""
id_like = id_like_match.group(1).strip('"\'') if id_like_match else ""
# 直接匹配
if distro_id == "ubuntu":
return "ubuntu"
elif distro_id == "debian":
return "debian"
elif distro_id in ["arch", "manjaro", "endeavouros"]:
return "arch"
elif distro_id in ["centos", "rhel", "rocky", "almalinux"]:
return "centos"
elif distro_id == "fedora":
return "fedora"
elif distro_id in ["opensuse", "opensuse-leap", "opensuse-tumbleweed"]:
return "opensuse"
# 通过 ID_LIKE 推断
if "ubuntu" in id_like:
return "ubuntu"
elif "debian" in id_like:
return "debian"
elif "arch" in id_like:
return "arch"
elif "rhel" in id_like or "centos" in id_like or "fedora" in id_like:
return "centos" # 使用centos命令集
elif "suse" in id_like:
return "opensuse"
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"):
def chroot_and_repair_grub(mount_point: str, target_disk: str,
is_uefi: bool = False, distro_type: str = "unknown") -> Tuple[bool, str]:
"""
Chroot到目标系统并执行GRUB修复命令。
:param mount_point: 目标系统根分区的挂载点
:param target_disk: GRUB要安装到的物理磁盘 (例如 /dev/sda)
:param target_disk: GRUB要安装到的物理磁盘
:param is_uefi: 目标系统是否使用UEFI启动
:param distro_type: 目标系统发行版类型 (用于选择正确的GRUB命令)
:param distro_type: 目标系统发行版类型
:return: (True/False, error_message)
"""
if not validate_device_path(target_disk):
return False, f"无效的目标磁盘路径: {target_disk}"
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"],
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"
@@ -203,14 +410,30 @@ def chroot_and_repair_grub(mount_point, target_disk, is_uefi=False, distro_type=
# 2. 更新GRUB配置文件
grub_update_cmd = []
if distro_type in ["debian", "ubuntu"]:
grub_update_cmd = ["update-grub"] # 这是一个脚本实际是调用grub-mkconfig
grub_update_cmd = ["update-grub"]
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
# 检测实际存在的配置文件路径
grub2_path = os.path.join(mount_point, "boot/grub2/grub.cfg")
grub_path = os.path.join(mount_point, "boot/grub/grub.cfg")
if os.path.exists(os.path.dirname(grub2_path)):
grub_update_cmd = ["grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"]
else:
grub_update_cmd = ["grub2-mkconfig", "-o", "/boot/grub/grub.cfg"]
elif distro_type == "fedora":
grub_update_cmd = ["grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"]
elif distro_type == "opensuse":
grub_update_cmd = ["grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"]
else:
# 尝试通用命令
grub_update_cmd = ["grub-mkconfig", "-o", "/boot/grub/grub.cfg"]
# 尝试通用命令,先检测配置文件路径
for cfg_path in ["/boot/grub/grub.cfg", "/boot/grub2/grub.cfg"]:
full_path = os.path.join(mount_point, cfg_path.lstrip('/'))
if os.path.exists(os.path.dirname(full_path)):
grub_update_cmd = ["grub-mkconfig", "-o", cfg_path]
break
else:
grub_update_cmd = ["grub-mkconfig", "-o", "/boot/grub/grub.cfg"]
success, _, stderr = run_command(
chroot_cmd_prefix + grub_update_cmd,
@@ -221,7 +444,8 @@ def chroot_and_repair_grub(mount_point, target_disk, is_uefi=False, distro_type=
return True, ""
def unmount_target_system(mount_point):
def unmount_target_system(mount_point: str) -> Tuple[bool, str]:
"""
卸载所有挂载的分区。
:param mount_point: 目标系统根分区的挂载点
@@ -231,17 +455,21 @@ def unmount_target_system(mount_point):
success = True
error_msg = ""
# 逆序卸载绑定
# 按照挂载的逆序卸载
# 挂载顺序: /dev, /dev/pts, /proc, /sys, /run, /boot/efi, /boot, 根分区
# 卸载顺序: /run, /sys, /proc, /dev/pts, /dev, /boot/efi, /boot, 根分区
# 1. 卸载绑定路径(先卸载子目录)
bind_paths = ["/run", "/sys", "/proc", "/dev/pts", "/dev"]
for path in reversed(bind_paths): # 逆序
for path in bind_paths:
target_path = os.path.join(mount_point, path.lstrip('/'))
if os.path.ismount(target_path): # 检查是否真的挂载了
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 (如果存在)
# 2. 卸载 /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}")
@@ -249,7 +477,7 @@ def unmount_target_system(mount_point):
success = False
error_msg += f"卸载 {efi_mount_point} 失败: {stderr}\n"
# 卸载 /boot (如果存在)
# 3. 卸载 /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}")
@@ -257,25 +485,25 @@ def unmount_target_system(mount_point):
success = False
error_msg += f"卸载 {boot_mount_point} 失败: {stderr}\n"
# 卸载根分区
# 4. 卸载根分区
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"
# 清理临时挂载点目录
# 5. 清理临时挂载点目录
if os.path.exists(mount_point) and not os.path.ismount(mount_point):
try:
os.rmdir(mount_point)
shutil.rmtree(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("--- 运行后端测试 ---")

View File

@@ -3,6 +3,8 @@
import tkinter as tk
from tkinter import ttk, messagebox
import threading
import os
import re
import backend # 导入后端逻辑
class GrubRepairApp:
@@ -153,18 +155,31 @@ class GrubRepairApp:
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:
root_part_name = self.selected_root_partition_info['name']
self.log_message(f"已选择根分区: {root_part_name}", "info")
# 检查是否是 LVM 逻辑卷
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("无法自动选择目标磁盘,请手动选择", "warning")
self.log_message("检测到 LVM 逻辑卷,请手动选择 GRUB 安装的目标磁盘", "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")
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("")
@@ -215,71 +230,76 @@ class GrubRepairApp:
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']
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")
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
# 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")
self.master.after(0, lambda: messagebox.showerror("修复失败", f"无法挂载目标系统分区。请检查分区选择和权限。\n错误: {mount_err}"))
return
# 2. 尝试识别发行版类型
self.log_message("正在检测目标系统发行版...", "info")
distro_type = backend.detect_distro_type(self.mount_point)
self.log_message(f"检测到目标系统发行版: {distro_type}", "info")
# 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")
# 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")
self.master.after(0, lambda: 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")
# 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")
self.master.after(0, lambda: messagebox.showwarning("卸载警告", f"部分分区可能未能正确卸载。请手动检查并卸载。\n错误: {unmount_err}"))
else:
self.log_message("所有分区已成功卸载。", "success")
self.log_message("--- GRUB 修复过程结束 ---", "info")
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引导修复过程中发生错误。请检查日志并尝试手动修复。")
# 最终结果提示
if repair_ok and unmount_ok:
self.master.after(0, lambda: messagebox.showinfo("修复成功", "GRUB引导修复已完成请重启系统以验证。"))
elif repair_ok and not unmount_ok:
self.master.after(0, lambda: messagebox.showwarning("修复完成,但有警告", "GRUB引导修复已完成但部分分区卸载失败。请重启系统以验证。"))
else:
self.master.after(0, lambda: messagebox.showerror("修复失败", "GRUB引导修复过程中发生错误。请检查日志并尝试手动修复。"))
self.start_button.config(state="normal")
except Exception as e:
self.log_message(f"发生未预期的错误: {str(e)}", "error")
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"))
if __name__ == "__main__":