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

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/dist/
/build/

View File

@@ -4,8 +4,8 @@ import logging
import re import re
import os import os
from PySide6.QtWidgets import QMessageBox, QInputDialog from PySide6.QtWidgets import QMessageBox, QInputDialog
from PySide6.QtCore import QObject, Signal, QThread # <--- 导入 QObject, Signal, QThread from PySide6.QtCore import QObject, Signal, QThread
from system_info import SystemInfoManager # 确保 SystemInfoManager 已导入 from system_info import SystemInfoManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -134,7 +134,7 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
# Use sed for robust removal with sudo # Use sed for robust removal with sudo
# suppress_critical_dialog_on_stderr_match is added to handle cases where fstab might not exist # 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. # 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( success, _, stderr = self._execute_shell_command(
command, command,
f"{fstab_path} 中删除 UUID={uuid} 的条目失败", 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 True # Consider it a success if the entry is not there
return False # Other errors are already handled by _execute_shell_command 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): def mount_partition(self, device_path, mount_point, add_to_fstab=False):
""" """
挂载指定设备到指定挂载点。 挂载指定设备到指定挂载点。
@@ -214,34 +314,57 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
""" """
logger.info(f"尝试卸载设备 {device_path}") logger.info(f"尝试卸载设备 {device_path}")
# 定义表示设备已未挂载的错误信息(中英文) # 定义表示设备已未挂载的错误信息(中英文)
# 增加了 "未指定挂载点"
already_unmounted_errors = ("not mounted", "未挂载", "未指定挂载点") already_unmounted_errors = ("not mounted", "未挂载", "未指定挂载点")
device_busy_errors = ("device is busy", "设备或资源忙", "Device or resource busy") # Common "device busy" messages
# 调用 _execute_shell_command并告诉它在遇到“已未挂载”错误时不要弹出其自身的关键错误对话框。 # First attempt to unmount
success, _, stderr = self._execute_shell_command( success, _, stderr = self._execute_shell_command(
["umount", device_path], ["umount", device_path],
f"卸载设备 {device_path} 失败", f"卸载设备 {device_path} 失败",
suppress_critical_dialog_on_stderr_match=already_unmounted_errors, suppress_critical_dialog_on_stderr_match=already_unmounted_errors + device_busy_errors, # Suppress both types of errors for initial attempt
show_dialog=show_dialog_on_error # <--- 将 show_dialog_on_error 传递给底层命令执行器 show_dialog=False # Don't show dialog on first attempt, we'll handle it
) )
if success: if success:
# 如果命令成功执行,则卸载成功。
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。") QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。")
return True return True
else: else:
# 如果命令失败,检查是否是因为设备已经未挂载或挂载点未指定。 # Check if it failed because it was already unmounted
is_already_unmounted_error = any(s in stderr for s in already_unmounted_errors) if any(s in stderr for s in already_unmounted_errors):
if is_already_unmounted_error:
logger.info(f"设备 {device_path} 已经处于未挂载状态(或挂载点未指定),无需重复卸载。") logger.info(f"设备 {device_path} 已经处于未挂载状态(或挂载点未指定),无需重复卸载。")
# 这种情况下,操作结果符合预期(设备已是未挂载),不弹出对话框,并返回 True。 return True # Consider it a success
# 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 return True
else: else:
# 对于其他类型的卸载失败(例如设备忙、权限不足等), logger.error(f"重试卸载设备 {device_path} 失败: {retry_stderr}")
# _execute_shell_command 应该已经弹出了关键错误对话框 if show_dialog_on_error and not any(s in retry_stderr for s in already_unmounted_errors):
# (因为它没有匹配到 `already_unmounted_errors` 进行抑制)。 QMessageBox.critical(None, "错误", f"重试卸载设备 {device_path} 失败。\n错误详情: {retry_stderr}")
# 所以,这里我们不需要再次弹出对话框,直接返回 False return False
else:
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 return False
def get_disk_free_space_info_mib(self, disk_path, total_disk_mib): def get_disk_free_space_info_mib(self, disk_path, total_disk_mib):
@@ -317,6 +440,11 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
QMessageBox.critical(None, "错误", "无效的磁盘路径或分区表类型。") QMessageBox.critical(None, "错误", "无效的磁盘路径或分区表类型。")
return False return False
# NEW: 尝试解决磁盘占用问题
if not self._resolve_device_occupation(disk_path, action_description=f"{disk_path} 上创建分区"):
logger.info(f"用户取消或未能解决磁盘 {disk_path} 的占用问题,取消创建分区。")
return False
# 1. 检查磁盘是否有分区表 # 1. 检查磁盘是否有分区表
has_partition_table = False has_partition_table = False
# Use suppress_critical_dialog_on_stderr_match for "unrecognized disk label" when checking # Use suppress_critical_dialog_on_stderr_match for "unrecognized disk label" when checking
@@ -419,6 +547,11 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
:param device_path: 要删除的分区路径。 :param device_path: 要删除的分区路径。
:return: True 如果成功,否则 False。 :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, "确认删除分区", reply = QMessageBox.question(None, "确认删除分区",
f"您确定要删除分区 {device_path} 吗?此操作将擦除分区上的所有数据!", f"您确定要删除分区 {device_path} 吗?此操作将擦除分区上的所有数据!",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No) QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
@@ -426,10 +559,8 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
logger.info(f"用户取消了删除分区 {device_path} 的操作。") logger.info(f"用户取消了删除分区 {device_path} 的操作。")
return False return False
# 尝试卸载分区 # 尝试卸载分区 (此方法内部会尝试解决占用问题)
# The unmount_partition method now handles "not mounted" gracefully without dialog and returns True. self.unmount_partition(device_path, show_dialog_on_error=False)
# 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.
# 从 fstab 中移除条目 # 从 fstab 中移除条目
# _remove_fstab_entry also handles "not found" gracefully. # _remove_fstab_entry also handles "not found" gracefully.
@@ -471,6 +602,11 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
logger.info("用户取消了文件系统选择。") logger.info("用户取消了文件系统选择。")
return False 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, "确认格式化", reply = QMessageBox.question(None, "确认格式化",
f"您确定要格式化设备 {device_path}{fstype} 吗?此操作将擦除所有数据!", f"您确定要格式化设备 {device_path}{fstype} 吗?此操作将擦除所有数据!",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No) QMessageBox.Yes | QMessageBox.No, QMessageBox.No)

