This commit is contained in:
zj
2026-02-04 21:23:16 +08:00
parent db1a92a457
commit d8d3bc310e
8 changed files with 219 additions and 60 deletions

View File

@@ -4,8 +4,8 @@ import logging
import re
import os
from PySide6.QtWidgets import QMessageBox, QInputDialog
from PySide6.QtCore import QObject, Signal, QThread # <--- 导入 QObject, Signal, QThread
from system_info import SystemInfoManager # 确保 SystemInfoManager 已导入
from PySide6.QtCore import QObject, Signal, QThread
from system_info import SystemInfoManager
logger = logging.getLogger(__name__)
@@ -134,7 +134,7 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
# Use sed for robust removal with sudo
# suppress_critical_dialog_on_stderr_match is added to handle cases where fstab might not exist
# or sed reports no changes, which is not a critical error for removal.
command = ["sed", "-i", f"/UUID={uuid}/d", fstab_path]
command = ["sed", "-i", f"/{re.escape(uuid)}/d", fstab_path] # Use re.escape for UUID
success, _, stderr = self._execute_shell_command(
command,
f"{fstab_path} 中删除 UUID={uuid} 的条目失败",
@@ -163,6 +163,106 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
return True # Consider it a success if the entry is not there
return False # Other errors are already handled by _execute_shell_command
def _resolve_device_occupation(self, device_path, action_description="操作"):
"""
尝试解决设备占用问题。
检查:
1. 是否是交换分区,如果是则提示用户关闭。
2. 是否有进程占用,如果有则提示用户终止。
:param device_path: 要检查的设备路径。
:param action_description: 正在尝试的操作描述,用于用户提示。
:return: True 如果占用已解决或没有占用False 如果用户取消或解决失败。
"""
logger.info(f"尝试解决设备 {device_path} 的占用问题,以便进行 {action_description}")
# 1. 检查是否是交换分区
block_devices = self.system_manager.get_block_devices()
device_info = self.system_manager._find_device_by_path_recursive(block_devices, device_path)
# Check if it's a swap partition and currently active
if device_info and device_info.get('fstype') == 'swap' and device_info.get('mountpoint') == '[SWAP]':
reply = QMessageBox.question(
None,
"设备占用 - 交换分区",
f"设备 {device_path} 是一个活跃的交换分区。为了进行 {action_description},需要关闭它。您要继续吗?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.No:
logger.info(f"用户取消了关闭交换分区 {device_path}")
return False
logger.info(f"尝试关闭交换分区 {device_path}")
success, _, stderr = self._execute_shell_command(
["swapoff", device_path],
f"关闭交换分区 {device_path} 失败",
show_dialog=True # Show dialog for swapoff failure
)
if not success:
logger.error(f"关闭交换分区 {device_path} 失败: {stderr}")
QMessageBox.critical(None, "错误", f"关闭交换分区 {device_path} 失败。无法进行 {action_description}")
return False
QMessageBox.information(None, "信息", f"交换分区 {device_path} 已成功关闭。")
# After swapoff, it might still be reported by fuser, so continue to fuser check.
# 2. 检查是否有进程占用 (使用 fuser)
success_fuser, stdout_fuser, stderr_fuser = self._execute_shell_command(
["fuser", "-vm", device_path],
f"检查设备 {device_path} 占用失败",
show_dialog=False, # Don't show dialog if fuser fails (e.g., device not busy)
suppress_critical_dialog_on_stderr_match=(
"No such file or directory", # if device doesn't exist
"not found", # if fuser itself isn't found
"Usage:" # if fuser gets invalid args, though unlikely here
)
)
pids = []
process_info_lines = []
# Parse fuser output only if it was successful and has stdout
if success_fuser and stdout_fuser:
for line in stdout_fuser.splitlines():
# Example: "/dev/sdb1: root 12078 .rce. gpg-agent"
match = re.match(r'^\S+:\s+\S+\s+(\d+)\s+.*', line)
if match:
pid = match.group(1)
pids.append(pid)
process_info_lines.append(line.strip())
if pids:
process_list_str = "\n".join(process_info_lines)
reply = QMessageBox.question(
None,
"设备占用 - 进程",
f"设备 {device_path} 正在被以下进程占用:\n{process_list_str}\n\n您要强制终止这些进程吗?这可能会导致数据丢失或系统不稳定!",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.No:
logger.info(f"用户取消了终止占用设备 {device_path} 的进程。")
return False
logger.info(f"尝试终止占用设备 {device_path} 的进程: {', '.join(pids)}")
all_killed = True
for pid in pids:
kill_success, _, kill_stderr = self._execute_shell_command(
["kill", "-9", pid],
f"终止进程 {pid} 失败",
show_dialog=True # Show dialog for kill failure
)
if not kill_success:
logger.error(f"终止进程 {pid} 失败: {kill_stderr}")
all_killed = False
if not all_killed:
QMessageBox.critical(None, "错误", f"未能终止所有占用设备 {device_path} 的进程。无法进行 {action_description}")
return False
QMessageBox.information(None, "信息", f"已尝试终止占用设备 {device_path} 的所有进程。")
else:
logger.info(f"设备 {device_path} 未被任何进程占用。")
logger.info(f"设备 {device_path} 的占用问题已尝试解决。")
return True
def mount_partition(self, device_path, mount_point, add_to_fstab=False):
"""
挂载指定设备到指定挂载点。
@@ -206,43 +306,66 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
return False
def unmount_partition(self, device_path, show_dialog_on_error=True):
"""
卸载指定设备。
:param device_path: 要卸载的设备路径。
:param show_dialog_on_error: 是否在发生错误时显示对话框。
:return: True 如果成功,否则 False。
"""
logger.info(f"尝试卸载设备 {device_path}")
# 定义表示设备已未挂载的错误信息(中英文)
# 增加了 "未指定挂载点"
already_unmounted_errors = ("not mounted", "未挂载", "未指定挂载点")
"""
卸载指定设备。
:param device_path: 要卸载的设备路径。
:param show_dialog_on_error: 是否在发生错误时显示对话框。
:return: True 如果成功,否则 False。
"""
logger.info(f"尝试卸载设备 {device_path}")
# 定义表示设备已未挂载的错误信息(中英文)
already_unmounted_errors = ("not mounted", "未挂载", "未指定挂载点")
device_busy_errors = ("device is busy", "设备或资源忙", "Device or resource busy") # Common "device busy" messages
# 调用 _execute_shell_command并告诉它在遇到“已未挂载”错误时不要弹出其自身的关键错误对话框。
success, _, stderr = self._execute_shell_command(
["umount", device_path],
f"卸载设备 {device_path} 失败",
suppress_critical_dialog_on_stderr_match=already_unmounted_errors,
show_dialog=show_dialog_on_error # <--- 将 show_dialog_on_error 传递给底层命令执行器
)
# First attempt to unmount
success, _, stderr = self._execute_shell_command(
["umount", device_path],
f"卸载设备 {device_path} 失败",
suppress_critical_dialog_on_stderr_match=already_unmounted_errors + device_busy_errors, # Suppress both types of errors for initial attempt
show_dialog=False # Don't show dialog on first attempt, we'll handle it
)
if success:
# 如果命令成功执行,则卸载成功。
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。")
return True
else:
# 如果命令失败,检查是否是因为设备已经未挂载或挂载点未指定。
is_already_unmounted_error = any(s in stderr for s in already_unmounted_errors)
if success:
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。")
return True
else:
# Check if it failed because it was already unmounted
if any(s in stderr for s in already_unmounted_errors):
logger.info(f"设备 {device_path} 已经处于未挂载状态(或挂载点未指定),无需重复卸载。")
return True # Consider it a success
if is_already_unmounted_error:
logger.info(f"设备 {device_path} 已经处于未挂载状态(或挂载点未指定),无需重复卸载。")
# 这种情况下,操作结果符合预期(设备已是未挂载),不弹出对话框,并返回 True。
return True
# Check if it failed because the device was busy
if any(s in stderr for s in device_busy_errors):
logger.warning(f"卸载设备 {device_path} 失败,设备忙。尝试解决占用问题。")
# Call the new helper to resolve occupation
if self._resolve_device_occupation(device_path, action_description=f"卸载 {device_path}"):
logger.info(f"设备 {device_path} 占用问题已解决,重试卸载。")
# Retry unmount after resolving occupation
retry_success, _, retry_stderr = self._execute_shell_command(
["umount", device_path],
f"重试卸载设备 {device_path} 失败",
suppress_critical_dialog_on_stderr_match=already_unmounted_errors,
show_dialog=show_dialog_on_error # Show dialog if retry fails for other reasons
)
if retry_success:
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。")
return True
else:
logger.error(f"重试卸载设备 {device_path} 失败: {retry_stderr}")
if show_dialog_on_error and not any(s in retry_stderr for s in already_unmounted_errors):
QMessageBox.critical(None, "错误", f"重试卸载设备 {device_path} 失败。\n错误详情: {retry_stderr}")
return False
else:
# 对于其他类型的卸载失败(例如设备忙、权限不足等),
# _execute_shell_command 应该已经弹出了关键错误对话框
# (因为它没有匹配到 `already_unmounted_errors` 进行抑制)。
# 所以,这里我们不需要再次弹出对话框,直接返回 False。
logger.info(f"用户取消或未能解决设备 {device_path} 的占用问题。")
if show_dialog_on_error:
QMessageBox.critical(None, "错误", f"未能解决设备 {device_path} 的占用问题,无法卸载。")
return False
else:
# Other types of unmount failures
logger.error(f"卸载设备 {device_path} 失败: {stderr}")
if show_dialog_on_error:
QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 失败。\n错误详情: {stderr}")
return False
def get_disk_free_space_info_mib(self, disk_path, total_disk_mib):
"""
@@ -317,6 +440,11 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
QMessageBox.critical(None, "错误", "无效的磁盘路径或分区表类型。")
return False
# NEW: 尝试解决磁盘占用问题
if not self._resolve_device_occupation(disk_path, action_description=f"{disk_path} 上创建分区"):
logger.info(f"用户取消或未能解决磁盘 {disk_path} 的占用问题,取消创建分区。")
return False
# 1. 检查磁盘是否有分区表
has_partition_table = False
# Use suppress_critical_dialog_on_stderr_match for "unrecognized disk label" when checking
@@ -419,6 +547,11 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
:param device_path: 要删除的分区路径。
:return: True 如果成功,否则 False。
"""
# NEW: 尝试解决分区占用问题
if not self._resolve_device_occupation(device_path, action_description=f"删除分区 {device_path}"):
logger.info(f"用户取消或未能解决分区 {device_path} 的占用问题,取消删除分区。")
return False
reply = QMessageBox.question(None, "确认删除分区",
f"您确定要删除分区 {device_path} 吗?此操作将擦除分区上的所有数据!",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
@@ -426,10 +559,8 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
logger.info(f"用户取消了删除分区 {device_path} 的操作。")
return False
# 尝试卸载分区
# The unmount_partition method now handles "not mounted" gracefully without dialog and returns True.
# So, we just call it.
self.unmount_partition(device_path, show_dialog_on_error=False) # show_dialog_on_error=False means no dialog for other errors too.
# 尝试卸载分区 (此方法内部会尝试解决占用问题)
self.unmount_partition(device_path, show_dialog_on_error=False)
# 从 fstab 中移除条目
# _remove_fstab_entry also handles "not found" gracefully.
@@ -471,6 +602,11 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
logger.info("用户取消了文件系统选择。")
return False
# NEW: 尝试解决设备占用问题
if not self._resolve_device_occupation(device_path, action_description=f"格式化设备 {device_path}"):
logger.info(f"用户取消或未能解决设备 {device_path} 的占用问题,取消格式化。")
return False
reply = QMessageBox.question(None, "确认格式化",
f"您确定要格式化设备 {device_path}{fstype} 吗?此操作将擦除所有数据!",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)