fix2222
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user