diff --git a/.qtcreator/pyproject.toml.user b/.qtcreator/pyproject.toml.user index f646c9c..effd638 100644 --- a/.qtcreator/pyproject.toml.user +++ b/.qtcreator/pyproject.toml.user @@ -1,6 +1,6 @@ - + EnvironmentId @@ -99,13 +99,15 @@ {2b4075d3-005b-46f6-8fd5-3bd8a94071db} 0 0 - 0 + 2 /home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2 true Python.PysideBuildStep + /home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2/bin/pyside6-project + /home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2/bin/pyside6-uic 1 构建 @@ -126,7 +128,7 @@ Python 3.14.2 Virtual Environment Python.PySideBuildConfiguration 0 - 0 + 2 0 @@ -224,7 +226,70 @@ /home/jing/qtpj/diskmanager :0 - 4 + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + dialogs.py + PythonEditor.RunConfiguration. + /home/jing/qtpj/diskmanager/dialogs.py + false + + /home/jing/qtpj/diskmanager/dialogs.py + true + true + /home/jing/qtpj/diskmanager + :0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + raid_operations.py + PythonEditor.RunConfiguration. + /home/jing/qtpj/diskmanager/raid_operations.py + false + + /home/jing/qtpj/diskmanager/raid_operations.py + true + true + /home/jing/qtpj/diskmanager + :0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + lvm_operations.py + PythonEditor.RunConfiguration. + /home/jing/qtpj/diskmanager/lvm_operations.py + false + + /home/jing/qtpj/diskmanager/lvm_operations.py + true + true + /home/jing/qtpj/diskmanager + :0 + + 7 /home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2/bin/python /home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2 @@ -326,7 +391,70 @@ /home/jing/qtpj/diskmanager :0 - 4 + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + dialogs.py + PythonEditor.RunConfiguration. + /home/jing/qtpj/diskmanager/dialogs.py + false + + /home/jing/qtpj/diskmanager/dialogs.py + true + true + /home/jing/qtpj/diskmanager + :0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + raid_operations.py + PythonEditor.RunConfiguration. + /home/jing/qtpj/diskmanager/raid_operations.py + false + + /home/jing/qtpj/diskmanager/raid_operations.py + true + true + /home/jing/qtpj/diskmanager + :0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + lvm_operations.py + PythonEditor.RunConfiguration. + /home/jing/qtpj/diskmanager/lvm_operations.py + false + + /home/jing/qtpj/diskmanager/lvm_operations.py + true + true + /home/jing/qtpj/diskmanager + :0 + + 7 diff --git a/__pycache__/dialogs.cpython-314.pyc b/__pycache__/dialogs.cpython-314.pyc index 5f4a612..9a467ca 100644 Binary files a/__pycache__/dialogs.cpython-314.pyc and b/__pycache__/dialogs.cpython-314.pyc differ diff --git a/__pycache__/disk_operations.cpython-314.pyc b/__pycache__/disk_operations.cpython-314.pyc index 5c5d56a..9ce4637 100644 Binary files a/__pycache__/disk_operations.cpython-314.pyc and b/__pycache__/disk_operations.cpython-314.pyc differ diff --git a/__pycache__/raid_operations.cpython-314.pyc b/__pycache__/raid_operations.cpython-314.pyc index c736bf5..ca26964 100644 Binary files a/__pycache__/raid_operations.cpython-314.pyc and b/__pycache__/raid_operations.cpython-314.pyc differ diff --git a/__pycache__/system_info.cpython-314.pyc b/__pycache__/system_info.cpython-314.pyc index f892df5..fcb119b 100644 Binary files a/__pycache__/system_info.cpython-314.pyc and b/__pycache__/system_info.cpython-314.pyc differ diff --git a/__pycache__/ui_form.cpython-314.pyc b/__pycache__/ui_form.cpython-314.pyc index 332f16e..e79d5a7 100644 Binary files a/__pycache__/ui_form.cpython-314.pyc and b/__pycache__/ui_form.cpython-314.pyc differ diff --git a/dialogs.py b/dialogs.py index ffec8ef..65337bc 100644 --- a/dialogs.py +++ b/dialogs.py @@ -388,36 +388,46 @@ class CreateLvDialog(QDialog): max_size_gb = self.vg_sizes.get(selected_vg, 0.0) # 确保 max_size_gb 至少是 spinbox 的最小值,以防卷组可用空间过小导致 UI 问题 - if max_size_gb < self.lv_size_spinbox.minimum(): - # 如果实际最大可用空间小于最小允许值,则将最小值临时调整为实际最大值 + current_min_limit = self.lv_size_spinbox.minimum() # 获取当前的最小值限制 + if max_size_gb < current_min_limit: + # 如果实际最大可用空间小于当前最小值限制,则将最小值临时调整为实际最大值 self.lv_size_spinbox.setMinimum(max_size_gb if max_size_gb > 0 else 0.01) # 至少0.01GB else: self.lv_size_spinbox.setMinimum(0.1) # 恢复正常最小值 - self.lv_size_spinbox.setMaximum(max(self.lv_size_spinbox.minimum(), max_size_gb)) # 设置最大值,确保不小于最小值 + # 计算并设置 spinbox 的最大值 + clamped_max_gb = max(self.lv_size_spinbox.minimum(), max_size_gb) + self.lv_size_spinbox.setMaximum(clamped_max_gb) # 如果选中了“使用最大可用空间”,则将 spinbox 值设置为最大值 if self.use_max_space_checkbox.isChecked(): - self.lv_size_spinbox.setValue(self.lv_size_spinbox.maximum()) + self.lv_size_spinbox.setValue(clamped_max_gb) # 使用计算出的最大值 else: # 如果当前值超过了新的最大值,则调整为新的最大值 - if self.lv_size_spinbox.value() > self.lv_size_spinbox.maximum(): - self.lv_size_spinbox.setValue(self.lv_size_spinbox.maximum()) + if self.lv_size_spinbox.value() > clamped_max_gb: + self.lv_size_spinbox.setValue(clamped_max_gb) # 如果当前值小于新的最小值,则调整 elif self.lv_size_spinbox.value() < self.lv_size_spinbox.minimum(): self.lv_size_spinbox.setValue(self.lv_size_spinbox.minimum()) - def _toggle_size_input(self, state): """根据“使用最大可用空间”复选框的状态切换大小输入框的启用/禁用状态。""" + # 获取当前选中的卷组的实际最大可用空间(GB) + selected_vg = self.vg_combo_box.currentText() + actual_max_gb = self.vg_sizes.get(selected_vg, 0.0) + + # 确保计算出的最大值不小于spinbox的最小值 + clamped_max_gb = max(self.lv_size_spinbox.minimum(), actual_max_gb) + if state == Qt.Checked: self.lv_size_spinbox.setDisabled(True) - self.lv_size_spinbox.setValue(self.lv_size_spinbox.maximum()) # 设置为最大值 + self.lv_size_spinbox.setValue(clamped_max_gb) # 使用计算出的最大值 else: self.lv_size_spinbox.setDisabled(False) # 如果之前是最大值,取消勾选后,恢复到最小值或一个合理值 - if self.lv_size_spinbox.value() == self.lv_size_spinbox.maximum(): + if self.lv_size_spinbox.value() == clamped_max_gb: # 比较时也使用计算出的最大值 self.lv_size_spinbox.setValue(self.lv_size_spinbox.minimum()) + # 否则,保持当前值(用户手动输入的值) def get_lv_info(self): diff --git a/disk_operations.py b/disk_operations.py index 803ad09..6c4007d 100644 --- a/disk_operations.py +++ b/disk_operations.py @@ -3,12 +3,13 @@ import logging import re import os from PySide6.QtWidgets import QMessageBox, QInputDialog +from system_info import SystemInfoManager logger = logging.getLogger(__name__) class DiskOperations: - def __init__(self): - pass + def __init__(self, system_manager: SystemInfoManager): # NEW: 接收 system_manager 实例 + self.system_manager = system_manager def _execute_shell_command(self, command_list, error_message, root_privilege=True, suppress_critical_dialog_on_stderr_match=None, input_data=None): @@ -18,6 +19,7 @@ class DiskOperations: :param error_message: 命令失败时显示给用户的错误消息。 :param root_privilege: 如果为 True,则使用 sudo 执行命令。 :param suppress_critical_dialog_on_stderr_match: 如果 stderr 包含此字符串,则不显示关键错误对话框。 + 可以是字符串或字符串元组。 :param input_data: 传递给命令stdin的数据 (str)。 :return: (True/False, stdout_str, stderr_str) """ @@ -50,10 +52,17 @@ class DiskOperations: logger.error(f"标准输出: {e.stdout.strip()}") logger.error(f"标准错误: {stderr_output}") - if suppress_critical_dialog_on_stderr_match and \ - (suppress_critical_dialog_on_stderr_match in stderr_output or \ - (isinstance(suppress_critical_dialog_on_stderr_match, tuple) and \ - any(s in stderr_output for s in suppress_critical_dialog_on_stderr_match))): + # Determine if the error dialog should be suppressed by this function + should_suppress_dialog_here = False + if suppress_critical_dialog_on_stderr_match: + if isinstance(suppress_critical_dialog_on_stderr_match, str): + if suppress_critical_dialog_on_stderr_match in stderr_output: + should_suppress_dialog_here = True + elif isinstance(suppress_critical_dialog_on_stderr_match, tuple): + if any(s in stderr_output for s in suppress_critical_dialog_on_stderr_match): + should_suppress_dialog_here = True + + if should_suppress_dialog_here: logger.info(f"错误信息 '{stderr_output}' 匹配抑制条件,不显示关键错误对话框。") else: QMessageBox.critical(None, "错误", f"{error_message}\n错误详情: {stderr_output}") @@ -68,162 +77,173 @@ class DiskOperations: return False, "", str(e) def _add_to_fstab(self, device_path, mount_point, fstype, uuid): - """ - 将设备的挂载信息添加到 /etc/fstab。 - 使用 UUID 识别设备以提高稳定性。 - """ - if not uuid or not mount_point or not fstype: - logger.error(f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。") - QMessageBox.warning(None, "警告", f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。") - return False + """ + 将设备的挂载信息添加到 /etc/fstab。 + 使用 UUID 识别设备以提高稳定性。 + """ + if not uuid or not mount_point or not fstype: + logger.error(f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。") + QMessageBox.warning(None, "警告", f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。") + return False - fstab_entry = f"UUID={uuid} {mount_point} {fstype} defaults 0 2" - fstab_path = "/etc/fstab" + fstab_entry = f"UUID={uuid} {mount_point} {fstype} defaults 0 2" + fstab_path = "/etc/fstab" - try: - # 检查 fstab 中是否已存在相同 UUID 或挂载点的条目 - with open(fstab_path, 'r') as f: - fstab_content = f.readlines() + try: + # 检查 fstab 中是否已存在相同 UUID 的条目 + # 使用 _execute_shell_command 来读取 fstab,尽管通常不需要 sudo + # 这里为了简化,直接读取文件,但写入时使用 _execute_shell_command + with open(fstab_path, 'r') as f: + fstab_content = f.readlines() - for line in fstab_content: - if f"UUID={uuid}" in line: - logger.warning(f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。跳过添加。") - QMessageBox.information(None, "信息", f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。") - return True # 认为成功,因为目标已达成 - # 检查挂载点是否已存在 (可能被其他设备使用) - if mount_point in line.split(): - logger.warning(f"挂载点 {mount_point} 已存在于 fstab 中。跳过添加。") - QMessageBox.information(None, "信息", f"挂载点 {mount_point} 已存在于 fstab 中。") - return True # 认为成功,因为目标已达成 + for line in fstab_content: + if f"UUID={uuid}" in line: + logger.warning(f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。跳过添加。") + QMessageBox.information(None, "信息", f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。") + return True # 认为成功,因为目标已达成 - # 如果不存在,则追加到 fstab - with open(fstab_path, 'a') as f: - f.write(fstab_entry + '\n') - logger.info(f"已将 {fstab_entry} 添加到 {fstab_path}。") - QMessageBox.information(None, "成功", f"设备 {device_path} 已成功添加到 /etc/fstab。") - return True - except Exception as e: - logger.error(f"写入 {fstab_path} 失败: {e}") - QMessageBox.critical(None, "错误", f"写入 {fstab_path} 失败: {e}") - return False + # 如果不存在,则追加到 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} 失败", + root_privilege=True + ) + if success: + logger.info(f"已将 {fstab_entry} 添加到 {fstab_path}。") + QMessageBox.information(None, "成功", f"设备 {device_path} 已成功添加到 /etc/fstab。") + return True + else: + return False # Error handled by _execute_shell_command + except Exception as e: + logger.error(f"处理 {fstab_path} 失败: {e}") + QMessageBox.critical(None, "错误", f"处理 {fstab_path} 失败: {e}") + return False def _remove_fstab_entry(self, device_path): - """ - 从 /etc/fstab 中移除指定设备的条目。 - 此方法需要 SystemInfoManager 来获取设备的 UUID。 - """ - success, stdout, stderr = self._execute_shell_command( - ["lsblk", "-no", "UUID", device_path], - f"获取设备 {device_path} 的 UUID 失败", - root_privilege=False, # lsblk 通常不需要 sudo - suppress_critical_dialog_on_stderr_match=("找不到或无法访问", "No such device or address") # 匹配中英文错误 - ) - if not success: - logger.warning(f"无法获取设备 {device_path} 的 UUID,无法从 fstab 中移除。错误: {stderr}") - return False + """ + 从 /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 中移除。") + return False + uuid = device_details.get('uuid') - uuid = stdout.strip() - if not uuid: - logger.warning(f"设备 {device_path} 没有 UUID,无法从 fstab 中移除。") - return False - - fstab_path = "/etc/fstab" - try: - with open(fstab_path, 'r') as f: - lines = f.readlines() - - new_lines = [] - removed = False - for line in lines: - if f"UUID={uuid}" in line: - logger.info(f"从 {fstab_path} 中移除了条目: {line.strip()}") - removed = True - else: - new_lines.append(line) - - if removed: - # 写入临时文件,然后替换原文件 - with open(fstab_path + ".tmp", 'w') as f_tmp: - f_tmp.writelines(new_lines) - os.rename(fstab_path + ".tmp", fstab_path) - logger.info(f"已从 {fstab_path} 中移除 UUID={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"/UUID={uuid}/d", fstab_path] + success, _, stderr = self._execute_shell_command( + command, + f"从 {fstab_path} 中删除 UUID={uuid} 的条目失败", + root_privilege=True, + suppress_critical_dialog_on_stderr_match=( + f"sed: {fstab_path}: No such file or directory", # English + f"sed: {fstab_path}: 没有那个文件或目录", # Chinese + "no changes were made" # if sed finds nothing to delete + ) + ) + if success: + logger.info(f"已从 {fstab_path} 中删除 UUID={uuid} 的条目。") return True else: - logger.info(f"fstab 中未找到 UUID={uuid} 的条目。") - return False - except Exception as e: - logger.error(f"修改 {fstab_path} 失败: {e}") - QMessageBox.critical(None, "错误", f"修改 {fstab_path} 失败: {e}") - return False + # 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}: 没有那个文件或目录", + "no changes were made", + "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 mount_partition(self, device_path, mount_point, add_to_fstab=False): - """ - 挂载指定设备到指定挂载点。 - :param device_path: 要挂载的设备路径。 - :param mount_point: 挂载点。 - :param add_to_fstab: 是否添加到 /etc/fstab。 - :return: True 如果成功,否则 False。 - """ - if not os.path.exists(mount_point): - try: - os.makedirs(mount_point) - logger.info(f"创建挂载点目录: {mount_point}") - except OSError as e: - QMessageBox.critical(None, "错误", f"创建挂载点目录 {mount_point} 失败: {e}") - logger.error(f"创建挂载点目录 {mount_point} 失败: {e}") + """ + 挂载指定设备到指定挂载点。 + :param device_path: 要挂载的设备路径。 + :param mount_point: 挂载点。 + :param add_to_fstab: 是否添加到 /etc/fstab。 + :return: True 如果成功,否则 False。 + """ + if not os.path.exists(mount_point): + try: + os.makedirs(mount_point) + logger.info(f"创建挂载点目录: {mount_point}") + except OSError as e: + QMessageBox.critical(None, "错误", f"创建挂载点目录 {mount_point} 失败: {e}") + logger.error(f"创建挂载点目录 {mount_point} 失败: {e}") + return False + + logger.info(f"尝试挂载设备 {device_path} 到 {mount_point}。") + success, _, stderr = self._execute_shell_command( + ["mount", device_path, mount_point], + f"挂载设备 {device_path} 失败" + ) + 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') + uuid = device_details.get('uuid') + if fstype and uuid: + self._add_to_fstab(device_path, mount_point, fstype, uuid) + else: + logger.error(f"无法获取设备 {device_path} 的文件系统类型或 UUID 以添加到 fstab。") + QMessageBox.warning(None, "警告", f"设备 {device_path} 已挂载,但无法获取文件系统类型或 UUID 以添加到 fstab。") + else: + logger.error(f"无法获取设备 {device_path} 的详细信息以添加到 fstab。") + QMessageBox.warning(None, "警告", f"设备 {device_path} 已挂载,但无法获取详细信息以添加到 fstab。") + return True + else: return False - logger.info(f"尝试挂载设备 {device_path} 到 {mount_point}。") - success, _, stderr = self._execute_shell_command( - ["mount", device_path, mount_point], - f"挂载设备 {device_path} 失败" - ) - if success: - QMessageBox.information(None, "成功", f"设备 {device_path} 已成功挂载到 {mount_point}。") - if add_to_fstab: - # 为了获取 fstype 和 UUID,需要再次调用 lsblk - success_details, stdout_details, stderr_details = self._execute_shell_command( - ["lsblk", "-no", "FSTYPE,UUID", device_path], - f"获取设备 {device_path} 的文件系统类型和 UUID 失败", - root_privilege=False, - suppress_critical_dialog_on_stderr_match=("找不到或无法访问", "No such device or address") - ) - if success_details: - fstype, uuid = stdout_details.strip().split() - self._add_to_fstab(device_path, mount_point, fstype, uuid) - else: - logger.error(f"无法获取设备 {device_path} 的详细信息以添加到 fstab: {stderr_details}") - QMessageBox.warning(None, "警告", f"设备 {device_path} 已挂载,但无法获取详细信息以添加到 fstab。") - return True - else: - 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}。") - try: + """ + 卸载指定设备。 + :param device_path: 要卸载的设备路径。 + :param show_dialog_on_error: 是否在发生错误时显示对话框。 + :return: True 如果成功,否则 False。 + """ + logger.info(f"尝试卸载设备 {device_path}。") + # 定义表示设备已未挂载的错误信息(中英文) + # 增加了 "未指定挂载点" + already_unmounted_errors = ("not mounted", "未挂载", "未指定挂载点") + + # 调用 _execute_shell_command,并告诉它在遇到“已未挂载”错误时,不要弹出其自身的关键错误对话框。 success, _, stderr = self._execute_shell_command( ["umount", device_path], f"卸载设备 {device_path} 失败", - suppress_critical_dialog_on_stderr_match="not mounted" if not show_dialog_on_error else None + suppress_critical_dialog_on_stderr_match=already_unmounted_errors ) + if success: + # 如果命令成功执行,则卸载成功。 QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。") return True else: - if show_dialog_on_error: - QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 失败。\n错误详情: {stderr}") - return False - except Exception as e: - logger.error(f"卸载设备 {device_path} 时发生异常: {e}") - if show_dialog_on_error: - QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 时发生异常: {e}") - 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} 已经处于未挂载状态(或挂载点未指定),无需重复卸载。") + # 这种情况下,操作结果符合预期(设备已是未挂载),不弹出对话框,并返回 True。 + return True + else: + # 对于其他类型的卸载失败(例如设备忙、权限不足等), + # _execute_shell_command 应该已经弹出了关键错误对话框 + # (因为它没有匹配到 `already_unmounted_errors` 进行抑制)。 + # 所以,这里我们不需要再次弹出对话框,直接返回 False。 + return False def get_disk_free_space_info_mib(self, disk_path, total_disk_mib): """ @@ -234,13 +254,19 @@ class DiskOperations: 如果磁盘有分区表但没有空闲空间,返回 (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} 分区信息失败", - root_privilege=True + root_privilege=True, + suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label") ) 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 @@ -294,23 +320,31 @@ class DiskOperations: # 1. 检查磁盘是否有分区表 has_partition_table = False - try: - 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: - 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'。") + # 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 else: - logger.info(f"parted print 命令失败,但可能不是因为没有分区表,错误信息: {stderr_check}") - except Exception as e: - logger.error(f"检查磁盘 {disk_path} 分区表时发生异常: {e}") - pass + logger.error(f"检查磁盘 {disk_path} 分区表时发生非预期的错误: {stderr_check}") + return False # Other critical error, stop operation. + else: # success_check is True + 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 @@ -374,12 +408,12 @@ class DiskOperations: create_cmd, f"在 {disk_path} 上创建分区失败" ) - if not success: + if success: + QMessageBox.information(None, "成功", f"在 {disk_path} 上成功创建了 {size_for_log} 的分区。") + return True + else: return False - QMessageBox.information(None, "成功", f"在 {disk_path} 上成功创建了 {size_for_log} 的分区。") - return True - def delete_partition(self, device_path): """ 删除指定分区。 @@ -394,9 +428,12 @@ class DiskOperations: return False # 尝试卸载分区 - self.unmount_partition(device_path, show_dialog_on_error=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. # 从 fstab 中移除条目 + # _remove_fstab_entry also handles "not found" gracefully. self._remove_fstab_entry(device_path) # 获取父磁盘和分区号 @@ -472,4 +509,3 @@ class DiskOperations: return True else: return False - diff --git a/form.ui b/form.ui index 4080348..7269374 100644 --- a/form.ui +++ b/form.ui @@ -14,7 +14,14 @@ Linux 存储管理工具 - + + + + + 刷新数据 + + + @@ -24,7 +31,7 @@ 块设备概览 - + @@ -130,13 +137,6 @@ - - - - 刷新数据 - - - @@ -152,7 +152,7 @@ 0 0 1000 - 23 + 30 diff --git a/mainwindow.py b/mainwindow.py index 394e7f9..58cfa40 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -35,7 +35,7 @@ class MainWindow(QMainWindow): # 初始化管理器和操作类 self.system_manager = SystemInfoManager() - self.disk_ops = DiskOperations() + self.disk_ops = DiskOperations(self.system_manager) self.raid_ops = RaidOperations() self.lvm_ops = LvmOperations() @@ -208,12 +208,12 @@ class MainWindow(QMainWindow): if dev_data.get('children'): logger.debug(f"磁盘 {disk_path} 存在现有分区,尝试计算下一个分区起始位置。") - calculated_start_mib = self.disk_ops.get_disk_next_partition_start_mib(disk_path) - if calculated_start_mib is None: + 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} 的分区起始位置。") return start_position_mib = calculated_start_mib - max_available_mib = total_disk_mib - start_position_mib + max_available_mib = largest_free_space_mib if max_available_mib < 0: max_available_mib = 0.0 else: diff --git a/raid_operations.py b/raid_operations.py index 3d05cce..a8f4e54 100644 --- a/raid_operations.py +++ b/raid_operations.py @@ -39,17 +39,18 @@ class RaidOperations: return True, stdout, stderr except subprocess.CalledProcessError as e: + stderr_output = e.stderr.strip() # 获取标准错误输出 logger.error(f"{error_msg_prefix} 命令: {full_cmd_str}") logger.error(f"退出码: {e.returncode}") logger.error(f"标准输出: {e.stdout.strip()}") - logger.error(f"标准错误: {e.stderr.strip()}") + logger.error(f"标准错误: {stderr_output}") # 根据 suppress_critical_dialog_on_stderr_match 参数决定是否弹出错误对话框 - if suppress_critical_dialog_on_stderr_match and suppress_critical_dialog_on_stderr_match in e.stderr.strip(): + if suppress_critical_dialog_on_stderr_match and suppress_critical_dialog_on_stderr_match in stderr_output: logger.info(f"特定错误 '{suppress_critical_dialog_on_stderr_match}' 匹配,已抑制错误对话框。") else: - QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: {e.stderr.strip()}") - return False, e.stdout, e.stderr + QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: {stderr_output}") + return False, e.stdout, stderr_output # 返回 stderr_output except FileNotFoundError: logger.error(f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。") QMessageBox.critical(None, "错误", f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。") @@ -59,13 +60,50 @@ class RaidOperations: QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n未知错误: {e}") return False, "", str(e) + def _get_next_available_md_device_name(self): + """ + 查找下一个可用的 /dev/mdX 设备名称(例如 /dev/md0, /dev/md1, ...)。 + """ + existing_md_numbers = set() + try: + raid_arrays = self.system_manager.get_mdadm_arrays() + for array in raid_arrays: + device_path = array.get('device') + if device_path and device_path.startswith('/dev/md'): + # 匹配 /dev/mdX 形式的设备名 + match = re.match(r'/dev/md(\d+)', device_path) + if match: + existing_md_numbers.add(int(match.group(1))) + except Exception as e: + logger.warning(f"获取现有 RAID 阵列信息失败,可能无法找到最优的下一个设备名: {e}") + + next_md_num = 0 + while next_md_num in existing_md_numbers: + next_md_num += 1 + + return f"/dev/md{next_md_num}" + def create_raid_array(self, devices, level, chunk_size): """ 创建 RAID 阵列。 :param devices: 成员设备列表,例如 ['/dev/sdb1', '/dev/sdc1'] - :param level: RAID 级别,例如 'raid0', 'raid1', 'raid5' + :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): + if level in ["0", "1", "5", "6", "10"]: # 增加 "6", "10" 确保全面 + level_str = f"raid{level}" + else: + level_str = level + else: + QMessageBox.critical(None, "错误", f"不支持的 RAID 级别类型: {type(level)}") + return False + level = level_str + # --- 标准化 RAID 级别输入结束 --- + if not devices or len(devices) < 2: QMessageBox.critical(None, "错误", "创建 RAID 阵列至少需要两个成员设备。") return False @@ -74,6 +112,17 @@ class RaidOperations: if level == "raid5" and len(devices) < 3: QMessageBox.critical(None, "错误", "RAID5 至少需要三个设备。") return False + # 补充其他 RAID 级别设备数量检查,与 dialogs.py 保持一致 + if level == "raid1" and len(devices) < 2: + QMessageBox.critical(None, "错误", "RAID1 至少需要两个设备。") + return False + if level == "raid6" and len(devices) < 4: + QMessageBox.critical(None, "错误", "RAID6 至少需要四个设备。") + return False + if level == "raid10" and len(devices) < 2: + QMessageBox.critical(None, "错误", "RAID10 至少需要两个设备。") + return False + # 确认操作 reply = QMessageBox.question(None, "确认创建 RAID 阵列", @@ -90,69 +139,80 @@ class RaidOperations: for dev in devices: # 使用 --force 选项,避免交互式提示 clear_cmd = ["mdadm", "--zero-superblock", "--force", dev] + + # 定义表示设备上没有超级块的错误信息,根据日志调整 + no_superblock_error_match = "Unrecognised md component device" + success, _, stderr = self._execute_shell_command( clear_cmd, f"清除设备 {dev} 上的旧 RAID 超级块失败", - suppress_critical_dialog_on_stderr_match="No superblocks" # 抑制“没有超级块”的错误提示 + suppress_critical_dialog_on_stderr_match=no_superblock_error_match ) - if success: + + # 检查 stderr 是否包含“未识别的 MD 组件设备”信息 + if no_superblock_error_match in stderr: + logger.info(f"设备 {dev} 上未找到旧 RAID 超级块,已确保其干净。") + elif success: logger.info(f"已清除设备 {dev} 上的旧 RAID 超级块。") else: - # 如果清除失败,但不是因为“没有超级块”,则可能需要进一步处理 - if "No superblocks" not in stderr: - QMessageBox.warning(None, "警告", f"清除设备 {dev} 上的旧 RAID 超级块可能失败,但尝试继续。") + logger.error(f"清除设备 {dev} 上的旧 RAID 超级块失败,中断 RAID 创建。") + return False # 2. 创建 RAID 阵列 - # 默认阵列名为 /dev/md/new_raid,可以考虑让用户输入 - array_name = "/dev/md/new_raid" + # --- 修改点:自动生成唯一的阵列名称 --- + array_name = self._get_next_available_md_device_name() + logger.info(f"将使用自动生成的 RAID 阵列名称: {array_name}") + # --- 修改点结束 --- + if level == "raid0": - # RAID0 至少2个设备 create_cmd = ["mdadm", "--create", array_name, "--level=raid0", f"--raid-devices={len(devices)}", f"--chunk={chunk_size}K"] + devices elif level == "raid1": - # RAID1 至少2个设备 create_cmd = ["mdadm", "--create", array_name, "--level=raid1", f"--raid-devices={len(devices)}"] + devices elif level == "raid5": - # RAID5 至少3个设备 create_cmd = ["mdadm", "--create", array_name, "--level=raid5", f"--raid-devices={len(devices)}"] + devices + elif level == "raid6": # 增加 RAID6 + create_cmd = ["mdadm", "--create", array_name, "--level=raid6", + f"--raid-devices={len(devices)}", f"--chunk={chunk_size}K"] + devices + elif level == "raid10": # 增加 RAID10 + create_cmd = ["mdadm", "--create", array_name, "--level=raid10", + f"--raid-devices={len(devices)}", f"--chunk={chunk_size}K"] + devices else: QMessageBox.critical(None, "错误", f"不支持的 RAID 级别: {level}") return False # 在 --create 命令中添加 --force 选项 - create_cmd.insert(2, "--force") # 插入到 --create 后面 + create_cmd.insert(2, "--force") - # <--- 关键修改:通过 input_to_command 参数传入 'y\n' 来强制 mdadm 接受 - if not self._execute_shell_command(create_cmd, f"创建 RAID 阵列失败", input_to_command='y\n')[0]: + success_create, stdout_create, stderr_create = self._execute_shell_command( + create_cmd, + f"创建 RAID 阵列失败", + input_to_command='y\n' # 尝试通过 stdin 传递 'y' + ) + if not success_create: + # 检查是否是由于 "Array name ... is in use already." 导致的失败 + if "Array name" in stderr_create and "is in use already" in stderr_create: + QMessageBox.critical(None, "错误", f"创建 RAID 阵列失败:阵列名称 {array_name} 已被占用。请尝试停止或删除现有阵列。") return False logger.info(f"成功创建 RAID {level} 阵列 {array_name}。") QMessageBox.information(None, "成功", f"成功创建 RAID {level} 阵列 {array_name}。") # 3. 刷新 mdadm 配置并等待阵列激活 - # 注意:这里使用 '>>' 重定向,subprocess.run 无法直接处理 shell 重定向符号 - # 需要改成先读取,再写入,或者使用 bash -c "..." - # 暂时先用 bash -c 的方式,更简单 - # 旧代码:self._execute_shell_command(["mdadm", "--examine", "--scan", ">>", "/etc/mdadm/mdadm.conf"], "更新 mdadm.conf 失败") - - # 获取新的 mdadm.conf 内容 examine_scan_cmd = ["mdadm", "--examine", "--scan"] success_scan, scan_stdout, _ = self._execute_shell_command(examine_scan_cmd, "扫描 mdadm 配置失败") if success_scan: - # 将扫描结果追加到 mdadm.conf append_to_conf_cmd = ["bash", "-c", f"echo '{scan_stdout.strip()}' >> /etc/mdadm/mdadm.conf"] if not self._execute_shell_command(append_to_conf_cmd, "更新 /etc/mdadm/mdadm.conf 失败")[0]: logger.warning("更新 /etc/mdadm/mdadm.conf 失败。") else: logger.warning("未能扫描到 mdadm 配置,跳过更新 mdadm.conf。") - - # self._execute_shell_command(["update-initramfs", "-u"], "更新 initramfs 失败") - return True + def stop_raid_array(self, array_path): """ 停止一个 RAID 阵列。 @@ -173,7 +233,9 @@ class RaidOperations: # 暂时先直接调用 umount 命令,不处理 fstab # 或者,更好的方式是让 MainWindow 调用 disk_ops.unmount_partition # 此处简化处理,只执行 umount 命令 - self._execute_shell_command(["umount", array_path], f"尝试卸载 {array_path} 失败", suppress_critical_dialog_on_stderr_match="not mounted") + # 这里的 suppress_critical_dialog_on_stderr_match 应该与 DiskOperations 中的定义保持一致 + self._execute_shell_command(["umount", array_path], f"尝试卸载 {array_path} 失败", + suppress_critical_dialog_on_stderr_match="not mounted") # 仅抑制英文,因为这里没有访问 DiskOperations 的 already_unmounted_errors if not self._execute_shell_command(["mdadm", "--stop", array_path], f"停止 RAID 阵列 {array_path} 失败")[0]: return False @@ -209,9 +271,24 @@ class RaidOperations: for dev in member_devices: # 使用 --force 选项,避免交互式提示 clear_cmd = ["mdadm", "--zero-superblock", "--force", dev] - if not self._execute_shell_command(clear_cmd, f"清除设备 {dev} 上的 RAID 超级块失败")[0]: + + # 定义表示设备上没有超级块的错误信息,根据日志调整 + no_superblock_error_match = "Unrecognised md component device" + + success, _, stderr = self._execute_shell_command( + clear_cmd, + f"清除设备 {dev} 上的 RAID 超级块失败", + suppress_critical_dialog_on_stderr_match=no_superblock_error_match + ) + + if no_superblock_error_match in stderr: + logger.info(f"设备 {dev} 上未找到旧 RAID 超级块,已确保其干净。") + elif success: + logger.info(f"已清除设备 {dev} 上的旧 RAID 超级块。") + else: success_all_cleared = False - QMessageBox.warning(None, "警告", f"未能清除设备 {dev} 上的 RAID 超级块,请手动检查。") + # 错误对话框已由 _execute_shell_command 弹出,这里只记录警告 + logger.warning(f"未能清除设备 {dev} 上的 RAID 超级块,请手动检查。") if success_all_cleared: logger.info(f"成功删除 RAID 阵列 {array_path} 并清除了成员设备超级块。") diff --git a/system_info.py b/system_info.py index f9bf584..b264e40 100644 --- a/system_info.py +++ b/system_info.py @@ -3,7 +3,7 @@ import subprocess import json import logging import re -import os # 导入 os 模块 +import os logger = logging.getLogger(__name__) @@ -82,7 +82,7 @@ class SystemInfoManager: Helper to find device data by its path recursively. Added type check for target_path. """ - if not isinstance(target_path, str): # 添加类型检查 + if not isinstance(target_path, str): logger.warning(f"传入 _find_device_by_path_recursive 的 target_path 不是字符串: {target_path} (类型: {type(target_path)})") return None for dev in dev_list: @@ -94,42 +94,109 @@ class SystemInfoManager: return found return None + def _find_device_by_maj_min_recursive(self, dev_list, target_maj_min): + """ + Helper to find device data by its major:minor number recursively. + """ + if not isinstance(target_maj_min, str): + logger.warning(f"传入 _find_device_by_maj_min_recursive 的 target_maj_min 不是字符串: {target_maj_min} (类型: {type(target_maj_min)})") + return None + for dev in dev_list: + if dev.get('maj:min') == target_maj_min: + return dev + if 'children' in dev: + found = self._find_device_by_maj_min_recursive(dev['children'], target_maj_min) + if found: + return found + return None + def get_mountpoint_for_device(self, device_path): """ 根据设备路径获取其挂载点。 - 此方法现在可以正确处理 /dev/md/new_raid 这样的 RAID 阵列别名。 - Added type check for device_path. + 此方法现在可以正确处理 /dev/md/new_raid 这样的 RAID 阵列别名,以及 LVM 逻辑卷的符号链接。 """ - if not isinstance(device_path, str): # 添加类型检查 + if not isinstance(device_path, str): logger.warning(f"传入 get_mountpoint_for_device 的 device_path 不是字符串: {device_path} (类型: {type(device_path)})") return None + original_device_path = device_path + logger.debug(f"get_mountpoint_for_device: 正在处理原始设备路径: {original_device_path}") + devices = self.get_block_devices() + logger.debug(f"get_mountpoint_for_device: 从 lsblk 获取到 {len(devices)} 个块设备信息。") + + target_maj_min = None + current_search_path = original_device_path + + # 1. 解析符号链接以获取实际路径,并尝试获取 maj:min + if original_device_path.startswith('/dev/') and os.path.exists(original_device_path): + try: + # 先尝试解析真实路径,因为 stat 可能对符号链接返回链接本身的 maj:min + resolved_path = os.path.realpath(original_device_path) + logger.debug(f"get_mountpoint_for_device: 原始设备路径 {original_device_path} 解析为 {resolved_path}") + current_search_path = resolved_path # 使用解析后的路径进行 stat 和后续查找 + + # 获取解析后路径的 maj:min + stat_output, _ = self._run_command(["stat", "-c", "%t:%T", resolved_path], check_output=True) + raw_maj_min = stat_output.strip() # e.g., "fc:0" + + # MODIFIED: Convert hex major to decimal + if ':' in raw_maj_min: + major_hex, minor_dec = raw_maj_min.split(':') + try: + major_dec = str(int(major_hex, 16)) # Convert hex to decimal string + target_maj_min = f"{major_dec}:{minor_dec}" # e.g., "252:0" + logger.debug(f"get_mountpoint_for_device: 解析后设备 {resolved_path} 的 maj:min (hex) 为 {raw_maj_min},转换为 decimal 为 {target_maj_min}") + except ValueError: + logger.warning(f"get_mountpoint_for_device: 无法将 maj:min 的主要部分 '{major_hex}' 从十六进制转换为十进制。") + target_maj_min = None # Fallback if conversion fails + else: + logger.warning(f"get_mountpoint_for_device: stat 输出 '{raw_maj_min}' 格式不符合预期。") + target_maj_min = None + + except subprocess.CalledProcessError as e: + logger.debug(f"get_mountpoint_for_device: 无法获取 {resolved_path} 的 maj:min (命令失败): {e.stderr.strip()}") + except Exception as e: + logger.debug(f"get_mountpoint_for_device: 无法获取 {resolved_path} 的 maj:min (未知错误): {e}") + elif original_device_path.startswith('/dev/') and not os.path.exists(original_device_path): + logger.warning(f"get_mountpoint_for_device: 设备路径 {original_device_path} 不存在,无法获取挂载点。") + return None + + # 2. 尝试使用 (可能已解析的) current_search_path 在 lsblk 输出中查找 + dev_info = self._find_device_by_path_recursive(devices, current_search_path) + logger.debug(f"get_mountpoint_for_device: _find_device_by_path_recursive 查找结果 (使用 {current_search_path}): {dev_info}") - # 1. 尝试直接使用提供的 device_path 在 lsblk 输出中查找 - dev_info = self._find_device_by_path_recursive(devices, device_path) if dev_info: - logger.debug(f"直接从 lsblk 获取到 {device_path} 的挂载点: {dev_info.get('mountpoint')}") - return dev_info.get('mountpoint') + mountpoint = dev_info.get('mountpoint') + logger.debug(f"get_mountpoint_for_device: 直接从 lsblk 获取到 {original_device_path} (解析为 {current_search_path}) 的挂载点: {mountpoint}") + return mountpoint - # 2. 如果直接查找失败,并且是 RAID 阵列(例如 /dev/md/new_raid) - if device_path.startswith('/dev/md'): # 此处 device_path 已通过类型检查 - logger.debug(f"处理 RAID 阵列 {device_path} 以获取挂载点...") + # 3. 如果直接路径查找失败,并且我们有 maj:min,尝试通过 maj:min 查找 + if target_maj_min: + logger.debug(f"get_mountpoint_for_device: 直接路径查找失败,尝试通过 maj:min ({target_maj_min}) 查找。") + dev_info_by_maj_min = self._find_device_by_maj_min_recursive(devices, target_maj_min) + logger.debug(f"get_mountpoint_for_device: 通过 maj:min 查找结果: {dev_info_by_maj_min}") + if dev_info_by_maj_min: + mountpoint = dev_info_by_maj_min.get('mountpoint') + logger.debug(f"get_mountpoint_for_device: 通过 maj:min 找到 {original_device_path} 的挂载点: {mountpoint}") + return mountpoint - # 获取 RAID 阵列的实际内核设备路径(例如 /dev/md127) - actual_md_device_path = self._get_actual_md_device_path(device_path) - logger.debug(f"RAID 阵列 {device_path} 的实际设备路径: {actual_md_device_path}") + # 4. 如果仍然查找失败,并且是 RAID 阵列(例如 /dev/md/new_raid) + if original_device_path.startswith('/dev/md'): + logger.debug(f"get_mountpoint_for_device: 正在处理 RAID 阵列 {original_device_path} 以获取挂载点...") + actual_md_device_path = self._get_actual_md_device_path(original_device_path) + logger.debug(f"get_mountpoint_for_device: RAID 阵列 {original_device_path} 的实际设备路径: {actual_md_device_path}") if actual_md_device_path: - # 现在,使用实际的内核设备路径从 lsblk 中查找挂载点 actual_dev_info = self._find_device_by_path_recursive(devices, actual_md_device_path) - logger.debug(f"实际设备路径 {actual_md_device_path} 的 lsblk 详情: {actual_dev_info}") + logger.debug(f"get_mountpoint_for_device: 实际设备路径 {actual_md_device_path} 的 lsblk 详情: {actual_dev_info}") if actual_dev_info: - logger.debug(f"在实际设备 {actual_md_device_path} 上找到了挂载点: {actual_dev_info.get('mountpoint')}") - return actual_dev_info.get('mountpoint') + mountpoint = actual_dev_info.get('mountpoint') + logger.debug(f"get_mountpoint_for_device: 在实际设备 {actual_md_device_path} 上找到了挂载点: {mountpoint}") + return mountpoint - logger.debug(f"未能获取到 {device_path} 的挂载点。") + logger.debug(f"get_mountpoint_for_device: 未能获取到 {original_device_path} 的挂载点。") return None def get_mdadm_arrays(self): @@ -269,9 +336,10 @@ class SystemInfoManager: except Exception as e: logger.error(f"获取LVM卷组信息失败: {e}") - # Get LVs + # Get LVs (MODIFIED: added -o lv_path) try: - stdout, _ = self._run_command(["lvs", "--reportformat", "json"]) + # 明确请求 lv_path,因为默认的 --reportformat json 不包含它 + stdout, _ = self._run_command(["lvs", "-o", "lv_name,vg_name,lv_uuid,lv_size,lv_attr,origin,snap_percent,lv_path", "--reportformat", "json"]) data = json.loads(stdout) if 'report' in data and data['report']: for lv_data in data['report'][0].get('lv', []): @@ -283,7 +351,7 @@ class SystemInfoManager: 'lv_attr': lv_data.get('lv_attr'), 'origin': lv_data.get('origin'), 'snap_percent': lv_data.get('snap_percent'), - 'lv_path': lv_data.get('lv_path') + 'lv_path': lv_data.get('lv_path') # 现在应该能正确获取到路径了 }) except subprocess.CalledProcessError as e: if "No logical volume found" in e.stderr or "No logical volumes found" in e.stdout: @@ -358,7 +426,7 @@ class SystemInfoManager: 通过检查 /dev/md/ 目录下的符号链接来获取。 Added type check for array_path. """ - if not isinstance(array_path, str): # 添加类型检查 + if not isinstance(array_path, str): logger.warning(f"传入 _get_actual_md_device_path 的 array_path 不是字符串: {array_path} (类型: {type(array_path)})") return None @@ -378,63 +446,103 @@ class SystemInfoManager: def get_device_details_by_path(self, device_path): """ 根据设备路径获取设备的 UUID 和文件系统类型 (fstype)。 - 此方法现在可以正确处理 /dev/md/new_raid 这样的 RAID 阵列别名。 + 此方法现在可以正确处理 /dev/md/new_raid 这样的 RAID 阵列别名,以及 LVM 逻辑卷的符号链接。 Added type check for device_path. :param device_path: 设备的路径,例如 '/dev/sdb1' 或 '/dev/md0' 或 '/dev/md/new_raid' :return: 包含 'uuid' 和 'fstype' 的字典,如果未找到则返回 None。 """ - if not isinstance(device_path, str): # 添加类型检查 + if not isinstance(device_path, str): logger.warning(f"传入 get_device_details_by_path 的 device_path 不是字符串: {device_path} (类型: {type(device_path)})") return None + original_device_path = device_path + logger.debug(f"get_device_details_by_path: 正在处理原始设备路径: {original_device_path}") + devices = self.get_block_devices() # 获取所有块设备信息 + logger.debug(f"get_device_details_by_path: 从 lsblk 获取到 {len(devices)} 个块设备信息。") - # 1. 首先,尝试直接使用提供的 device_path 在 lsblk 输出中查找 - # 对于 /dev/md/new_raid 这样的别名,这一步通常会返回 None - lsblk_details = self._find_device_by_path_recursive(devices, device_path) - logger.debug(f"lsblk_details for {device_path} (direct lookup): {lsblk_details}") + target_maj_min = None + current_search_path = original_device_path - if lsblk_details and lsblk_details.get('fstype'): # 即使没有UUID,有fstype也算找到部分信息 - # 如果直接找到了 fstype,就返回 lsblk 提供的 UUID 和 fstype - # 这里的 UUID 应该是文件系统 UUID - logger.debug(f"直接从 lsblk 获取到 {device_path} 的详情: {lsblk_details}") + # 1. 解析符号链接以获取实际路径,并尝试获取 maj:min + if original_device_path.startswith('/dev/') and os.path.exists(original_device_path): + try: + resolved_path = os.path.realpath(original_device_path) + logger.debug(f"get_device_details_by_path: 原始设备路径 {original_device_path} 解析为 {resolved_path}") + current_search_path = resolved_path + + stat_output, _ = self._run_command(["stat", "-c", "%t:%T", resolved_path], check_output=True) + raw_maj_min = stat_output.strip() # e.g., "fc:0" + + # MODIFIED: Convert hex major to decimal + if ':' in raw_maj_min: + major_hex, minor_dec = raw_maj_min.split(':') + try: + major_dec = str(int(major_hex, 16)) # Convert hex to decimal string + target_maj_min = f"{major_dec}:{minor_dec}" # e.g., "252:0" + logger.debug(f"get_device_details_by_path: 解析后设备 {resolved_path} 的 maj:min (hex) 为 {raw_maj_min},转换为 decimal 为 {target_maj_min}") + except ValueError: + logger.warning(f"get_device_details_by_path: 无法将 maj:min 的主要部分 '{major_hex}' 从十六进制转换为十进制。") + target_maj_min = None + else: + logger.warning(f"get_device_details_by_path: stat 输出 '{raw_maj_min}' 格式不符合预期。") + target_maj_min = None + + except subprocess.CalledProcessError as e: + logger.debug(f"get_device_details_by_path: 无法获取 {resolved_path} 的 maj:min (命令失败): {e.stderr.strip()}") + except Exception as e: + logger.debug(f"get_device_details_by_path: 无法获取 {resolved_path} 的 maj:min (未知错误): {e}") + elif original_device_path.startswith('/dev/') and not os.path.exists(original_device_path): + logger.warning(f"get_device_details_by_path: 设备路径 {original_device_path} 不存在,无法获取详情。") + return None + + # 2. 首先,尝试使用 (可能已解析的) current_search_path 在 lsblk 输出中查找 + lsblk_details = self._find_device_by_path_recursive(devices, current_search_path) + logger.debug(f"get_device_details_by_path: _find_device_by_path_recursive 查找结果 (使用 {current_search_path}, 直接查找): {lsblk_details}") + + if lsblk_details and lsblk_details.get('fstype'): + logger.debug(f"get_device_details_by_path: 直接从 lsblk 获取到 {original_device_path} (解析为 {current_search_path}) 的详情: {lsblk_details}") return { 'uuid': lsblk_details.get('uuid'), 'fstype': lsblk_details.get('fstype') } - # 2. 如果直接查找失败,并且是 RAID 阵列(例如 /dev/md/new_raid) - if device_path.startswith('/dev/md'): # 此处 device_path 已通过类型检查 - logger.debug(f"处理 RAID 阵列 {device_path}...") + # 3. 如果直接路径查找失败,并且我们有 maj:min,尝试通过 maj:min 查找 + if target_maj_min: + logger.debug(f"get_device_details_by_path: 直接路径查找失败,尝试通过 maj:min ({target_maj_min}) 查找。") + dev_info_by_maj_min = self._find_device_by_maj_min_recursive(devices, target_maj_min) + logger.debug(f"get_device_details_by_path: 通过 maj:min 查找结果: {dev_info_by_maj_min}") + if dev_info_by_maj_min and dev_info_by_maj_min.get('fstype'): + logger.debug(f"get_device_details_by_path: 通过 maj:min 找到 {original_device_path} 的文件系统详情: {dev_info_by_maj_min}") + return { + 'uuid': dev_info_by_maj_min.get('uuid'), + 'fstype': dev_info_by_maj_min.get('fstype') + } - # 获取 RAID 阵列的实际内核设备路径(例如 /dev/md127) - actual_md_device_path = self._get_actual_md_device_path(device_path) - logger.debug(f"RAID 阵列 {device_path} 的实际设备路径: {actual_md_device_path}") + # 4. 如果仍然没有找到,并且是 RAID 阵列(例如 /dev/md/new_raid) + if original_device_path.startswith('/dev/md'): + logger.debug(f"get_device_details_by_path: 正在处理 RAID 阵列 {original_device_path}...") + + actual_md_device_path = self._get_actual_md_device_path(original_device_path) + logger.debug(f"get_device_details_by_path: RAID 阵列 {original_device_path} 的实际设备路径: {actual_md_device_path}") if actual_md_device_path: - # 现在,使用实际的内核设备路径从 lsblk 中查找 fstype 和 UUID (文件系统 UUID) actual_device_lsblk_details = self._find_device_by_path_recursive(devices, actual_md_device_path) - logger.debug(f"实际设备路径 {actual_md_device_path} 的 lsblk 详情: {actual_device_lsblk_details}") + logger.debug(f"get_device_details_by_path: 实际设备路径 {actual_md_device_path} 的 lsblk 详情: {actual_device_lsblk_details}") if actual_device_lsblk_details and actual_device_lsblk_details.get('fstype'): - # 找到了实际 RAID 设备上的文件系统信息 - # 此时的 UUID 是文件系统 UUID,fstype 是文件系统类型 - logger.debug(f"在实际设备 {actual_md_device_path} 上找到了文件系统详情: {actual_device_lsblk_details}") + logger.debug(f"get_device_details_by_path: 在实际设备 {actual_md_device_path} 上找到了文件系统详情: {actual_device_lsblk_details}") return { 'uuid': actual_device_lsblk_details.get('uuid'), 'fstype': actual_device_lsblk_details.get('fstype') } else: - # RAID 设备存在,但 lsblk 没有报告文件系统 (例如,尚未格式化) - # 此时 fstype 为 None。如果需要,我们可以返回 RAID 阵列本身的 UUID,但 fstype 仍为 None - logger.warning(f"RAID 阵列 {device_path} (实际设备 {actual_md_device_path}) 未找到文件系统类型。") - # 对于 fstab,如果没有 fstype,就无法创建条目。 - # 此时返回 None,让调用者知道无法写入 fstab。 + logger.warning(f"get_device_details_by_path: RAID 阵列 {original_device_path} (实际设备 {actual_md_device_path}) 未找到文件系统类型。") return None else: - logger.warning(f"无法确定 RAID 阵列 {device_path} 的实际内核设备路径。") - return None # 无法解析实际设备路径,也无法获取 fstype + logger.warning(f"get_device_details_by_path: 无法确定 RAID 阵列 {original_device_path} 的实际内核设备路径。") + return None - # 3. 如果仍然没有找到,返回 None - logger.debug(f"未能获取到 {device_path} 的任何详情。") + # 5. 如果仍然没有找到,返回 None + logger.debug(f"get_device_details_by_path: 未能获取到 {original_device_path} 的任何详情。") return None diff --git a/ui_form.py b/ui_form.py index 3efa696..783dd25 100644 --- a/ui_form.py +++ b/ui_form.py @@ -27,18 +27,23 @@ class Ui_MainWindow(object): MainWindow.resize(1000, 700) self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") - self.verticalLayout = QVBoxLayout(self.centralwidget) - self.verticalLayout.setObjectName(u"verticalLayout") + self.verticalLayout_2 = QVBoxLayout(self.centralwidget) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.refreshButton = QPushButton(self.centralwidget) + self.refreshButton.setObjectName(u"refreshButton") + + self.verticalLayout_2.addWidget(self.refreshButton) + self.tabWidget = QTabWidget(self.centralwidget) self.tabWidget.setObjectName(u"tabWidget") self.tab_block_devices = QWidget() self.tab_block_devices.setObjectName(u"tab_block_devices") - self.verticalLayout_2 = QVBoxLayout(self.tab_block_devices) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.verticalLayout = QVBoxLayout(self.tab_block_devices) + self.verticalLayout.setObjectName(u"verticalLayout") self.treeWidget_block_devices = QTreeWidget(self.tab_block_devices) self.treeWidget_block_devices.setObjectName(u"treeWidget_block_devices") - self.verticalLayout_2.addWidget(self.treeWidget_block_devices) + self.verticalLayout.addWidget(self.treeWidget_block_devices) self.tabWidget.addTab(self.tab_block_devices, "") self.tab_raid = QWidget() @@ -62,23 +67,20 @@ class Ui_MainWindow(object): self.tabWidget.addTab(self.tab_lvm, "") - self.verticalLayout.addWidget(self.tabWidget) - - self.refreshButton = QPushButton(self.centralwidget) - self.refreshButton.setObjectName(u"refreshButton") - - self.verticalLayout.addWidget(self.refreshButton) + self.verticalLayout_2.addWidget(self.tabWidget) self.logOutputTextEdit = QTextEdit(self.centralwidget) self.logOutputTextEdit.setObjectName(u"logOutputTextEdit") self.logOutputTextEdit.setReadOnly(True) - self.verticalLayout.addWidget(self.logOutputTextEdit) + self.verticalLayout_2.addWidget(self.logOutputTextEdit) + self.verticalLayout_2.setStretch(1, 4) + self.verticalLayout_2.setStretch(2, 1) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(MainWindow) self.menubar.setObjectName(u"menubar") - self.menubar.setGeometry(QRect(0, 0, 1000, 23)) + self.menubar.setGeometry(QRect(0, 0, 1000, 30)) MainWindow.setMenuBar(self.menubar) self.statusbar = QStatusBar(MainWindow) self.statusbar.setObjectName(u"statusbar") @@ -94,6 +96,7 @@ class Ui_MainWindow(object): def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Linux \u5b58\u50a8\u7ba1\u7406\u5de5\u5177", None)) + self.refreshButton.setText(QCoreApplication.translate("MainWindow", u"\u5237\u65b0\u6570\u636e", None)) ___qtreewidgetitem = self.treeWidget_block_devices.headerItem() ___qtreewidgetitem.setText(12, QCoreApplication.translate("MainWindow", u"\u7236\u8bbe\u5907\u540d", None)); ___qtreewidgetitem.setText(11, QCoreApplication.translate("MainWindow", u"\u4e3b\u6b21\u53f7", None)); @@ -115,6 +118,5 @@ class Ui_MainWindow(object): ___qtreewidgetitem2 = self.treeWidget_lvm.headerItem() ___qtreewidgetitem2.setText(0, QCoreApplication.translate("MainWindow", u"1", None)); self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_lvm), QCoreApplication.translate("MainWindow", u"LVM \u7ba1\u7406", None)) - self.refreshButton.setText(QCoreApplication.translate("MainWindow", u"\u5237\u65b0\u6570\u636e", None)) # retranslateUi