View File

@@ -6,8 +6,8 @@ from PySide6.QtWidgets import QMessageBox
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class LvmOperations: class LvmOperations:
def __init__(self): def __init__(self, disk_ops=None): # <--- Add disk_ops parameter
pass self.disk_ops = disk_ops # Store disk_ops instance
def _execute_shell_command(self, command_list, error_message, root_privilege=True, def _execute_shell_command(self, command_list, error_message, root_privilege=True,
suppress_critical_dialog_on_stderr_match=None, input_data=None, suppress_critical_dialog_on_stderr_match=None, input_data=None,
@@ -92,6 +92,15 @@ class LvmOperations:
QMessageBox.critical(None, "错误", "无效的设备路径。") QMessageBox.critical(None, "错误", "无效的设备路径。")
return False return False
# NEW: 尝试解决设备占用问题
if self.disk_ops: # Ensure disk_ops is available
if not self.disk_ops._resolve_device_occupation(device_path, action_description=f"{device_path} 上创建物理卷"):
logger.info(f"用户取消或未能解决设备 {device_path} 的占用问题,取消创建物理卷。")
return False
else:
logger.warning("DiskOperations 实例未传递给 LvmOperations无法解决设备占用问题。")
reply = QMessageBox.question(None, "确认创建物理卷", reply = QMessageBox.question(None, "确认创建物理卷",
f"您确定要在设备 {device_path} 上创建物理卷吗?此操作将覆盖设备上的数据。", f"您确定要在设备 {device_path} 上创建物理卷吗?此操作将覆盖设备上的数据。",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No) QMessageBox.Yes | QMessageBox.No, QMessageBox.No)

View File

@@ -36,11 +36,12 @@ class MainWindow(QMainWindow):
# 初始化管理器和操作类 # 初始化管理器和操作类
self.system_manager = SystemInfoManager() self.system_manager = SystemInfoManager()
self.lvm_ops = LvmOperations() # <--- 先初始化 LvmOperations # Correct order for dependency injection:
# <--- 将 lvm_ops 实例传递给 DiskOperations self.lvm_ops = LvmOperations() # Initialize LVM first
self.disk_ops = DiskOperations(self.system_manager, self.lvm_ops) self.disk_ops = DiskOperations(self.system_manager, self.lvm_ops) # DiskOperations needs system_manager and lvm_ops
self.raid_ops = RaidOperations() self.lvm_ops.disk_ops = self.disk_ops # Inject disk_ops into lvm_ops
# self.lvm_ops = LvmOperations() # 这一行是重复的,可以删除 self.raid_ops = RaidOperations(self.system_manager, self.disk_ops) # RaidOperations needs system_manager and disk_ops
# 连接刷新按钮的信号到槽函数 # 连接刷新按钮的信号到槽函数
if hasattr(self.ui, 'refreshButton'): if hasattr(self.ui, 'refreshButton'):
@@ -188,6 +189,11 @@ class MainWindow(QMainWindow):
""" """
处理擦除物理盘分区表的操作。 处理擦除物理盘分区表的操作。
""" """
# NEW: 尝试解决磁盘占用问题
if not self.disk_ops._resolve_device_occupation(device_path, action_description=f"擦除 {device_path} 上的分区表"):
logger.info(f"用户取消或未能解决磁盘 {device_path} 的占用问题,取消擦除分区表。")
return
reply = QMessageBox.question( reply = QMessageBox.question(
self, self,
"确认擦除分区表", "确认擦除分区表",
@@ -455,7 +461,7 @@ class MainWindow(QMainWindow):
array_uuid = array_data.get('uuid') array_uuid = array_data.get('uuid')
if not array_uuid or array_uuid == 'N/A': if not array_uuid or array_uuid == 'N/A':
QMessageBox.critical(self, "错误", f"无法激活 RAID 阵列 {array_path}:缺少 UUID 信息。") QMessageBox.critical(None, "错误", f"无法激活 RAID 阵列 {array_path}:缺少 UUID 信息。")
logger.error(f"激活 RAID 阵列 {array_path} 失败:缺少 UUID。") logger.error(f"激活 RAID 阵列 {array_path} 失败:缺少 UUID。")
return return
@@ -494,7 +500,7 @@ class MainWindow(QMainWindow):
def _handle_delete_active_raid_array(self, array_path, member_devices, uuid): # <--- 修改方法名并添加 uuid 参数 def _handle_delete_active_raid_array(self, array_path, member_devices, uuid): # <--- 修改方法名并添加 uuid 参数
""" """
处理删除活动 RAID 阵列的操作。 处理删除一个活动 RAID 阵列的操作。
""" """
if self.raid_ops.delete_active_raid_array(array_path, member_devices, uuid): # <--- 调用修改后的方法 if self.raid_ops.delete_active_raid_array(array_path, member_devices, uuid): # <--- 调用修改后的方法
self.refresh_all_info() self.refresh_all_info()
@@ -850,4 +856,3 @@ if __name__ == "__main__":
widget = MainWindow() widget = MainWindow()
widget.show() widget.show()
sys.exit(app.exec()) sys.exit(app.exec())

View File

@@ -7,12 +7,15 @@ from system_info import SystemInfoManager
import re import re
import pexpect # <--- 新增导入 pexpect import pexpect # <--- 新增导入 pexpect
import sys import sys
# NEW: 导入 DiskOperations
from disk_operations import DiskOperations
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RaidOperations: class RaidOperations:
def __init__(self): def __init__(self, system_manager: SystemInfoManager, disk_ops: DiskOperations): # <--- Add system_manager and disk_ops parameters
self.system_manager = SystemInfoManager() self.system_manager = system_manager
self.disk_ops = disk_ops # Store disk_ops instance
def _execute_shell_command(self, command_list, error_msg_prefix, suppress_critical_dialog_on_stderr_match=None, input_to_command=None): def _execute_shell_command(self, command_list, error_msg_prefix, suppress_critical_dialog_on_stderr_match=None, input_to_command=None):
""" """
@@ -165,7 +168,7 @@ class RaidOperations:
# 等待一小段时间,确保 mdadm 接收并处理了输入,并清空缓冲区 # 等待一小段时间,确保 mdadm 接收并处理了输入,并清空缓冲区
# 避免在下一次 expect 之前,旧的输出再次被匹配 # 避免在下一次 expect 之前,旧的输出再次被匹配
child.expect(pexpect.TIMEOUT, timeout=1) # child.expect(pexpect.TIMEOUT, timeout=1) # This line might consume output needed for next prompt
except pexpect.exceptions.TIMEOUT: except pexpect.exceptions.TIMEOUT:
# 如果在等待任何提示时超时,说明没有更多提示了,或者命令已完成。 # 如果在等待任何提示时超时,说明没有更多提示了,或者命令已完成。
@@ -179,7 +182,6 @@ class RaidOperations:
break # 跳出 while 循环,进入等待 EOF 阶段 break # 跳出 while 循环,进入等待 EOF 阶段
try: try:
# 增加一个合理的超时时间,以防 mdadm 在后台进行长时间初始化 # 增加一个合理的超时时间,以防 mdadm 在后台进行长时间初始化
child.expect(pexpect.EOF, timeout=120) # 例如,等待 120 秒 child.expect(pexpect.EOF, timeout=120) # 例如,等待 120 秒
@@ -277,6 +279,11 @@ class RaidOperations:
QMessageBox.critical(None, "错误", "RAID10 至少需要两个设备。") QMessageBox.critical(None, "错误", "RAID10 至少需要两个设备。")
return False return False
# NEW: 尝试解决所有成员设备的占用问题
for dev in devices:
if not self.disk_ops._resolve_device_occupation(dev, action_description=f"创建 RAID 阵列,成员 {dev}"):
logger.info(f"用户取消或未能解决设备 {dev} 的占用问题,取消创建 RAID 阵列。")
return False
# 确认操作 # 确认操作
reply = QMessageBox.question(None, "确认创建 RAID 阵列", reply = QMessageBox.question(None, "确认创建 RAID 阵列",
@@ -358,6 +365,7 @@ class RaidOperations:
# 这里需要确保 /etc/mdadm.conf 存在且可写入 # 这里需要确保 /etc/mdadm.conf 存在且可写入
# _execute_shell_command 会自动添加 sudo # _execute_shell_command 会自动添加 sudo
# 使用 tee -a 而不是 >> 来确保 sudo 权限下的写入
success_append, _, _ = self._execute_shell_command( success_append, _, _ = self._execute_shell_command(
["bash", "-c", f"echo '{scan_stdout.strip()}' | tee -a /etc/mdadm.conf > /dev/null"], ["bash", "-c", f"echo '{scan_stdout.strip()}' | tee -a /etc/mdadm.conf > /dev/null"],
"更新 /etc/mdadm/mdadm.conf 失败" "更新 /etc/mdadm/mdadm.conf 失败"
@@ -387,13 +395,11 @@ class RaidOperations:
logger.info(f"尝试停止 RAID 阵列: {array_path}") logger.info(f"尝试停止 RAID 阵列: {array_path}")
# 尝试卸载阵列(如果已挂载),不显示错误对话框 # NEW: 尝试卸载阵列(如果已挂载),利用 DiskOperations 的增强卸载功能
# _execute_shell_command 会自动添加 sudo # unmount_partition 内部会尝试解决设备占用问题
self._execute_shell_command( if not self.disk_ops.unmount_partition(array_path, show_dialog_on_error=False):
["umount", array_path], logger.warning(f"未能成功卸载 RAID 阵列 {array_path}。尝试继续停止操作。")
f"尝试卸载 {array_path} 失败", # 即使卸载失败,我们仍然尝试停止 mdadm 阵列,因为有时 mdadm --stop 可以在设备忙时强制停止。
suppress_critical_dialog_on_stderr_match=("not mounted", "未挂载") # 修改这里,添加中文错误信息
)
if not self._execute_shell_command(["mdadm", "--stop", array_path], f"停止 RAID 阵列 {array_path} 失败")[0]: if not self._execute_shell_command(["mdadm", "--stop", array_path], f"停止 RAID 阵列 {array_path} 失败")[0]:
return False return False
@@ -421,6 +427,7 @@ class RaidOperations:
logger.info(f"尝试删除活动 RAID 阵列: {array_path} (UUID: {uuid})") logger.info(f"尝试删除活动 RAID 阵列: {array_path} (UUID: {uuid})")
# 1. 停止阵列 # 1. 停止阵列
# stop_raid_array 内部会调用 self.disk_ops.unmount_partition 来尝试卸载并解决占用
if not self.stop_raid_array(array_path): if not self.stop_raid_array(array_path):
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 失败,因为无法停止阵列。") QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 失败,因为无法停止阵列。")
return False return False