From 622e894883037028d839e177d0d7e9b86df2214b Mon Sep 17 00:00:00 2001 From: zj <1052308357@qq.com> Date: Wed, 11 Feb 2026 03:34:53 +0800 Subject: [PATCH] add log --- __pycache__/backend.cpython-36.pyc | Bin 13064 -> 20136 bytes backend.py | 524 ++++++++++++++++++++--------- 2 files changed, 357 insertions(+), 167 deletions(-) diff --git a/__pycache__/backend.cpython-36.pyc b/__pycache__/backend.cpython-36.pyc index f427b280c296e1db47ef444b5b890c55b6a35ebd..b9e7183f35df37d3496e9a12292c19c532cccb1c 100644 GIT binary patch literal 20136 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�NFkVasktV$zOvjbjgu8>aYe33mYv?!2DNyL8lX9 zR|Q`Hmj_Oaz6PWCcYVpQS`HVgDC2|za#hop>dW-y_`Y!AE7Y#w+6Uouk(&im9l9w} zFRer=%B3KuAtyqP8SvbXi!{sh1mlTvUgC6s^J|PV-vlv)+ErZpOjGUlz5RL;F2>z( zr9Aph%Sc$ifnAK)EV^)WnC>AOKO8HJe>=GZhoGzKUqo}{gfTy>o9744&sEIO*BdjW ze*^OQW_vw00n51ic9W?OvuT!a#@RU^fqv(Kof|G+I$L??s5}_`D~M@0-MI1MSv0hP zdlN(jXYZA>hiBeCKtp+Ta{BPk5RZ|8PbuDIKX0$kQ3`zHMxKk4d0&}2SNYMI=59@# zF}v#=H6;7gG|8G~zhIuoXrE2tz`B|5%^(MBYlgfR=NaT#F2%^8Pvr_Y8p!8M3sWgM z4fpK5>yFGFx2M3wrKKr)rC^gn=oH1id)RsCp{7n4y;v&>TpA*k)9;~q;~~hIqfh6# zq{~C+Pxn9g!1}F-O_i>v`aM~Z%R&*DiL5XZg`$-(aVSv~7W6_C*a`F4ut*2Eo4pes z)=&~NADphd@T1-Y&F!&Qt{glzbN14e6WA7X?oQ!oR`Qq6AC+ssC=zQ@sX{ST*n{Ox zCgcd$u3cM-HmdCes9ZaC;AtAZO+xG1p~!m(-j&UcHAm1}7sEe79VYuscjb<`{Cx7$i_!g_0}hW6^b zci_Y>8W-zl@}2C(Na(uB%OBD=Tsd%b`r=us@A+oJ+)4>r^QL569_~Q>W0BuS1WHJf zn%5qYKGzM@Kx%tlYaoP8&WDmepsuab!>|c@4<%0`NiUF`lNlUCTw7qD?iJsn#xy0w zed#uugN&ST0!v4EQlwQ7;YPYUhJ((bVQC&jhHSn`VC|mkZY}nx;gVO)?Ir{c>HBdI zBabVNV=F)6^+9O(m_Lq`epL43iTO$MQ;D9qkK+0`{z8uG!&JgS8ighoqn-}d_<{EC zL`{?q#Ta)?LvKx#L9>1vj|`Lgsn}xnw`A~M*Itu>J6{<$7Ry>qYwHmNvwVA@fuLy~ zM?fDw7~L*&{3Cw4jQ|Z7;5yuXfG^f<1@#tqI25dJb>wX^(!$X9(5gY4v&GA}`5@a6 z+`|c&-u_zPPNl3p(ftsel`(dG0qapJ1xRO#-+6c=z}w+@4$XLf=i%kH*?8+Xq4lAS zB~FYSTDaH48fB~;tYppSjTV}(w~iOB^WcTfXH1hGCOTT^YzmDwFim=xxOE;(d<+2} zDejFBc=>)oeBTQ48s$+0j#?Hqj#?1R?T2p_W#1|W->L<^)jGf|CK%kbXyLFSJS@z3 zSEH^r;}5}?h4x=sZh5LjdR}xZWe7GsiINXuS_%*u>+#|~UJ$4e72+rn2ZC6=iawyX z$;O>^deS#L4&pYQ=<9!j=ufuHD=A4fiu);HVM@^`cuKo%E0<3P#h4`wLu^H5GA89GNyl`= zGgi!D@d$MdQbO@1S2Y`=ljBe_A~m1mv&b&Y6Mgy@l7-&|DkVr>jz1)$Mlp2-mEhly zHzf0mk&h!!l9BSY+)BvCC_I}E$hXqfjnbY3D9X2+;UZYj)u4zy*m#l3%A|I?_CPCd|f#RR|wD?jFjW~YmgqFLz-zU zYtXc5gk+F0H~-}n#ev$_OTxrYOmle?O+KjPTMCk_XBH&2e%$=-lRj!f(A`L>@$aGB zLzIwei9jzY=n&~(!4j2u!`x34XjMcO*94erRa>I9z6?Hpr+)E#1ejACj3uM|Q sEe3!4A`QH-_Qs&v{_u*GLACP+MP2i~_P#{A>u%Eh6wgI1*#g@C0NIh|u>b%7 diff --git a/backend.py b/backend.py index 65fe621..45984b1 100644 --- a/backend.py +++ b/backend.py @@ -6,6 +6,7 @@ import json import time import re import shutil +import glob from typing import Tuple, List, Dict, Optional # 常量配置 @@ -16,6 +17,36 @@ DEFAULT_GRUB_CONFIG_PATHS = [ ] +def log_step(step_name: str, detail: str = ""): + """输出步骤日志""" + separator = "=" * 60 + print(f"\n[STEP] {separator}") + print(f"[STEP] {step_name}") + if detail: + print(f"[STEP] 详情: {detail}") + print(f"[STEP] {separator}\n") + + +def log_info(msg: str): + """输出信息日志""" + print(f"[INFO] {msg}") + + +def log_warning(msg: str): + """输出警告日志""" + print(f"[WARN] {msg}") + + +def log_error(msg: str): + """输出错误日志""" + print(f"[ERROR] {msg}") + + +def log_debug(msg: str): + """输出调试日志""" + print(f"[DEBUG] {msg}") + + def validate_device_path(path: str) -> bool: """ 验证设备路径格式是否合法(防止命令注入)。 @@ -24,12 +55,6 @@ def validate_device_path(path: str) -> bool: """ 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_-]+$', @@ -42,15 +67,12 @@ def run_command(command: List[str], description: str = "执行命令", timeout: int = COMMAND_TIMEOUT) -> Tuple[bool, str, str]: """ 运行一个系统命令,并捕获其输出和错误。 - :param command: 要执行的命令列表 (例如 ["sudo", "lsblk"]) - :param description: 命令的描述,用于日志 - :param cwd: 更改工作目录 - :param shell: 是否使用shell执行命令 - :param timeout: 命令超时时间(秒) - :return: (success, stdout, stderr) 表示成功、标准输出、标准错误 """ + cmd_str = ' '.join(command) + log_info(f"执行命令: {cmd_str}") + log_debug(f"工作目录: {cwd or '当前目录'}, 超时: {timeout}秒") + try: - print(f"[Backend] {description}: {' '.join(command)}") result = subprocess.run( command, capture_output=True, @@ -60,20 +82,26 @@ def run_command(command: List[str], description: str = "执行命令", shell=shell, timeout=timeout ) - print(f"[Backend] 命令成功: {description}") + if result.stdout: + log_debug(f"stdout: {result.stdout[:500]}") # 限制输出长度 + if result.stderr: + log_debug(f"stderr: {result.stderr[:500]}") + log_info(f"✓ 命令成功: {description}") return True, result.stdout, result.stderr except subprocess.CalledProcessError as e: - print(f"[Backend] 命令失败: {description}") - print(f"[Backend] 错误输出: {e.stderr}") + log_error(f"✗ 命令失败: {description}") + log_error(f"返回码: {e.returncode}") + log_error(f"stdout: {e.stdout}") + log_error(f"stderr: {e.stderr}") return False, e.stdout, e.stderr except subprocess.TimeoutExpired: - print(f"[Backend] 命令超时: {description}") - return False, "", f"命令执行超时(超过 {timeout} 秒)" + log_error(f"✗ 命令超时(超过 {timeout} 秒): {description}") + return False, "", f"命令执行超时" except FileNotFoundError: - print(f"[Backend] 命令未找到: {' '.join(command)}") + log_error(f"✗ 命令未找到: {command[0]}") return False, "", f"命令未找到: {command[0]}" except Exception as e: - print(f"[Backend] 发生未知错误: {e}") + log_error(f"✗ 发生未知错误: {e}") return False, "", str(e) @@ -87,18 +115,15 @@ def _process_partition(block_device: Dict, all_disks: List, all_partitions: List 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 @@ -111,11 +136,10 @@ def _process_partition(block_device: Dict, all_disks: List, all_partitions: List "uuid": block_device.get("uuid"), "partlabel": block_device.get("partlabel"), "label": block_device.get("label"), - "parttype": (block_device.get("parttype") or "").upper() # EFI分区类型GUID + "parttype": (block_device.get("parttype") or "").upper() } 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() @@ -124,24 +148,13 @@ def _process_partition(block_device: Dict, all_disks: List, all_partitions: List 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: @@ -161,16 +174,16 @@ def _process_partition(block_device: Dict, all_disks: List, all_partitions: List } 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) + 扫描系统中的所有磁盘和分区。 """ + log_step("扫描系统分区", "使用 lsblk 获取磁盘和分区信息") + success, stdout, stderr = run_command( ["sudo", "lsblk", "-J", "-o", "NAME,FSTYPE,SIZE,MOUNTPOINT,UUID,PARTLABEL,LABEL,TYPE,PARTTYPE"], "扫描分区" @@ -188,43 +201,46 @@ def scan_partitions() -> Tuple[bool, List, List, List, str]: for block_device in data.get("blockdevices", []): _process_partition(block_device, all_disks, all_partitions, all_efi_partitions) + log_info(f"扫描完成: 发现 {len(all_disks)} 个磁盘, {len(all_partitions)} 个分区, {len(all_efi_partitions)} 个EFI分区") + for d in all_disks: + log_debug(f" 磁盘: {d['name']}") + for p in all_partitions: + log_debug(f" 分区: {p['name']} ({p['fstype']})") + for e in all_efi_partitions: + log_info(f" EFI分区: {e['name']}") + return True, all_disks, all_partitions, all_efi_partitions, "" except json.JSONDecodeError as e: + log_error(f"解析 lsblk 输出失败: {e}") return False, [], [], [], f"解析lsblk输出失败: {e}" except Exception as e: + log_error(f"处理分区数据时发生未知错误: {e}") return False, [], [], [], f"处理分区数据时发生未知错误: {e}" def _cleanup_partial_mount(mount_point: str, mounted_boot: bool, mounted_efi: bool, bind_paths_mounted: List[str]) -> None: - """ - 清理部分挂载的资源(用于挂载失败时的回滚)。 - """ - print(f"[Backend] 清理部分挂载资源: {mount_point}") + """清理部分挂载的资源""" + log_warning(f"清理部分挂载资源: {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) @@ -236,253 +252,451 @@ def mount_target_system(root_partition: str, boot_partition: Optional[str] = Non efi_partition: Optional[str] = None) -> Tuple[bool, str, str]: """ 挂载目标系统的根分区、/boot分区和EFI分区到临时目录。 - :param root_partition: 目标系统的根分区设备路径 - :param boot_partition: 目标系统的独立 /boot 分区设备路径 (可选) - :param efi_partition: 目标系统的EFI系统分区设备路径 (可选,仅UEFI) - :return: (True/False, mount_point, error_message) """ - # 验证输入 + log_step("挂载目标系统", f"根分区: {root_partition}, /boot: {boot_partition or '无'}, EFI: {efi_partition or '无'}") + if not validate_device_path(root_partition): + log_error(f"无效的根分区路径: {root_partition}") return False, "", f"无效的根分区路径: {root_partition}" if boot_partition and not validate_device_path(boot_partition): + log_error(f"无效的/boot分区路径: {boot_partition}") return False, "", f"无效的/boot分区路径: {boot_partition}" if efi_partition and not validate_device_path(efi_partition): + log_error(f"无效的EFI分区路径: {efi_partition}") return False, "", f"无效的EFI分区路径: {efi_partition}" - # 创建临时挂载点 mount_point = "/mnt_grub_repair_" + str(int(time.time())) + log_info(f"创建临时挂载点: {mount_point}") + try: os.makedirs(mount_point, exist_ok=False) + log_info(f"✓ 挂载点创建成功") except Exception as e: + log_error(f"创建挂载点失败: {e}") return False, "", f"创建挂载点失败: {e}" - # 跟踪已挂载的资源,用于失败时清理 bind_paths_mounted = [] mounted_boot = False mounted_efi = False # 1. 挂载根分区 + log_info(f"[1/4] 挂载根分区 {root_partition} 到 {mount_point}") success, _, stderr = run_command(["sudo", "mount", root_partition, mount_point], f"挂载根分区 {root_partition}") if not success: + log_error(f"挂载根分区失败,清理中...") os.rmdir(mount_point) return False, "", f"挂载根分区失败: {stderr}" + + # 检查挂载是否成功 + if not os.path.ismount(mount_point): + log_error(f"挂载点未激活: {mount_point}") + return False, "", "挂载根分区失败: 挂载点未激活" + log_info(f"✓ 根分区挂载成功") - # 2. 如果有独立 /boot 分区 + # 检查关键目录 + for check_dir in ["etc", "boot"]: + full_path = os.path.join(mount_point, check_dir) + if os.path.exists(full_path): + log_info(f" 发现目录: /{check_dir}") + else: + log_warning(f" 未找到目录: /{check_dir}") + + # 2. 挂载 /boot 分区 if boot_partition: + log_info(f"[2/4] 挂载 /boot 分区 {boot_partition}") boot_mount_point = os.path.join(mount_point, "boot") if not os.path.exists(boot_mount_point): os.makedirs(boot_mount_point) + log_debug(f"创建 /boot 挂载点") + success, _, stderr = run_command(["sudo", "mount", boot_partition, boot_mount_point], f"挂载 /boot 分区 {boot_partition}") if not success: + log_error(f"挂载 /boot 分区失败,开始清理...") _cleanup_partial_mount(mount_point, False, False, []) return False, "", f"挂载 /boot 分区失败: {stderr}" mounted_boot = True + log_info(f"✓ /boot 分区挂载成功") + else: + log_info(f"[2/4] 无独立的 /boot 分区,跳过") - # 3. 如果有EFI分区 (UEFI系统) + # 3. 挂载 EFI 分区 if efi_partition: + log_info(f"[3/4] 挂载 EFI 分区 {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) + log_debug(f"创建 /boot/efi 挂载点") + success, _, stderr = run_command(["sudo", "mount", efi_partition, efi_mount_point], f"挂载 EFI 分区 {efi_partition}") if not success: + log_error(f"挂载 EFI 分区失败,开始清理...") _cleanup_partial_mount(mount_point, mounted_boot, False, []) return False, "", f"挂载 EFI 分区失败: {stderr}" mounted_efi = True + log_info(f"✓ EFI 分区挂载成功") + + # 检查 EFI 分区内容 + efi_path = os.path.join(mount_point, "boot/efi/EFI") + if os.path.exists(efi_path): + log_info(f" EFI 分区内容:") + try: + for item in os.listdir(efi_path): + log_info(f" - {item}") + except Exception as e: + log_warning(f" 无法列出 EFI 目录: {e}") + else: + log_info(f"[3/4] 无 EFI 分区,跳过") - # 4. 绑定必要的伪文件系统 + # 4. 绑定伪文件系统 + log_info(f"[4/4] 绑定伪文件系统 (/dev, /proc, /sys 等)") 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) + log_debug(f"创建目录: {target_path}") + success, _, stderr = run_command(["sudo", "mount", "--bind", path, target_path], - f"绑定 {path} 到 {target_path}") + f"绑定 {path}") if not success: + log_error(f"绑定 {path} 失败,开始清理...") _cleanup_partial_mount(mount_point, mounted_boot, mounted_efi, bind_paths_mounted) return False, "", f"绑定 {path} 失败: {stderr}" bind_paths_mounted.append(target_path) - + + log_info(f"✓ 所有绑定完成") + log_info(f"挂载摘要: 根分区 ✓, /boot {'✓' if mounted_boot else '✗'}, EFI {'✓' if mounted_efi else '✗'}") + return True, mount_point, "" def detect_distro_type(mount_point: str) -> str: """ - 尝试检测目标系统发行版类型。 - :param mount_point: 目标系统根分区的挂载点 - :return: "arch", "centos", "debian", "ubuntu", "fedora", "opensuse", "unknown" + 检测目标系统发行版类型。 """ + log_step("检测发行版类型", f"挂载点: {mount_point}") + os_release_path = os.path.join(mount_point, "etc/os-release") + log_info(f"检查文件: {os_release_path}") + if not os.path.exists(os_release_path): - # 尝试 /etc/issue 作为备选 + log_warning(f"未找到 {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() + log_debug(f"/etc/issue 内容: {content[:200]}") if "ubuntu" in content: + log_info(f"通过 /etc/issue 检测到: ubuntu") return "ubuntu" elif "debian" in content: + log_info(f"通过 /etc/issue 检测到: debian") return "debian" elif "centos" in content or "rhel" in content: + log_info(f"通过 /etc/issue 检测到: centos") return "centos" elif "fedora" in content: + log_info(f"通过 /etc/issue 检测到: fedora") return "fedora" - except Exception: - pass + except Exception as e: + log_warning(f"读取 /etc/issue 失败: {e}") return "unknown" try: with open(os_release_path, "r") as f: content = f.read() + log_debug(f"/etc/os-release 内容:\n{content}") - # 解析 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 "" - # 直接匹配 + log_info(f"ID={distro_id}, ID_LIKE={id_like}") + if distro_id == "ubuntu": + log_info(f"检测到发行版: ubuntu") return "ubuntu" elif distro_id == "debian": + log_info(f"检测到发行版: debian") return "debian" elif distro_id in ["arch", "manjaro", "endeavouros"]: + log_info(f"检测到发行版: arch (ID={distro_id})") return "arch" elif distro_id in ["centos", "rhel", "rocky", "almalinux"]: + log_info(f"检测到发行版: centos (ID={distro_id})") return "centos" elif distro_id == "fedora": + log_info(f"检测到发行版: fedora") return "fedora" elif distro_id in ["opensuse", "opensuse-leap", "opensuse-tumbleweed"]: + log_info(f"检测到发行版: opensuse (ID={distro_id})") return "opensuse" - # 通过 ID_LIKE 推断 if "ubuntu" in id_like: + log_info(f"通过 ID_LIKE 推断: ubuntu") return "ubuntu" elif "debian" in id_like: + log_info(f"通过 ID_LIKE 推断: debian") return "debian" elif "arch" in id_like: + log_info(f"通过 ID_LIKE 推断: arch") return "arch" elif "rhel" in id_like or "centos" in id_like or "fedora" in id_like: - return "centos" # 使用centos命令集 + log_info(f"通过 ID_LIKE 推断: centos") + return "centos" elif "suse" in id_like: + log_info(f"通过 ID_LIKE 推断: opensuse") return "opensuse" + log_warning(f"无法识别的发行版,ID={distro_id}, ID_LIKE={id_like}") return "unknown" except Exception as e: - print(f"[Backend] 无法读取os-release文件: {e}") + log_error(f"读取 os-release 文件失败: {e}") return "unknown" +def check_chroot_environment(mount_point: str) -> Tuple[bool, str]: + """ + 检查 chroot 环境是否可用。 + """ + log_step("检查 chroot 环境", f"挂载点: {mount_point}") + + # 检查关键命令是否存在 + critical_commands = ["grub-install", "grub-mkconfig", "update-grub"] + found_commands = [] + + for cmd in critical_commands: + success, _, _ = run_command(["sudo", "chroot", mount_point, "which", cmd], + f"检查命令 {cmd}", timeout=10) + if success: + found_commands.append(cmd) + log_info(f" ✓ 找到命令: {cmd}") + else: + log_warning(f" ✗ 未找到命令: {cmd}") + + if not found_commands: + log_error(f"chroot 环境中未找到关键的 GRUB 命令") + return False, "chroot 环境中缺少 GRUB 命令" + + # 检查 grub-install 版本 + success, stdout, _ = run_command(["sudo", "chroot", mount_point, "grub-install", "--version"], + "检查 grub-install 版本", timeout=10) + if success: + log_info(f"grub-install 版本: {stdout.strip()}") + + # 检查 EFI 目录(UEFI 模式) + efi_dir = os.path.join(mount_point, "boot/efi") + if os.path.ismount(efi_dir): + log_info(f"✓ EFI 分区已挂载到 /boot/efi") + # 检查 EFI 目录权限 + try: + stat = os.stat(efi_dir) + log_info(f" EFI 目录权限: {oct(stat.st_mode)}") + except Exception as e: + log_warning(f" 无法获取 EFI 目录权限: {e}") + else: + log_info(f"EFI 分区未挂载(可能是 BIOS 模式)") + + return True, "" + + 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要安装到的物理磁盘 - :param is_uefi: 目标系统是否使用UEFI启动 - :param distro_type: 目标系统发行版类型 - :return: (True/False, error_message) """ + log_step("修复 GRUB", f"目标磁盘: {target_disk}, UEFI: {is_uefi}, 发行版: {distro_type}") + if not validate_device_path(target_disk): + log_error(f"无效的目标磁盘路径: {target_disk}") return False, f"无效的目标磁盘路径: {target_disk}" + # 检查 chroot 环境 + ok, err = check_chroot_environment(mount_point) + if not ok: + return False, err + chroot_cmd_prefix = ["sudo", "chroot", mount_point] - # 1. 安装GRUB到目标磁盘 + # 1. 安装GRUB + log_info(f"[1/2] 安装 GRUB 到 {target_disk}") + if is_uefi: - # UEFI模式:首先尝试正常安装(注册NVRAM启动项) - success, _, stderr = run_command( + log_info(f"UEFI 模式安装...") + + # 检查 EFI 分区是否正确挂载 + efi_check_path = os.path.join(mount_point, "boot/efi/EFI") + if os.path.exists(efi_check_path): + log_info(f"✓ EFI 目录存在: {efi_check_path}") + try: + contents = os.listdir(efi_check_path) + log_info(f" 当前 EFI 目录内容: {contents}") + except Exception as e: + log_warning(f" 无法列出 EFI 目录: {e}") + else: + log_warning(f"✗ EFI 目录不存在: {efi_check_path}") + + # 第一次尝试:正常安装 + log_info(f"尝试 1/3: 标准 UEFI 安装(带 NVRAM)...") + success, stdout, stderr = run_command( chroot_cmd_prefix + ["grub-install", "--target=x86_64-efi", - "--efi-directory=/boot/efi", "--bootloader-id=GRUB"], - "安装UEFI GRUB(带NVRAM)" + "--efi-directory=/boot/efi", "--bootloader-id=GRUB", + "--verbose"], + "安装UEFI GRUB(带NVRAM)", + timeout=60 ) - # 如果失败是因为EFI变量不支持(常见于Live环境修复外部磁盘),使用 --no-nvram 重试 - if not success and ("EFI variables are not supported" in stderr or - "efibootmgr" in stderr or - "NVRAM" in stderr): - print(f"[Backend] 警告: EFI变量访问失败,尝试不使用NVRAM (--no-nvram) 安装...") - success, _, stderr = run_command( - chroot_cmd_prefix + ["grub-install", "--target=x86_64-efi", - "--efi-directory=/boot/efi", "--bootloader-id=GRUB", - "--no-nvram"], - "安装UEFI GRUB(不带NVRAM)" - ) - - # 如果仍然失败,尝试 --removable 选项(某些固件需要) if not success: - print(f"[Backend] 警告: 标准安装失败,尝试 --removable 选项...") - success, _, stderr = run_command( - chroot_cmd_prefix + ["grub-install", "--target=x86_64-efi", - "--efi-directory=/boot/efi", - "--removable"], - "安装UEFI GRUB(可移动模式)" - ) + log_warning(f"标准安装失败,错误: {stderr}") + + if "EFI variables are not supported" in stderr or "efibootmgr" in stderr: + log_info(f"尝试 2/3: 使用 --no-nvram 选项...") + success, stdout, stderr = run_command( + chroot_cmd_prefix + ["grub-install", "--target=x86_64-efi", + "--efi-directory=/boot/efi", "--bootloader-id=GRUB", + "--no-nvram", "--verbose"], + "安装UEFI GRUB(不带NVRAM)", + timeout=60 + ) + + if not success: + log_info(f"尝试 3/3: 使用 --removable 选项...") + success, stdout, stderr = run_command( + chroot_cmd_prefix + ["grub-install", "--target=x86_64-efi", + "--efi-directory=/boot/efi", + "--removable", "--verbose"], + "安装UEFI GRUB(可移动模式)", + timeout=60 + ) + + # 检查安装结果 + if success: + log_info(f"✓ GRUB EFI 文件安装成功") + # 检查生成的文件 + efi_grub_path = os.path.join(mount_point, "boot/efi/EFI/GRUB") + efi_boot_path = os.path.join(mount_point, "boot/efi/EFI/Boot") + + for path in [efi_grub_path, efi_boot_path]: + if os.path.exists(path): + log_info(f" 检查目录: {path}") + try: + files = os.listdir(path) + for f in files: + full = os.path.join(path, f) + size = os.path.getsize(full) + log_info(f" - {f} ({size} bytes)") + except Exception as e: + log_warning(f" 无法列出: {e}") else: - success, _, stderr = run_command( - chroot_cmd_prefix + ["grub-install", target_disk], - "安装BIOS GRUB" + log_info(f"BIOS 模式安装...") + success, stdout, stderr = run_command( + chroot_cmd_prefix + ["grub-install", "--verbose", target_disk], + "安装BIOS GRUB", + timeout=60 ) + if not success: + log_error(f"GRUB 安装失败") return False, f"GRUB安装失败: {stderr}" + + log_info(f"✓ GRUB 安装成功") - # 2. 更新GRUB配置文件 + # 2. 更新GRUB配置 + log_info(f"[2/2] 更新 GRUB 配置文件") + grub_update_cmd = [] + config_path = "" + if distro_type in ["debian", "ubuntu"]: grub_update_cmd = ["update-grub"] + config_path = "/boot/grub/grub.cfg" elif distro_type == "arch": grub_update_cmd = ["grub-mkconfig", "-o", "/boot/grub/grub.cfg"] + config_path = "/boot/grub/grub.cfg" elif distro_type == "centos": - # 检测实际存在的配置文件路径 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"] + config_path = "/boot/grub2/grub.cfg" else: grub_update_cmd = ["grub2-mkconfig", "-o", "/boot/grub/grub.cfg"] + config_path = "/boot/grub/grub.cfg" elif distro_type == "fedora": grub_update_cmd = ["grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"] + config_path = "/boot/grub2/grub.cfg" elif distro_type == "opensuse": grub_update_cmd = ["grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"] + config_path = "/boot/grub2/grub.cfg" else: - # 尝试通用命令,先检测配置文件路径 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] + config_path = cfg_path break else: grub_update_cmd = ["grub-mkconfig", "-o", "/boot/grub/grub.cfg"] - - success, _, stderr = run_command( + config_path = "/boot/grub/grub.cfg" + + log_info(f"使用命令: {' '.join(grub_update_cmd)}") + log_info(f"配置文件路径: {config_path}") + + success, stdout, stderr = run_command( chroot_cmd_prefix + grub_update_cmd, - "更新GRUB配置文件" + "更新GRUB配置文件", + timeout=120 ) + if not success: + log_error(f"GRUB 配置文件更新失败: {stderr}") return False, f"GRUB配置文件更新失败: {stderr}" - + + log_info(f"✓ GRUB 配置文件更新成功") + + # 检查生成的配置文件 + full_config_path = os.path.join(mount_point, config_path.lstrip('/')) + if os.path.exists(full_config_path): + size = os.path.getsize(full_config_path) + log_info(f" 配置文件大小: {size} bytes") + # 检查配置文件中是否包含菜单项 + try: + with open(full_config_path, 'r') as f: + content = f.read() + menu_entries = content.count('menuentry') + log_info(f" 发现 {menu_entries} 个启动菜单项") + except Exception as e: + log_warning(f" 无法读取配置文件: {e}") + else: + log_warning(f" 配置文件未找到: {full_config_path}") + + # UEFI 模式下输出重要提示 + if is_uefi: + log_step("UEFI 修复完成提示") + log_info(f"GRUB EFI 文件已安装到 EFI 分区") + log_info(f"如果无法启动,请检查:") + log_info(f" 1. BIOS/UEFI 设置中是否启用了 UEFI 启动模式") + log_info(f" 2. 启动顺序中是否有 'GRUB' 或 'Linux' 选项") + log_info(f" 3. 安全启动 (Secure Boot) 是否已禁用") + log_info(f" 4. EFI 分区中的文件: /EFI/GRUB/grubx64.efi") + return True, "" def unmount_target_system(mount_point: str) -> Tuple[bool, str]: """ 卸载所有挂载的分区。 - :param mount_point: 目标系统根分区的挂载点 - :return: (True/False, error_message) """ - print(f"[Backend] 正在卸载分区 from {mount_point}...") + log_step("卸载目标系统", f"挂载点: {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 bind_paths: target_path = os.path.join(mount_point, path.lstrip('/')) @@ -491,43 +705,51 @@ def unmount_target_system(mount_point: str) -> Tuple[bool, str]: if not s: success = False error_msg += f"卸载绑定 {target_path} 失败: {stderr}\n" + else: + log_debug(f"未挂载,跳过: {target_path}") - # 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}") + s, _, stderr = run_command(["sudo", "umount", efi_mount_point], f"卸载 EFI 分区") if not s: success = False - error_msg += f"卸载 {efi_mount_point} 失败: {stderr}\n" + error_msg += f"卸载 EFI 分区失败: {stderr}\n" + else: + log_debug(f"EFI 分区未挂载,跳过") - # 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}") + s, _, stderr = run_command(["sudo", "umount", boot_mount_point], f"卸载 /boot 分区") if not s: success = False - error_msg += f"卸载 {boot_mount_point} 失败: {stderr}\n" + error_msg += f"卸载 /boot 分区失败: {stderr}\n" + else: + log_debug(f"/boot 分区未挂载,跳过") - # 4. 卸载根分区 if os.path.ismount(mount_point): - s, _, stderr = run_command(["sudo", "umount", mount_point], f"卸载根分区 {mount_point}") + s, _, stderr = run_command(["sudo", "umount", mount_point], f"卸载根分区") if not s: success = False - error_msg += f"卸载根分区 {mount_point} 失败: {stderr}\n" + error_msg += f"卸载根分区失败: {stderr}\n" + else: + log_debug(f"根分区未挂载,跳过") - # 5. 清理临时挂载点目录 if os.path.exists(mount_point) and not os.path.ismount(mount_point): try: shutil.rmtree(mount_point) - print(f"[Backend] 清理临时目录 {mount_point}") + log_info(f"✓ 清理临时目录: {mount_point}") except OSError as e: - print(f"[Backend] 无法删除临时目录 {mount_point}: {e}") - error_msg += f"无法删除临时目录 {mount_point}: {e}\n" + log_warning(f"无法删除临时目录 {mount_point}: {e}") + error_msg += f"无法删除临时目录: {e}\n" + if success: + log_info(f"✓ 所有分区已卸载") + else: + log_warning(f"部分分区卸载失败") + return success, error_msg -# 示例:如果直接运行backend.py,可以进行一些测试 if __name__ == "__main__": print("--- 运行后端测试 ---") @@ -539,35 +761,3 @@ if __name__ == "__main__": 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 进行完整测试。")