From c7e0dc10365721f6261f226051f95434a5697d4b Mon Sep 17 00:00:00 2001 From: zj <1052308357@qq.comm> Date: Thu, 5 Feb 2026 01:41:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=A7=A3=E9=99=A4=E5=8D=A0?= =?UTF-8?q?=E7=94=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/disk_operations.cpython-314.pyc | Bin 33644 -> 25750 bytes __pycache__/lvm_operations.cpython-314.pyc | Bin 21023 -> 20878 bytes .../occupation_resolver.cpython-314.pyc | Bin 0 -> 16484 bytes __pycache__/raid_operations.cpython-314.pyc | Bin 28453 -> 27765 bytes disk_operations.py | 282 +++------------- lvm_operations.py | 51 +-- mainwindow.py | 171 ++++++---- occupation_resolver.py | 307 ++++++++++++++++++ pyproject.toml | 2 +- raid_operations.py | 122 ++----- 10 files changed, 512 insertions(+), 423 deletions(-) create mode 100644 __pycache__/occupation_resolver.cpython-314.pyc create mode 100644 occupation_resolver.py diff --git a/__pycache__/disk_operations.cpython-314.pyc b/__pycache__/disk_operations.cpython-314.pyc index 493291e6336e4edf2d7c01669edf55b0cc499b21..635c76b2b9a8bcc16773ace6b25fd1af7ca6102c 100644 GIT binary patch delta 3801 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 diff --git a/__pycache__/occupation_resolver.cpython-314.pyc b/__pycache__/occupation_resolver.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68fa2dae034a24515d021d746e31ba17e758c991 GIT binary patch literal 16484 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 Ka}hCA^8W+Yj8m8Z delta 3175 zcmZ`*3v83u6~5Q@?^pbcoftb#?8HeN=LLB+p?MJAq0r#|z)vB8;3ZCC2#)tV<|Q#G zFcQjmWQRet(zQG~=(KjwtSy6v(P>gOO_Q89D5=+0bj2ZPon{CqbgHV|`#UBPQh&+c ze|+w_-#Pc5^WS?OkKwnz!kPk&T8YThRDRHx>+jdh!N2KuZqRK;EVDx0py|=r&?e;0 zfS+zL=5ErJV`_^~k3I!uB35cbtgKu#txIOH1G(h=H_M$a9MP}Y9qjRLU*T`>e$nIi zba;89)GYA@x2+C%c~6h8+aClOc495e#XFS}LLw%=$y~UCZRMlpY78TF=!?5WIvihA|~?zg<)q^jJ`i^6#h<< zanwrPB_8U6{FhH=5Me)Buk$BH(0aRndesQAhy;WiiAyyZ=QRXe;)o>Z?P{OaP*spJ z=jP_}diY$Fn<0cm?f0XxjKUFl+SnZXaW!uwn#NWJ`sW%43&nSnupac09+che~QkGHb7@-j-a( z7c#G^!m5nn%v$2fV2X*d^%f$fXMr<6D}_h}mACV2uX%3g*N2VHM`R=qHs+tfP4FMEzJ)JZmoK5ZmNRDX^u=2)C-#$*G)(*qTb2r-uEnq`>bjNx*Oh_<>c!KP#1j zd?i)ENI7Y<6uw_)g;iBaSn#HwllYQs0U2u#$T{S;B?6y$b22tlSX)-9n3J;(u+LAAMSZDw5K$Q3ls zQt^zxm0Rf^CHUN#(BEi~#ToO+Zke$I>WmgiKo#G}G#96$TRuZT!p!GXYyl?%oz-Yz zF?%KHLT4rLLxd#5gs_k9>WaeDlNzVW`(<7a<2ad2qj(uL^Z!HIWo zMlbf-L&e0*e(%?ZqQ~AO*7wi6d;k0Z;f~LV_PrLpaox@?T(QJ{f8hOS-={dc@MgdxPCwJG>;69llm?OLuE)C?MQpJPn9<5CGk> z6qviruZ)O^72ihbE-f=+a<+f8yh=^)onVr1IzQ3@Zs=sDq)2Ciu7*r{rmOUDvfc zGTdUqOM_u+&xkG*FM?CC?9q!?C;s>r^t)}UW8^=7oWNRF5`6BqIfUZShy2^SL;jwYfQRq#(IVy7 zQ^Q&cXW<8THmLx|OXbpiq?HJ!!KRlER>+agcw+C7y`$RVVQul{Jz@8{u(o(q%YCZl zzECBfXgtywPOrJP@!G0zLwh)_V?@;%lY*M<5L$#*3Cv424jyN(O7Z(pzhR%{0QGf{ z0=XOz|8ok5352Xru+a=x7D`k3UaIV)aD>9E6pn&s<9AZJB>otvo7(Yp=xM4^pCt;H znkR=a;{OCgP35|CR6I}N0)>l!n{&vroXyMdYv6D0$MW|!Etlfs(9mkbe}>N1wVAi5 zA!XvrD3nmhC*YFAiT?s$wwegXsZTyFHv=`bt3ld?U>D-ke1T38nm{>%V9-jB?ZV{$jgd(4rI9l$jkv^a~n1Uld zFB6j^hZ7$7?5TGu_Qn**Q8$Sg2PuDBpKKs?N(0WVO=?=`q~oKB=GD-*bxq29RJ=~% zuM|F^@OOx9ofmIEX}eg@)6R^iy|n;ZA_Nx^c+!hVXDTAisL+u&`x*DiO!p+pJGJO;q6&P=93TSAJjC90uA|X6n7$Ym-3qJ-EE;R uZyg^YLv(S$1yIuE$d01?I||*X!t$xYGEnuIB747dO0rjm&69|tK=FSP5+E4> diff --git a/disk_operations.py b/disk_operations.py index 59c2ebe..de18dd3 100644 --- a/disk_operations.py +++ b/disk_operations.py @@ -9,7 +9,7 @@ from system_info import SystemInfoManager logger = logging.getLogger(__name__) -# --- 新增: 后台格式化工作线程 --- +# --- 后台格式化工作线程 --- class FormatWorker(QObject): # 定义信号,用于向主线程发送格式化结果 finished = Signal(bool, str, str, str) # 成功状态, 设备路径, 标准输出, 标准错误 @@ -19,7 +19,6 @@ class FormatWorker(QObject): super().__init__(parent) self.device_path = device_path self.fs_type = fs_type - # 接收一个可调用的函数,用于执行shell命令 (这里是 DiskOperations._execute_shell_command) self._execute_shell_command_func = execute_shell_command_func def run(self): @@ -48,18 +47,18 @@ class FormatWorker(QObject): command_list, f"格式化设备 {self.device_path} 为 {self.fs_type} 失败", root_privilege=True, - show_dialog=False # <--- 关键:阻止工作线程弹出 QMessageBox + show_dialog=False # 阻止工作线程弹出 QMessageBox ) self.finished.emit(success, self.device_path, stdout, stderr) logger.info(f"后台格式化完成: 设备 {self.device_path}, 成功: {success}") -class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 +class DiskOperations(QObject): # 继承 QObject 以便发出信号 # 定义信号,用于通知主线程格式化操作的开始和结束 formatting_finished = Signal(bool, str, str, str) # 成功状态, 设备路径, 标准输出, 标准错误 formatting_started = Signal(str) # 设备路径 - def __init__(self, system_manager: SystemInfoManager, lvm_ops, parent=None): # <--- 构造函数现在接收 lvm_ops 实例 + def __init__(self, system_manager: SystemInfoManager, lvm_ops, parent=None): super().__init__(parent) self.system_manager = system_manager self._lvm_ops = lvm_ops # 保存 LvmOperations 实例,以便调用其 _execute_shell_command @@ -89,8 +88,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 try: # 检查 fstab 中是否已存在相同 UUID 的条目 - # 使用 _execute_shell_command 来读取 fstab,尽管通常不需要 sudo - # 这里为了简化,直接读取文件,但写入时使用 _execute_shell_command with open(fstab_path, 'r') as f: fstab_content = f.readlines() @@ -101,7 +98,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 return True # 认为成功,因为目标已达成 # 如果不存在,则追加到 fstab - # 使用 _execute_shell_command for sudo write success, _, stderr = self._execute_shell_command( ["sh", "-c", f"echo '{fstab_entry}' >> {fstab_path}"], f"将 {device_path} 添加到 {fstab_path} 失败", @@ -123,7 +119,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 从 /etc/fstab 中移除指定设备的条目。 此方法需要 SystemInfoManager 来获取设备的 UUID。 """ - # NEW: 使用 system_manager 获取 UUID,它会处理路径解析 device_details = self.system_manager.get_device_details_by_path(device_path) if not device_details or not device_details.get('uuid'): logger.warning(f"无法获取设备 {device_path} 的 UUID,无法从 fstab 中移除。") @@ -131,9 +126,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 uuid = device_details.get('uuid') fstab_path = "/etc/fstab" - # 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"/{re.escape(uuid)}/d", fstab_path] # Use re.escape for UUID success, _, stderr = self._execute_shell_command( command, @@ -149,10 +141,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 logger.info(f"已从 {fstab_path} 中删除 UUID={uuid} 的条目。") return True else: - # If sed failed, it might be because the entry wasn't found, which is fine. - # _execute_shell_command would have suppressed the dialog if it matched the suppress_critical_dialog_on_stderr_match. - # So, if we reach here, it's either a real error (dialog shown by _execute_shell_command) - # or a suppressed "no changes" type of error. In both cases, if no real error, we return True. if any(s in stderr for s in ( f"sed: {fstab_path}: No such file or directory", f"sed: {fstab_path}: 没有那个文件或目录", @@ -160,109 +148,9 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 "No such file or directory" # more general check )): logger.info(f"fstab 中未找到 UUID={uuid} 的条目,无需删除。") - 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 True 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): """ 挂载指定设备到指定挂载点。 @@ -288,7 +176,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 if success: QMessageBox.information(None, "成功", f"设备 {device_path} 已成功挂载到 {mount_point}。") if add_to_fstab: - # NEW: 使用 self.system_manager 获取 fstype 和 UUID device_details = self.system_manager.get_device_details_by_path(device_path) if device_details: fstype = device_details.get('fstype') @@ -306,66 +193,32 @@ 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", "未挂载", "未指定挂载点") - device_busy_errors = ("device is busy", "设备或资源忙", "Device or resource busy") # Common "device busy" messages + """ + 卸载指定设备。 + :param device_path: 要卸载的设备路径。 + :param show_dialog_on_error: 是否在发生错误时显示对话框。 + :return: True 如果成功,否则 False。 + """ + logger.info(f"尝试卸载设备 {device_path}。") + already_unmounted_errors = ("not mounted", "未挂载", "未指定挂载点") - # 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 - ) + 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 + ) - 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 - - # 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: - logger.info(f"用户取消或未能解决设备 {device_path} 的占用问题。") - if show_dialog_on_error: - QMessageBox.critical(None, "错误", f"未能解决设备 {device_path} 的占用问题,无法卸载。") - return False + if success: + QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。") + return True 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 + is_already_unmounted_error = any(s in stderr for s in already_unmounted_errors) + if is_already_unmounted_error: + logger.info(f"设备 {device_path} 已经处于未挂载状态(或挂载点未指定),无需重复卸载。") + return True + else: + return False def get_disk_free_space_info_mib(self, disk_path, total_disk_mib): """ @@ -376,8 +229,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 如果磁盘有分区表但没有空闲空间,返回 (None, None)。 """ logger.debug(f"尝试获取磁盘 {disk_path} 的空闲空间信息 (MiB)。") - # suppress_critical_dialog_on_stderr_match is added to handle cases where parted might complain about - # an unrecognized disk label, which is expected for a fresh disk. success, stdout, stderr = self._execute_shell_command( ["parted", "-s", disk_path, "unit", "MiB", "print", "free"], f"获取磁盘 {disk_path} 分区信息失败", @@ -386,9 +237,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 ) if not success: - # If parted failed and it wasn't due to an unrecognized label (handled by suppress_critical_dialog_on_stderr_match), - # then _execute_shell_command would have shown a dialog. - # We just log and return None. logger.error(f"获取磁盘 {disk_path} 空闲空间信息失败: {stderr}") return None, None @@ -396,18 +244,14 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 free_spaces = [] lines = stdout.splitlines() - # Regex to capture StartMiB and SizeMiB from a "Free Space" line - # It's made more flexible to match "Free Space", "空闲空间", and "可用空间" - # Example: " 0.02MiB 8192MiB 8192MiB 可用空间" - # We need to capture the first numeric value (Start) and the third numeric value (Size). free_space_line_pattern = re.compile(r'^\s*(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(?:Free Space|空闲空间|可用空间)') for line in lines: match = free_space_line_pattern.match(line) if match: try: - start_mib = float(match.group(1)) # Capture StartMiB - size_mib = float(match.group(3)) # Capture SizeMiB (the third numeric value) + start_mib = float(match.group(1)) + size_mib = float(match.group(3)) free_spaces.append({'start_mib': start_mib, 'size_mib': size_mib}) except ValueError as ve: logger.warning(f"解析 parted free space 行 '{line}' 失败: {ve}") @@ -417,7 +261,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 logger.warning(f"在磁盘 {disk_path} 上未找到空闲空间。") return None, None - # 找到最大的空闲空间块 largest_free_space = max(free_spaces, key=lambda x: x['size_mib']) start_mib = largest_free_space['start_mib'] size_mib = largest_free_space['size_mib'] @@ -440,39 +283,28 @@ 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 success_check, stdout_check, stderr_check = self._execute_shell_command( ["parted", "-s", disk_path, "print"], f"检查磁盘 {disk_path} 分区表失败", suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label"), root_privilege=True ) - # If success_check is False, it means _execute_shell_command encountered an error. - # If that error was "unrecognized disk label", it was suppressed, and we treat it as no partition table. - # If it was another error, a dialog was shown by _execute_shell_command, and we should stop. if not success_check: - # Check if the failure was due to "unrecognized disk label" (which means no partition table) if any(s in stderr_check for s in ("无法辨识的磁盘卷标", "unrecognized disk label")): logger.info(f"磁盘 {disk_path} 没有可识别的分区表。") - has_partition_table = False # Explicitly set to False + has_partition_table = False else: logger.error(f"检查磁盘 {disk_path} 分区表时发生非预期的错误: {stderr_check}") - return False # Other critical error, stop operation. - else: # success_check is True + return False + else: if "Partition Table: unknown" not in stdout_check and "分区表:unknown" not in stdout_check: has_partition_table = True else: logger.info(f"parted print 报告磁盘 {disk_path} 的分区表为 'unknown'。") has_partition_table = False - actual_start_mib_for_parted = 0.0 # 2. 如果没有分区表,则创建分区表 @@ -492,7 +324,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 ) if not success: return False - # 对于新创建分区表的磁盘,第一个分区通常从 1MiB 开始以确保兼容性和对齐 actual_start_mib_for_parted = 1.0 else: # 如果有分区表,获取下一个可用分区的起始位置 @@ -501,10 +332,7 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 QMessageBox.critical(None, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置或没有空闲空间。") return False - # 如果 parted 报告的起始位置非常接近 0 MiB (例如 0.0 MiB 或 0.02 MiB), - # 为了安全和兼容性,也将其调整为 1.0 MiB。 - # 否则,使用 parted 报告的精确起始位置。 - if start_mib_from_parted < 1.0: # covers 0.0MiB and values like 0.017MiB (17.4kB) + if start_mib_from_parted < 1.0: actual_start_mib_for_parted = 1.0 else: actual_start_mib_for_parted = start_mib_from_parted @@ -514,12 +342,7 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 end_pos = "100%" size_for_log = "最大可用空间" else: - # 计算结束 MiB - # 注意:这里计算的 end_mib 是基于用户请求的大小, - # parted 会根据实际可用空间和对齐进行微调。 end_mib = actual_start_mib_for_parted + size_gb * 1024 - - # 简单检查,如果请求的大小导致结束位置超出磁盘总大小,则使用最大可用 if end_mib > total_disk_mib: QMessageBox.warning(None, "警告", f"请求的分区大小 ({size_gb}GB) 超出了可用空间。将调整为最大可用空间。") end_pos = "100%" @@ -547,11 +370,6 @@ 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) @@ -559,15 +377,13 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 logger.info(f"用户取消了删除分区 {device_path} 的操作。") return False - # 尝试卸载分区 (此方法内部会尝试解决占用问题) + # 尝试卸载分区 self.unmount_partition(device_path, show_dialog_on_error=False) # 从 fstab 中移除条目 - # _remove_fstab_entry also handles "not found" gracefully. self._remove_fstab_entry(device_path) # 获取父磁盘和分区号 - # 例如 /dev/sdb1 -> /dev/sdb, 1 match = re.match(r'(/dev/[a-z]+)(\d+)', device_path) if not match: QMessageBox.critical(None, "错误", f"无法解析设备路径 {device_path}。") @@ -602,11 +418,6 @@ 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) @@ -626,20 +437,19 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 # 创建 QThread 和 FormatWorker 实例 thread = QThread() - # 将 self._execute_shell_command (它内部调用 LvmOperations._execute_shell_command) 传递给工作线程 worker = FormatWorker(device_path, fstype, self._execute_shell_command) worker.moveToThread(thread) # 连接信号和槽 - thread.started.connect(worker.run) # 线程启动时执行 worker 的 run 方法 - worker.finished.connect(lambda s, dp, o, e: self._on_formatting_finished(s, dp, o, e)) # worker 完成时调用处理函数 - worker.finished.connect(thread.quit) # worker 完成时退出线程 - worker.finished.connect(worker.deleteLater) # worker 完成时自动删除 worker 对象 - thread.finished.connect(thread.deleteLater) # 线程退出时自动删除线程对象 + thread.started.connect(worker.run) + worker.finished.connect(lambda s, dp, o, e: self._on_formatting_finished(s, dp, o, e)) + worker.finished.connect(thread.quit) + worker.finished.connect(worker.deleteLater) + thread.finished.connect(thread.deleteLater) - self.active_format_workers[device_path] = thread # 存储线程以便管理 - self.formatting_started.emit(device_path) # 发出信号通知主界面格式化已开始 - thread.start() # 启动线程 + self.active_format_workers[device_path] = thread + self.formatting_started.emit(device_path) + thread.start() QMessageBox.information(None, "开始格式化", f"设备 {device_path} 正在后台格式化为 {fstype}。完成后将通知您。") return True @@ -649,13 +459,11 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号 处理格式化工作线程完成后的结果。此槽函数在主线程中执行。 """ if device_path in self.active_format_workers: - del self.active_format_workers[device_path] # 从跟踪列表中移除 + del self.active_format_workers[device_path] - # 在主线程中显示最终结果的消息框 if success: QMessageBox.information(None, "格式化成功", f"设备 {device_path} 已成功格式化。") else: QMessageBox.critical(None, "格式化失败", f"格式化设备 {device_path} 失败。\n错误详情: {stderr}") - self.formatting_finished.emit(success, device_path, stdout, stderr) # 发出信号通知 MainWindow 刷新界面 - + self.formatting_finished.emit(success, device_path, stdout, stderr) diff --git a/lvm_operations.py b/lvm_operations.py index b1c6b86..abd27ed 100644 --- a/lvm_operations.py +++ b/lvm_operations.py @@ -6,12 +6,12 @@ from PySide6.QtWidgets import QMessageBox logger = logging.getLogger(__name__) class LvmOperations: - def __init__(self, disk_ops=None): # <--- Add disk_ops parameter - self.disk_ops = disk_ops # Store disk_ops instance + def __init__(self): + pass def _execute_shell_command(self, command_list, error_message, root_privilege=True, - suppress_critical_dialog_on_stderr_match=None, input_data=None, - show_dialog=True): # <--- 新增 show_dialog 参数,默认为 True + suppress_critical_dialog_on_stderr_match=None, input_data=None, + show_dialog=True, expected_non_zero_exit_codes=None): """ 通用地运行一个 shell 命令,并处理错误。 :param command_list: 命令及其参数的列表。 @@ -21,11 +21,12 @@ class LvmOperations: 可以是字符串或字符串元组/列表。 :param input_data: 传递给命令stdin的数据 (str)。 :param show_dialog: 如果为 False,则不显示关键错误对话框。 + :param expected_non_zero_exit_codes: 预期为非零但仍视为成功的退出码列表。 :return: (True/False, stdout_str, stderr_str) """ if not all(isinstance(arg, str) for arg in command_list): logger.error(f"命令列表包含非字符串元素: {command_list}") - if show_dialog: # <--- 根据 show_dialog 决定是否弹出对话框 + if show_dialog: QMessageBox.critical(None, "错误", f"内部错误:尝试执行的命令包含无效参数。\n命令详情: {command_list}") return False, "", "内部错误:命令参数类型不正确。" @@ -53,6 +54,12 @@ class LvmOperations: logger.error(f"标准输出: {e.stdout.strip()}") logger.error(f"标准错误: {stderr_output}") + # NEW LOGIC: Check if the exit code is expected as non-critical + if expected_non_zero_exit_codes and e.returncode in expected_non_zero_exit_codes: + logger.info(f"命令 '{full_cmd_str}' 以预期非零退出码 {e.returncode} 结束,仍视为成功。") + return True, e.stdout.strip(), stderr_output # Treat as success + + should_suppress_dialog = False if suppress_critical_dialog_on_stderr_match: if isinstance(suppress_critical_dialog_on_stderr_match, str): @@ -64,23 +71,21 @@ class LvmOperations: should_suppress_dialog = True break - if show_dialog and not should_suppress_dialog: # <--- 根据 show_dialog 决定是否弹出对话框 + if show_dialog and not should_suppress_dialog: QMessageBox.critical(None, "错误", f"{error_message}\n错误详情: {stderr_output}") return False, e.stdout.strip(), stderr_output except FileNotFoundError: - if show_dialog: # <--- 根据 show_dialog 决定是否弹出对话框 + if show_dialog: QMessageBox.critical(None, "错误", f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。") logger.error(f"命令 '{command_list[0]}' 未找到。") return False, "", f"命令 '{command_list[0]}' 未找到。" except Exception as e: - if show_dialog: # <--- 根据 show_dialog 决定是否弹出对话框 + if show_dialog: QMessageBox.critical(None, "错误", f"执行命令时发生未知错误: {e}") logger.error(f"执行命令 {full_cmd_str} 时发生未知错误: {e}") return False, "", str(e) - # 以下是 LvmOperations 中其他方法的代码,保持不变。 - # ... (create_pv, delete_pv, create_vg, delete_vg, create_lv, delete_lv, activate_lv, deactivate_lv) ... def create_pv(self, device_path): """ 创建物理卷 (PV)。 @@ -92,15 +97,6 @@ class LvmOperations: QMessageBox.critical(None, "错误", "无效的设备路径。") 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, "确认创建物理卷", f"您确定要在设备 {device_path} 上创建物理卷吗?此操作将覆盖设备上的数据。", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) @@ -109,20 +105,17 @@ class LvmOperations: return False logger.info(f"尝试在 {device_path} 上创建物理卷。") - # 第一次尝试创建物理卷,抑制 "device is partitioned" 错误,因为我们将在代码中处理它 success, _, stderr = self._execute_shell_command( - ["pvcreate", "-y", device_path], # -y 自动确认 + ["pvcreate", "-y", device_path], f"在 {device_path} 上创建物理卷失败", - suppress_critical_dialog_on_stderr_match="device is partitioned" # 抑制此特定错误 + suppress_critical_dialog_on_stderr_match="device is partitioned" ) if success: QMessageBox.information(None, "成功", f"物理卷已在 {device_path} 上成功创建。") return True else: - # 如果 pvcreate 失败,检查是否是 "device is partitioned" 错误 if "device is partitioned" in stderr: - # 提示用户是否要擦除分区表 wipe_reply = QMessageBox.question( None, "设备已分区", f"设备 {device_path} 已分区。您是否要擦除设备上的所有分区表," @@ -131,35 +124,28 @@ class LvmOperations: ) if wipe_reply == QMessageBox.Yes: logger.warning(f"用户选择擦除 {device_path} 上的分区表并创建物理卷。") - # 尝试擦除分区表 (使用 GPT 作为默认,通常更现代且支持大容量磁盘) - # 注意:这里需要 parted 命令,确保系统已安装 parted mklabel_success, _, mklabel_stderr = self._execute_shell_command( - ["parted", "-s", device_path, "mklabel", "gpt"], # 使用 gpt 分区表 + ["parted", "-s", device_path, "mklabel", "gpt"], f"擦除 {device_path} 上的分区表失败" ) if mklabel_success: logger.info(f"已成功擦除 {device_path} 上的分区表。重试创建物理卷。") - # 再次尝试创建物理卷 retry_success, _, retry_stderr = self._execute_shell_command( ["pvcreate", "-y", device_path], f"在 {device_path} 上创建物理卷失败 (重试)" - # 重试时不再抑制,如果再次失败,_execute_shell_command 会显示错误 ) if retry_success: QMessageBox.information(None, "成功", f"物理卷已在 {device_path} 上成功创建。") return True else: - # 如果重试失败,_execute_shell_command 已经显示错误 return False else: - # mklabel 失败,_execute_shell_command 已经显示错误 return False else: logger.info(f"用户取消了擦除 {device_path} 分区表的操作。") QMessageBox.information(None, "信息", f"未在已分区设备 {device_path} 上创建物理卷。") return False else: - # 对于其他类型的 pvcreate 失败,_execute_shell_command 已经显示了错误 return False def delete_pv(self, device_path): @@ -374,4 +360,3 @@ class LvmOperations: return True else: return False - diff --git a/mainwindow.py b/mainwindow.py index 8224d65..62f8343 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -6,7 +6,7 @@ import os from PySide6.QtWidgets import (QApplication, QMainWindow, QTreeWidgetItem, QMessageBox, QHeaderView, QMenu, QInputDialog, QDialog) -from PySide6.QtCore import Qt, QPoint, QThread # <--- 确保导入 QThread +from PySide6.QtCore import Qt, QPoint, QThread # 导入自动生成的 UI 文件 from ui_form import Ui_MainWindow @@ -20,6 +20,8 @@ from disk_operations import DiskOperations from raid_operations import RaidOperations # 导入 LVM 操作模块 from lvm_operations import LvmOperations +# 导入新的 resolver 类 +from occupation_resolver import OccupationResolver # 导入自定义对话框 from dialogs import (CreatePartitionDialog, MountDialog, CreateRaidDialog, CreatePvDialog, CreateVgDialog, CreateLvDialog) @@ -34,13 +36,13 @@ class MainWindow(QMainWindow): setup_logging(self.ui.logOutputTextEdit) logger.info("应用程序启动。") - # 初始化管理器和操作类 + # 初始化管理器和操作类 (恢复原始的初始化顺序和依赖关系) self.system_manager = SystemInfoManager() - # Correct order for dependency injection: - self.lvm_ops = LvmOperations() # Initialize LVM first - self.disk_ops = DiskOperations(self.system_manager, self.lvm_ops) # DiskOperations needs system_manager and lvm_ops - self.lvm_ops.disk_ops = self.disk_ops # Inject disk_ops into lvm_ops - self.raid_ops = RaidOperations(self.system_manager, self.disk_ops) # RaidOperations needs system_manager and disk_ops + self.lvm_ops = LvmOperations() + # DiskOperations 仍然需要 lvm_ops 的 _execute_shell_command 方法 + self.disk_ops = DiskOperations(self.system_manager, self.lvm_ops) + # RaidOperations 仍然需要 system_manager + self.raid_ops = RaidOperations(self.system_manager) # 连接刷新按钮的信号到槽函数 @@ -59,10 +61,16 @@ class MainWindow(QMainWindow): self.ui.treeWidget_lvm.setContextMenuPolicy(Qt.CustomContextMenu) self.ui.treeWidget_lvm.customContextMenuRequested.connect(self.show_lvm_context_menu) - # <--- 新增: 连接 DiskOperations 的格式化完成信号 + # 连接 DiskOperations 的格式化完成信号 self.disk_ops.formatting_finished.connect(self.on_disk_formatting_finished) - # 可选:连接格式化开始信号,用于显示进度或禁用相关操作 - # self.disk_ops.formatting_started.connect(self.on_disk_formatting_started) + + # 实例化 OccupationResolver + # 将 lvm_ops._execute_shell_command 和 system_manager.get_mountpoint_for_device 传递给它 + # 确保 lvm_ops 和 system_manager 已经被正确初始化 + self.occupation_resolver = OccupationResolver( + shell_executor_func=self.lvm_ops._execute_shell_command, + mount_info_getter_func=self.system_manager.get_mountpoint_for_device + ) # 初始化时刷新所有数据 self.refresh_all_info() @@ -158,10 +166,8 @@ class MainWindow(QMainWindow): if device_type == 'disk': create_partition_action = menu.addAction(f"创建分区 {device_path}...") create_partition_action.triggered.connect(lambda: self._handle_create_partition(device_path, dev_data)) - # --- 新增功能:擦除分区表 --- wipe_action = menu.addAction(f"擦除分区表 {device_path}...") wipe_action.triggered.connect(lambda: self._handle_wipe_partition_table(device_path)) - # --- 新增功能结束 --- menu.addSeparator() if device_type == 'part': @@ -178,22 +184,70 @@ class MainWindow(QMainWindow): format_action = menu.addAction(f"格式化分区 {device_path}...") format_action.triggered.connect(lambda: self._handle_format_partition(device_path)) + menu.addSeparator() # Add separator before new occupation options + + # --- 新增:解除设备占用选项 --- + # This option is available for any disk or partition that might be occupied + if device_type in ['disk', 'part']: + resolve_occupation_action = menu.addAction(f"解除占用 {device_path}") + resolve_occupation_action.triggered.connect(lambda: self._handle_resolve_device_occupation(device_path)) + + # --- 新增:关闭交换分区选项 --- + # This option is only available for active swap partitions + if device_type == 'part' and mount_point == '[SWAP]': + deactivate_swap_action = menu.addAction(f"关闭交换分区 {device_path}") + deactivate_swap_action.triggered.connect(lambda: self._handle_deactivate_swap(device_path)) if menu.actions(): menu.exec(self.ui.treeWidget_block_devices.mapToGlobal(pos)) else: logger.info("右键点击了空白区域或设备没有可用的操作。") - # --- 新增方法:处理擦除分区表 --- + # --- 新增:处理解除设备占用 --- + def _handle_resolve_device_occupation(self, device_path: str) -> bool: + """ + 处理设备占用问题,通过调用 OccupationResolver 进行全面检查和修复。 + """ + success = self.occupation_resolver.resolve_occupation(device_path) + if success: + # 如果成功解除占用,刷新 UI 显示最新的设备状态 + self.refresh_all_info() + return success + + # --- 新增:处理关闭交换分区 --- + def _handle_deactivate_swap(self, device_path): + """ + 处理关闭交换分区的操作。 + """ + reply = QMessageBox.question( + None, + "确认关闭交换分区", + f"您确定要关闭交换分区 {device_path} 吗?", + 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.lvm_ops._execute_shell_command( + ["swapoff", device_path], + f"关闭交换分区 {device_path} 失败", + show_dialog=True + ) + if success: + QMessageBox.information(None, "成功", f"交换分区 {device_path} 已成功关闭。") + self.refresh_all_info() + return True + else: + logger.error(f"关闭交换分区 {device_path} 失败: {stderr}") + return False + def _handle_wipe_partition_table(self, device_path): """ 处理擦除物理盘分区表的操作。 """ - # 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( self, "确认擦除分区表", @@ -208,10 +262,8 @@ class MainWindow(QMainWindow): return logger.warning(f"尝试擦除 {device_path} 上的分区表。") - # 使用 LvmOperations 中通用的 _execute_shell_command 来执行 parted 命令 - # 这里不需要 show_dialog=False,因为这个操作本身就是同步且需要用户确认的 success, _, stderr = self.lvm_ops._execute_shell_command( - ["parted", "-s", device_path, "mklabel", "gpt"], # 默认使用 gpt 分区表类型 + ["parted", "-s", device_path, "mklabel", "gpt"], f"擦除 {device_path} 上的分区表失败" ) @@ -219,9 +271,7 @@ class MainWindow(QMainWindow): QMessageBox.information(self, "成功", f"设备 {device_path} 上的分区表已成功擦除。") self.refresh_all_info() else: - # 错误信息已由 _execute_shell_command 处理并显示 pass - # --- 新增方法结束 --- def _handle_create_partition(self, disk_path, dev_data): total_disk_mib = 0.0 @@ -260,7 +310,6 @@ class MainWindow(QMainWindow): if dev_data.get('children'): logger.debug(f"磁盘 {disk_path} 存在现有分区,尝试计算下一个分区起始位置。") - # 假设 disk_ops.get_disk_free_space_info_mib 能够正确处理 calculated_start_mib, largest_free_space_mib = self.disk_ops.get_disk_free_space_info_mib(disk_path, total_disk_mib) if calculated_start_mib is None or largest_free_space_mib is None: QMessageBox.critical(self, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置。") @@ -271,8 +320,8 @@ class MainWindow(QMainWindow): max_available_mib = 0.0 else: logger.debug(f"磁盘 {disk_path} 没有现有分区,假定从 0 MiB 开始,最大可用空间为整个磁盘。") - max_available_mib = max(0.0, total_disk_mib - 1.0) # 留一点空间,避免边界问题 - start_position_mib = 1.0 # 现代分区表通常从1MB或更大偏移开始 + max_available_mib = max(0.0, total_disk_mib - 1.0) + start_position_mib = 1.0 logger.debug(f"磁盘 {disk_path} 没有现有分区,假定从 {start_position_mib} MiB 开始,最大可用空间为 {max_available_mib} MiB。") dialog = CreatePartitionDialog(self, disk_path, total_disk_mib, max_available_mib) @@ -308,18 +357,14 @@ class MainWindow(QMainWindow): """ 调用 DiskOperations 的异步格式化方法。 """ - # DiskOperations.format_partition 会处理用户确认和启动后台线程 self.disk_ops.format_partition(device_path) - # 界面刷新将在格式化完成后由 on_disk_formatting_finished 槽函数触发,所以这里不需要 refresh_all_info() - # <--- 新增: 格式化完成后的槽函数 def on_disk_formatting_finished(self, success, device_path, stdout, stderr): """ 接收 DiskOperations 发出的格式化完成信号,并刷新界面。 """ logger.info(f"格式化完成信号接收: 设备 {device_path}, 成功: {success}") - # QMessageBox 已经在 DiskOperations 的 _on_formatting_finished 中处理 - self.refresh_all_info() # 刷新所有信息以显示更新后的文件系统类型等 + self.refresh_all_info() # --- RAID 管理 Tab --- def refresh_raid_info(self): @@ -336,7 +381,7 @@ class MainWindow(QMainWindow): self.ui.treeWidget_raid.header().setSectionResizeMode(i, QHeaderView.ResizeToContents) try: - raid_arrays = self.system_manager.get_mdadm_arrays() # 现在会返回所有阵列,包括停止的 + raid_arrays = self.system_manager.get_mdadm_arrays() if not raid_arrays: item = QTreeWidgetItem(self.ui.treeWidget_raid) item.setText(0, "未找到RAID阵列。") @@ -350,14 +395,13 @@ class MainWindow(QMainWindow): logger.warning(f"RAID阵列 '{array.get('name', '未知')}' 的设备路径无效,跳过。") continue - # 对于停止状态的阵列,挂载点可能不存在或不相关 current_mount_point = "" if array.get('state') != 'Stopped (Configured)': current_mount_point = self.system_manager.get_mountpoint_for_device(array_path) array_item.setText(0, array_path) array_item.setText(1, array.get('level', 'N/A')) - array_item.setText(2, array.get('state', 'N/A')) # 显示实际状态 + array_item.setText(2, array.get('state', 'N/A')) array_item.setText(3, array.get('array_size', 'N/A')) array_item.setText(4, array.get('active_devices', 'N/A')) array_item.setText(5, array.get('failed_devices', 'N/A')) @@ -369,17 +413,14 @@ class MainWindow(QMainWindow): array_item.setText(11, current_mount_point if current_mount_point else "") array_item.setExpanded(True) - # 存储完整的阵列数据,包括状态,供上下文菜单使用 array_item.setData(0, Qt.UserRole, array) - # 对于停止状态的阵列,成员设备可能为空,跳过显示子项 if array.get('state') != 'Stopped (Configured)': for member in array.get('member_devices', []): member_item = QTreeWidgetItem(array_item) member_item.setText(0, f" {member.get('device_path', 'N/A')}") - member_item.setText(1, f"成员: {member.get('raid_device', 'N/A')}") # 假设 raid_device 字段存在 + member_item.setText(1, f"成员: {member.get('raid_device', 'N/A')}") member_item.setText(2, member.get('state', 'N/A')) - # member_item.setText(3, f"Major: {member.get('major', 'N/A')}, Minor: {member.get('minor', 'N/A')}") # 假设 major/minor 字段存在 for i in range(len(raid_headers)): self.ui.treeWidget_raid.resizeColumnToContents(i) @@ -397,7 +438,7 @@ class MainWindow(QMainWindow): create_raid_action.triggered.connect(self._handle_create_raid_array) menu.addSeparator() - if item and item.parent() is None: # 只针对顶层阵列项 + if item and item.parent() is None: array_data = item.data(0, Qt.UserRole) if not array_data: logger.warning(f"无法获取 RAID 阵列 {item.text(0)} 的详细数据。") @@ -406,7 +447,7 @@ class MainWindow(QMainWindow): array_path = array_data.get('device') array_state = array_data.get('state', 'N/A') member_devices = [m.get('device_path') for m in array_data.get('member_devices', [])] - array_uuid = array_data.get('uuid') # <--- 获取 UUID + array_uuid = array_data.get('uuid') if not array_path or array_path == 'N/A': logger.warning(f"RAID阵列 '{array_data.get('name', '未知')}' 的设备路径无效,无法显示操作。") @@ -415,11 +456,9 @@ class MainWindow(QMainWindow): if array_state == 'Stopped (Configured)': activate_action = menu.addAction(f"激活阵列 {array_path}") activate_action.triggered.connect(lambda: self._handle_activate_raid_array(array_path)) - # <--- 新增:删除停止状态阵列的配置条目 delete_config_action = menu.addAction(f"删除配置文件条目 (UUID: {array_uuid})") delete_config_action.triggered.connect(lambda: self._handle_delete_configured_raid_array(array_uuid)) - # 停止状态的阵列不显示卸载、停止、删除、格式化等操作 - else: # 活动或降级状态的阵列 + else: current_mount_point = self.system_manager.get_mountpoint_for_device(array_path) if current_mount_point and current_mount_point != '[SWAP]' and current_mount_point != '': @@ -430,10 +469,15 @@ class MainWindow(QMainWindow): mount_action.triggered.connect(lambda: self._handle_mount(array_path)) menu.addSeparator() + # --- 新增:RAID 阵列解除占用选项 --- + resolve_occupation_action = menu.addAction(f"解除占用 {array_path}") + resolve_occupation_action.triggered.connect(lambda: self._handle_resolve_device_occupation(array_path)) + menu.addSeparator() # 在解除占用后添加分隔符 + + stop_action = menu.addAction(f"停止阵列 {array_path}") stop_action.triggered.connect(lambda: self._handle_stop_raid_array(array_path)) - # <--- 修改:删除活动阵列的动作,传递 UUID delete_action = menu.addAction(f"删除阵列 {array_path}") delete_action.triggered.connect(lambda: self._handle_delete_active_raid_array(array_path, member_devices, array_uuid)) @@ -449,8 +493,8 @@ class MainWindow(QMainWindow): """ 处理激活已停止的 RAID 阵列。 """ - item = self.ui.treeWidget_raid.currentItem() # 获取当前选中的项 - if not item or item.parent() is not None: # 确保是顶层阵列项 + item = self.ui.treeWidget_raid.currentItem() + if not item or item.parent() is not None: logger.warning("未选择有效的 RAID 阵列进行激活。") return @@ -477,7 +521,7 @@ class MainWindow(QMainWindow): logger.info(f"用户取消了激活 RAID 阵列 {array_path} 的操作。") return - if self.raid_ops.activate_raid_array(array_path, array_uuid): # 传递 UUID + if self.raid_ops.activate_raid_array(array_path, array_uuid): self.refresh_all_info() def _handle_create_raid_array(self): @@ -498,16 +542,16 @@ class MainWindow(QMainWindow): if self.raid_ops.stop_raid_array(array_path): self.refresh_all_info() - 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): """ 处理删除一个活动的 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() - def _handle_delete_configured_raid_array(self, uuid): # <--- 新增方法 + def _handle_delete_configured_raid_array(self, uuid): """ - 处理删除停止状态 RAID 阵列的配置文件条目。 + 处理停止状态 RAID 阵列的配置文件条目。 """ if self.raid_ops.delete_configured_raid_array(uuid): self.refresh_all_info() @@ -516,9 +560,7 @@ class MainWindow(QMainWindow): """ 处理 RAID 阵列的格式化。 """ - # 这将调用 DiskOperations 的异步格式化方法 self.disk_ops.format_partition(array_path) - # 刷新操作会在 on_disk_formatting_finished 中触发,所以这里不需要 refresh_all_info() # --- LVM 管理 Tab --- def refresh_lvm_info(self): @@ -697,6 +739,11 @@ class MainWindow(QMainWindow): mount_lv_action.triggered.connect(lambda: self._handle_mount(lv_path)) menu.addSeparator() + # --- 新增:逻辑卷解除占用选项 --- + resolve_occupation_action = menu.addAction(f"解除占用 {lv_path}") + resolve_occupation_action.triggered.connect(lambda: self._handle_resolve_device_occupation(lv_path)) + menu.addSeparator() # 在解除占用后添加分隔符 + delete_lv_action = menu.addAction(f"删除逻辑卷 {lv_name}") delete_lv_action.triggered.connect(lambda: self._handle_delete_lv(lv_name, vg_name)) @@ -739,7 +786,7 @@ class MainWindow(QMainWindow): available_pvs.append(f"/dev/{pv_name}") if not available_pvs: - QMessageBox.warning(self, "警告", "没有可用于创建卷组的物理卷。请确保有未分配给任何卷组的物理卷。") + QMessageBox.warning(self, "警告", "没有可用于创建卷组的物理卷。") return dialog = CreateVgDialog(self, available_pvs) @@ -758,27 +805,23 @@ class MainWindow(QMainWindow): if vg_name and vg_name != 'N/A': available_vgs.append(vg_name) - # 将字符串转换为小写并去除可能存在的 '<' 符号,以便更准确地匹配 free_size_str = vg.get('vg_free', '0B').strip().lower() current_vg_size_gb = 0.0 - # 修正正则表达式: - # 1. 允许可选的 '<' 符号在数字前 - # 2. 确保单位匹配正确,如果单位缺失,默认按 GB 处理 (LVM 的 vg_free 常常默认 GB) match = re.match(r' Tuple[bool, str, str] + mount_info_getter_func: 一个可调用对象,用于获取设备的挂载点。 + 期望签名: (device_path: str) -> Optional[str] + """ + self._execute_shell_command = shell_executor_func + self._get_mountpoint_for_device = mount_info_getter_func + + def _run_command(self, cmd: List[str], error_msg: str, **kwargs) -> Tuple[bool, str, str]: + """ + 执行 shell 命令的包装器。 + """ + return self._execute_shell_command(cmd, error_msg, **kwargs) + + def get_all_mounts_under_path(self, base_path: str) -> List[str]: + """ + 获取指定路径下所有嵌套的挂载点。 + 返回一个挂载点列表,从最深层到最浅层排序。 + """ + mounts = [] + # 尝试使用 findmnt 命令更可靠地获取嵌套挂载信息 + # -l: 列出所有挂载 + # -o TARGET: 只输出目标路径 + # -n: 不显示标题行 + # -r: 原始输出,方便解析 + # --target : 限制只查找 base_path 下的挂载 + # -t noautofs,nosnapfs,nofuse,nocifs,nonfs,notmpfs,nobind: 排除一些不相关的或虚拟的挂载 + findmnt_cmd = ["findmnt", "-l", "-o", "TARGET", "-n", "-r", "--target", base_path, + "-t", "noautofs,nosnapfs,nofuse,nocifs,nonfs,notmpfs,nobind"] + success, stdout, stderr = self._run_command(findmnt_cmd, f"获取 {base_path} 下的挂载信息失败", show_dialog=False) + + if not success: + logger.warning(f"findmnt 命令失败,尝试解析 'mount' 命令输出: {stderr}") + # 如果 findmnt 失败或不可用,则回退到解析 'mount' 命令输出 + success, stdout, stderr = self._run_command(["mount"], "获取挂载信息失败", show_dialog=False) + if not success: + logger.error(f"获取挂载信息失败: {stderr}") + return [] + + for line in stdout.splitlines(): + match = re.search(r' on (\S+) type ', line) + if match: + mount_point = match.group(1) + # 确保是嵌套挂载点而不是基础挂载点本身 + if mount_point.startswith(base_path) and mount_point != base_path: + mounts.append(mount_point) + else: + for line in stdout.splitlines(): + mount_point = line.strip() + # findmnt --target 已经过滤了,这里只需确保不是空行 + if mount_point and mount_point.startswith(base_path) and mount_point != base_path: + mounts.append(mount_point) + + # 排序:从最深层到最浅层,以便正确卸载。路径越长,通常越深层。 + mounts.sort(key=lambda x: len(x.split('/')), reverse=True) + logger.debug(f"在 {base_path} 下找到的嵌套挂载点(从深到浅): {mounts}") + return mounts + + def _unmount_nested_mounts(self, base_mount_point: str) -> bool: + """ + 尝试卸载指定基础挂载点下的所有嵌套挂载。 + """ + nested_mounts = self.get_all_mounts_under_path(base_mount_point) + if not nested_mounts: + logger.debug(f"在 {base_mount_point} 下没有找到嵌套挂载点。") + return True # 没有需要卸载的嵌套挂载 + + logger.info(f"开始卸载 {base_mount_point} 下的嵌套挂载点: {nested_mounts}") + all_nested_unmounted = True + for mount_point in nested_mounts: + if not self._unmount_target(mount_point): + logger.error(f"未能卸载嵌套挂载点 {mount_point}。") + all_nested_unmounted = False + # 即使一个失败,也尝试卸载其他,但标记整体失败 + return all_nested_unmounted + + def _unmount_target(self, target_path: str) -> bool: + """ + 尝试卸载一个目标(设备或挂载点)。 + 先尝试普通卸载,如果失败则尝试强制卸载,再失败则尝试懒惰卸载。 + """ + logger.info(f"尝试卸载 {target_path}。") + success, stdout, stderr = self._run_command( + ["umount", target_path], + f"卸载 {target_path} 失败", + show_dialog=False, + expected_non_zero_exit_codes=[1] # umount 返回 1 表示未挂载或忙碌 + ) + if success: + logger.info(f"成功卸载 {target_path}。") + return True + else: + # 检查是否因为未挂载而失败 + if "not mounted" in stderr.lower() or "未挂载" in stderr: + logger.info(f"目标 {target_path} 未挂载,无需卸载。") + return True + + logger.warning(f"卸载 {target_path} 失败: {stderr}") + # 尝试强制卸载 -f + logger.info(f"尝试强制卸载 {target_path} (umount -f)。") + success_force, stdout_force, stderr_force = self._run_command( + ["umount", "-f", target_path], + f"强制卸载 {target_path} 失败", + show_dialog=False, + expected_non_zero_exit_codes=[1] + ) + if success_force: + logger.info(f"成功强制卸载 {target_path}。") + return True + else: + logger.warning(f"强制卸载 {target_path} 失败: {stderr_force}") + # 尝试懒惰卸载 -l + logger.info(f"尝试懒惰卸载 {target_path} (umount -l)。") + success_lazy, stdout_lazy, stderr_lazy = self._run_command( + ["umount", "-l", target_path], + f"懒惰卸载 {target_path} 失败", + show_dialog=False, + expected_non_zero_exit_codes=[1] + ) + if success_lazy: + logger.info(f"成功懒惰卸载 {target_path}。") + return True + else: + logger.error(f"懒惰卸载 {target_path} 失败: {stderr_lazy}") + return False + + def _kill_pids(self, pids: List[str]) -> bool: + """ + 终止指定PID的进程。 + """ + all_killed = True + for pid in pids: + logger.info(f"尝试终止进程 {pid}。") + kill_success, _, kill_stderr = self._run_command( + ["kill", "-9", pid], + f"终止进程 {pid} 失败", + show_dialog=False # UI 交互在更高层处理 + ) + if not kill_success: + logger.error(f"终止进程 {pid} 失败: {kill_stderr}") + all_killed = False + else: + logger.info(f"成功终止进程 {pid}。") + return all_killed + + def resolve_occupation(self, device_path: str) -> bool: + """ + 尝试解除设备占用,包括处理嵌套挂载和终止进程。 + 返回 True 如果成功解除占用,否则返回 False。 + """ + logger.info(f"开始尝试解除设备 {device_path} 的占用。") + + # 1. 获取设备的当前主挂载点 + main_mount_point = self._get_mountpoint_for_device(device_path) + + # 2. 如果设备有主挂载点,先尝试卸载所有嵌套挂载 + if main_mount_point and main_mount_point != 'N/A' and main_mount_point != '[SWAP]': + logger.debug(f"设备 {device_path} 的主挂载点是 {main_mount_point}。") + + # 尝试卸载嵌套挂载,并循环几次,因为某些挂载可能需要多次尝试 + max_unmount_attempts = 3 + for attempt in range(max_unmount_attempts): + logger.info(f"尝试卸载嵌套挂载点 (第 {attempt + 1} 次尝试)。") + if self._unmount_nested_mounts(main_mount_point): + logger.info(f"所有嵌套挂载点已成功卸载或已不存在。") + break + else: + logger.warning(f"卸载嵌套挂载点失败 (第 {attempt + 1} 次尝试),可能需要重试。") + if attempt == max_unmount_attempts - 1: + QMessageBox.critical(None, "错误", f"未能卸载设备 {device_path} 下的所有嵌套挂载点。") + return False + + # 尝试卸载主挂载点本身 + logger.info(f"尝试卸载主挂载点 {main_mount_point}。") + if self._unmount_target(main_mount_point): + QMessageBox.information(None, "成功", f"设备 {device_path} 及其所有挂载点已成功卸载。") + return True # 主挂载点已卸载,设备应该已空闲 + + # 3. 检查设备本身或其(可能已不存在的)主挂载点是否仍被占用 (fuser) + # 即使主挂载点已卸载,也可能存在进程直接占用设备文件的情况,或者之前卸载失败。 + fuser_targets = [device_path] + if main_mount_point and main_mount_point != 'N/A' and main_mount_point != '[SWAP]' and main_mount_point not in fuser_targets: + fuser_targets.append(main_mount_point) + + pids_to_kill = set() + kernel_mounts_found = False + occupation_info_lines = [] + + for target in fuser_targets: + logger.debug(f"执行 fuser -vm {target} 检查占用。") + success_fuser, stdout_fuser, stderr_fuser = self._run_command( + ["fuser", "-vm", target], + f"检查 {target} 占用失败", + show_dialog=False, + suppress_critical_dialog_on_stderr_match=( + "No such file or directory", "not found", "Usage:", "not mounted" + ), + expected_non_zero_exit_codes=[1] + ) + + if success_fuser and stdout_fuser: + logger.debug(f"fuser -vm {target} 输出:\n{stdout_fuser}") + for line in stdout_fuser.splitlines(): + occupation_info_lines.append(line.strip()) + + if "kernel" in line: + kernel_mounts_found = True + + match = re.match(r'^\S+:\s+\S+\s+(?P\d+)\s+.*', line) + if match: + pids_to_kill.add(match.group('id')) + + pids_to_kill_list = list(pids_to_kill) + + if not kernel_mounts_found and not pids_to_kill_list: + QMessageBox.information(None, "信息", f"设备 {device_path} 未被任何进程占用。") + logger.info(f"设备 {device_path} 未被任何进程占用。") + return True + + # 4. 处理剩余的内核挂载(如果嵌套挂载和主挂载卸载失败,这里会再次捕捉到) + if kernel_mounts_found: + process_list_str = "\n".join(sorted(list(set(occupation_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}。") + QMessageBox.information(None, "信息", f"已取消卸载设备 {device_path}。") + return False + + logger.info(f"再次尝试卸载设备 {device_path}。") + if self._unmount_target(device_path): # 尝试直接卸载设备路径 + QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。") + return True + else: + QMessageBox.critical(None, "错误", f"未能卸载设备 {device_path}。") + return False + + # 5. 处理用户进程占用 + if pids_to_kill_list: + process_list_str = "\n".join(sorted(list(set(occupation_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} 的进程。") + QMessageBox.information(None, "信息", f"已取消终止占用设备 {device_path} 的进程。") + return False + + if self._kill_pids(pids_to_kill_list): + QMessageBox.information(None, "成功", f"已尝试终止占用设备 {device_path} 的所有进程。") + # 进程终止后,设备可能仍然被挂载,因此再次尝试卸载。 + if main_mount_point and main_mount_point != 'N/A' and main_mount_point != '[SWAP]': + logger.info(f"进程终止后,尝试再次卸载主挂载点 {main_mount_point}。") + if self._unmount_target(main_mount_point): + QMessageBox.information(None, "信息", f"设备 {device_path} 已成功卸载。") + return True + else: # 如果没有主挂载点,检查设备本身 + logger.info(f"进程终止后,尝试再次卸载设备 {device_path}。") + if self._unmount_target(device_path): + QMessageBox.information(None, "信息", f"设备 {device_path} 已成功卸载。") + return True + + # 如果进程终止后仍未能卸载 + QMessageBox.warning(None, "警告", f"进程已终止,但未能成功卸载设备 {device_path}。可能仍有其他问题。") + return False # 表示部分成功或需要进一步手动干预 + else: + QMessageBox.critical(None, "错误", f"未能终止所有占用设备 {device_path} 的进程。") + return False + + logger.warning(f"设备 {device_path} 占用解决逻辑未能完全处理。") + return False + diff --git a/pyproject.toml b/pyproject.toml index 27bdb73..7559d02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,4 +2,4 @@ name = "PySide Widgets Project" [tool.pyside6-project] -files = ["dialogs.py", "disk_operations.py", "form.ui", "logger_config.py", "lvm_operations.py", "mainwindow.py", "raid_operations.py", "system_info.py"] +files = ["dialogs.py", "disk_operations.py", "form.ui", "logger_config.py", "lvm_operations.py", "mainwindow.py", "occupation_resolver.py", "raid_operations.py", "system_info.py"] diff --git a/raid_operations.py b/raid_operations.py index 8d7fa1b..34146e9 100644 --- a/raid_operations.py +++ b/raid_operations.py @@ -1,21 +1,18 @@ # raid_operations.py import logging import subprocess -from PySide6.QtWidgets import QMessageBox, QInputDialog, QLineEdit # 导入 QInputDialog 和 QLineEdit +from PySide6.QtWidgets import QMessageBox, QInputDialog, QLineEdit from PySide6.QtCore import Qt from system_info import SystemInfoManager import re -import pexpect # <--- 新增导入 pexpect +import pexpect import sys -# NEW: 导入 DiskOperations -from disk_operations import DiskOperations logger = logging.getLogger(__name__) class RaidOperations: - def __init__(self, system_manager: SystemInfoManager, disk_ops: DiskOperations): # <--- Add system_manager and disk_ops parameters + def __init__(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): """ @@ -35,10 +32,9 @@ class RaidOperations: logger.info(f"执行命令: {full_cmd_str}") try: - # SystemInfoManager._run_command 应该负责在 root_privilege=True 时添加 sudo stdout, stderr = self.system_manager._run_command( command_list, - root_privilege=True, # 假设所有 RAID 操作都需要 root 权限 + root_privilege=True, check_output=True, input_data=input_to_command ) @@ -54,7 +50,6 @@ class RaidOperations: logger.error(f"标准输出: {e.stdout.strip()}") logger.error(f"标准错误: {stderr_output}") - # --- 修改开始 --- should_suppress_dialog = False if suppress_critical_dialog_on_stderr_match: if isinstance(suppress_critical_dialog_on_stderr_match, str): @@ -64,13 +59,12 @@ class RaidOperations: for pattern in suppress_critical_dialog_on_stderr_match: if pattern in stderr_output: should_suppress_dialog = True - break # 找到一个匹配就足够了 + break if should_suppress_dialog: logger.info(f"特定错误 '{stderr_output}' 匹配抑制条件,已抑制错误对话框。") else: QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: {stderr_output}") - # --- 修改结束 --- return False, e.stdout, stderr_output except FileNotFoundError: @@ -87,25 +81,19 @@ class RaidOperations: 专门处理 mdadm --create 命令的交互式执行。 通过 pexpect 监听提示并弹出 QMessageBox 让用户选择。 """ - # pexpect.spawn 需要一个字符串命令,并且 sudo 应该包含在内 - # 假设 SystemInfoManager._run_command 在 root_privilege=True 时会添加 sudo - # 但 pexpect.spawn 需要完整的命令字符串,所以这里手动添加 sudo full_cmd_str = "sudo " + ' '.join(command_list) logger.info(f"执行交互式命令: {full_cmd_str}") stdout_buffer = [] try: - # 增加 timeout,因为用户交互可能需要时间 - child = pexpect.spawn(full_cmd_str, encoding='utf-8', timeout=300) # 增加超时到 5 分钟 + child = pexpect.spawn(full_cmd_str, encoding='utf-8', timeout=300) child.logfile = sys.stdout - # --- 1. 优先处理 sudo 密码提示 --- try: - # 尝试匹配 sudo 密码提示,设置较短的超时,如果没出现就继续 index = child.expect(['[pP]assword for', pexpect.TIMEOUT], timeout=5) - if index == 0: # 匹配到密码提示 - stdout_buffer.append(child.before) # 捕获提示前的输出 + if index == 0: + stdout_buffer.append(child.before) password, ok = QInputDialog.getText( None, "Sudo 密码", "请输入您的 sudo 密码:", QLineEdit.Password ) @@ -114,45 +102,36 @@ class RaidOperations: child.close() return False, "".join(stdout_buffer), "Sudo 密码未提供或取消。" child.sendline(password) - stdout_buffer.append(child.after) # 捕获发送密码后的输出 + stdout_buffer.append(child.after) logger.info("已发送 sudo 密码。") - elif index == 1: # 超时,未出现密码提示 (可能是 NOPASSWD 或已缓存) + elif index == 1: logger.info("Sudo 未提示输入密码(可能已配置 NOPASSWD 或密码已缓存)。") - stdout_buffer.append(child.before) # 捕获任何初始输出 + stdout_buffer.append(child.before) except pexpect.exceptions.EOF: stdout_buffer.append(child.before) logger.error("命令在 sudo 密码提示前退出。") child.close() return False, "".join(stdout_buffer), "命令在 sudo 密码提示前退出。" except pexpect.exceptions.TIMEOUT: - # 再次捕获超时,确保日志记录 logger.info("Sudo 密码提示超时,可能已配置 NOPASSWD 或密码已缓存。") stdout_buffer.append(child.before) - - # 定义交互式提示及其处理方式 - prompts_data = [ # 重命名为 prompts_data 以避免与循环变量混淆 + prompts_data = [ (r"To optimalize recovery speed, .* write-(?:intent|indent) bitmap, do you want to enable it now\? \[y/N\]\s*", "mdadm 建议启用写入意图位图以优化恢复速度。您希望现在启用它吗?"), (r"Continue creating array \[y/N\]\s*", "mdadm 警告:检测到驱动器大小不一致或分区表。创建阵列将覆盖数据。是否仍要继续创建阵列?") ] - # 提取所有模式,以便 pexpect.expect 可以同时监听它们 patterns_to_expect = [p for p, _ in prompts_data] - # 循环处理所有可能的提示,直到没有更多提示或命令结束 while True: try: - # 尝试匹配任何一个预定义的提示 - # pexpect 会等待直到匹配到其中一个模式,或者超时,或者遇到 EOF - index = child.expect(patterns_to_expect, timeout=10) # 每次等待10秒 - stdout_buffer.append(child.before) # 捕获到提示前的输出 + index = child.expect(patterns_to_expect, timeout=10) + stdout_buffer.append(child.before) - # 获取匹配到的提示的描述 matched_description = prompts_data[index][1] - # 弹出对话框询问用户 reply = QMessageBox.question(None, "mdadm 提示", matched_description, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) @@ -164,28 +143,20 @@ class RaidOperations: logger.info(f"用户对提示 '{matched_description}' 选择 '否'") child.sendline(user_choice) - stdout_buffer.append(user_choice + '\n') # 记录用户输入 - - # 等待一小段时间,确保 mdadm 接收并处理了输入,并清空缓冲区 - # 避免在下一次 expect 之前,旧的输出再次被匹配 - # child.expect(pexpect.TIMEOUT, timeout=1) # This line might consume output needed for next prompt + stdout_buffer.append(user_choice + '\n') except pexpect.exceptions.TIMEOUT: - # 如果在等待任何提示时超时,说明没有更多提示了,或者命令已完成。 logger.debug("在等待 mdadm 提示时超时,可能没有更多交互。") - stdout_buffer.append(child.before) # 捕获当前为止的输出 - break # 跳出 while 循环,进入等待 EOF 阶段 + stdout_buffer.append(child.before) + break except pexpect.exceptions.EOF: - # 如果在等待提示时遇到 EOF,说明命令已经执行完毕或失败 logger.warning("在等待 mdadm 提示时遇到 EOF。") stdout_buffer.append(child.before) - break # 跳出 while 循环,进入等待 EOF 阶段 - + break try: - # 增加一个合理的超时时间,以防 mdadm 在后台进行长时间初始化 - child.expect(pexpect.EOF, timeout=120) # 例如,等待 120 秒 - stdout_buffer.append(child.before) # 捕获命令结束前的任何剩余输出 + child.expect(pexpect.EOF, timeout=120) + stdout_buffer.append(child.before) logger.info("mdadm 命令已正常结束 (通过 EOF 捕获)。") except pexpect.exceptions.TIMEOUT: stdout_buffer.append(child.before) @@ -197,12 +168,11 @@ class RaidOperations: stdout_buffer.append(child.before) logger.info("mdadm 命令已正常结束 (通过 EOF 捕获)。") - child.close() final_output = "".join(stdout_buffer) if child.exitstatus == 0: logger.info(f"交互式命令 {full_cmd_str} 成功完成。") - return True, final_output, "" # pexpect 很难区分 stdout/stderr,都视为 stdout + return True, final_output, "" else: logger.error(f"交互式命令 {full_cmd_str} 失败,退出码: {child.exitstatus}") QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: 命令执行失败,退出码 {child.exitstatus}\n输出: {final_output}") @@ -247,7 +217,6 @@ class RaidOperations: :param level: RAID 级别,例如 'raid0', 'raid1', 'raid5' (也可以是整数 0, 1, 5) :param chunk_size: Chunk 大小 (KB) """ - # --- 标准化 RAID 级别输入 --- if isinstance(level, int): level_str = f"raid{level}" elif isinstance(level, str): @@ -259,13 +228,11 @@ class RaidOperations: QMessageBox.critical(None, "错误", f"不支持的 RAID 级别类型: {type(level)}") return False level = level_str - # --- 标准化 RAID 级别输入结束 --- - if not devices or len(devices) < 1: # RAID0 理论上可以一个设备,但通常至少两个 + if not devices or len(devices) < 1: QMessageBox.critical(None, "错误", "创建 RAID 阵列至少需要一个设备(RAID0)。") return False - # 检查其他 RAID 级别所需的设备数量 if level == "raid1" and len(devices) < 2: QMessageBox.critical(None, "错误", "RAID1 至少需要两个设备。") return False @@ -275,17 +242,10 @@ class RaidOperations: if level == "raid6" and len(devices) < 4: QMessageBox.critical(None, "错误", "RAID6 至少需要四个设备。") return False - if level == "raid10" and len(devices) < 2: # RAID10 至少需要 2 个设备 (例如 1+0) + if level == "raid10" and len(devices) < 2: QMessageBox.critical(None, "错误", "RAID10 至少需要两个设备。") 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 阵列", f"你确定要使用设备 {', '.join(devices)} 创建 RAID {level} 阵列吗?\n" "此操作将销毁设备上的所有数据!", @@ -322,23 +282,18 @@ class RaidOperations: create_cmd_base = ["mdadm", "--create", array_name, "--level=" + level.replace("raid", ""), f"--raid-devices={len(devices)}"] - # 添加 chunk size (RAID0, RAID5, RAID6, RAID10 通常需要) if level in ["raid0", "raid5", "raid6", "raid10"]: create_cmd_base.append(f"--chunk={chunk_size}K") create_cmd = create_cmd_base + devices - - # 在 --create 命令中添加 --force 选项 create_cmd.insert(2, "--force") - # 调用新的交互式方法 success_create, stdout_create, stderr_create = self._execute_interactive_mdadm_command( create_cmd, f"创建 RAID 阵列失败" ) if not success_create: - # pexpect 捕获的输出可能都在 stdout_create 中 if "Array name" in stdout_create and "is in use already" in stdout_create: QMessageBox.critical(None, "错误", f"创建 RAID 阵列失败:阵列名称 {array_name} 已被占用。请尝试停止或删除现有阵列。") return False @@ -350,7 +305,6 @@ class RaidOperations: examine_scan_cmd = ["mdadm", "--examine", "--scan"] success_scan, scan_stdout, _ = self._execute_shell_command(examine_scan_cmd, "扫描 mdadm 配置失败") if success_scan: - # --- 新增:确保 /etc 目录存在 --- mkdir_cmd = ["mkdir", "-p", "/etc"] success_mkdir, _, stderr_mkdir = self._execute_shell_command( mkdir_cmd, @@ -359,13 +313,9 @@ class RaidOperations: if not success_mkdir: logger.error(f"无法创建 /etc 目录,更新 mdadm.conf 失败。错误: {stderr_mkdir}") QMessageBox.critical(None, "错误", f"无法创建 /etc 目录,更新 mdadm.conf 失败。\n详细信息: {stderr_mkdir}") - return False # 如果连目录都无法创建,则认为创建阵列失败,因为无法保证重启后自动识别 + return False logger.info("已确保 /etc 目录存在。") - # --- 新增结束 --- - # 这里需要确保 /etc/mdadm.conf 存在且可写入 - # _execute_shell_command 会自动添加 sudo - # 使用 tee -a 而不是 >> 来确保 sudo 权限下的写入 success_append, _, _ = self._execute_shell_command( ["bash", "-c", f"echo '{scan_stdout.strip()}' | tee -a /etc/mdadm.conf > /dev/null"], "更新 /etc/mdadm/mdadm.conf 失败" @@ -395,11 +345,12 @@ class RaidOperations: logger.info(f"尝试停止 RAID 阵列: {array_path}") - # NEW: 尝试卸载阵列(如果已挂载),利用 DiskOperations 的增强卸载功能 - # unmount_partition 内部会尝试解决设备占用问题 - if not self.disk_ops.unmount_partition(array_path, show_dialog_on_error=False): - logger.warning(f"未能成功卸载 RAID 阵列 {array_path}。尝试继续停止操作。") - # 即使卸载失败,我们仍然尝试停止 mdadm 阵列,因为有时 mdadm --stop 可以在设备忙时强制停止。 + # Original umount call (not using disk_ops.unmount_partition) + self._execute_shell_command( + ["umount", array_path], + f"尝试卸载 {array_path} 失败", + suppress_critical_dialog_on_stderr_match=("not mounted", "未挂载") + ) if not self._execute_shell_command(["mdadm", "--stop", array_path], f"停止 RAID 阵列 {array_path} 失败")[0]: return False @@ -408,7 +359,7 @@ class RaidOperations: QMessageBox.information(None, "成功", f"成功停止 RAID 阵列 {array_path}。") return True - def delete_active_raid_array(self, array_path, member_devices, uuid): # <--- 修改方法名并添加 uuid 参数 + def delete_active_raid_array(self, array_path, member_devices, uuid): """ 删除一个活动的 RAID 阵列。 此操作将停止阵列、清除成员设备上的超级块,并删除 mdadm.conf 中的配置。 @@ -427,7 +378,6 @@ class RaidOperations: logger.info(f"尝试删除活动 RAID 阵列: {array_path} (UUID: {uuid})") # 1. 停止阵列 - # stop_raid_array 内部会调用 self.disk_ops.unmount_partition 来尝试卸载并解决占用 if not self.stop_raid_array(array_path): QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 失败,因为无法停止阵列。") return False @@ -459,7 +409,7 @@ class RaidOperations: except Exception as e: QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 的配置失败: {e}") logger.error(f"删除 RAID 阵列 {array_path} 的配置失败: {e}") - return False # 如果配置文件删除失败,也视为整体操作失败 + return False if success_all_cleared: logger.info(f"成功删除 RAID 阵列 {array_path} 并清除了成员设备超级块。") @@ -469,7 +419,7 @@ class RaidOperations: QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 完成,但部分成员设备超级块未能完全清除。") return False - def delete_configured_raid_array(self, uuid): # <--- 新增方法 + def delete_configured_raid_array(self, uuid): """ 只删除 mdadm.conf 中停止状态 RAID 阵列的配置条目。 :param uuid: 要删除的 RAID 阵列的 UUID。 @@ -494,7 +444,7 @@ class RaidOperations: return False - def activate_raid_array(self, array_path, array_uuid): # 添加 array_uuid 参数 + def activate_raid_array(self, array_path, array_uuid): """ 激活一个已停止的 RAID 阵列。 使用 mdadm --assemble --uuid= @@ -506,12 +456,9 @@ class RaidOperations: logger.info(f"尝试激活 RAID 阵列: {array_path} (UUID: {array_uuid})") - # 使用 mdadm --assemble --uuid= - # 这样可以确保只激活用户选择的特定阵列 success, stdout, stderr = self._execute_shell_command( ["mdadm", "--assemble", array_path, "--uuid", array_uuid], f"激活 RAID 阵列 {array_path} 失败", - # 抑制一些常见的非致命错误,例如阵列已经激活 suppress_critical_dialog_on_stderr_match=("mdadm: device /dev/md", "already active", "No such file or directory") ) @@ -522,4 +469,3 @@ class RaidOperations: else: logger.error(f"激活 RAID 阵列 {array_path} 失败。") return False -