fix bug45

This commit is contained in:
zj
2026-02-02 22:31:02 +08:00
parent ac809c08df
commit 3eecfc4db0
13 changed files with 651 additions and 290 deletions

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE QtCreatorProject>
<!-- Written by QtCreator 18.0.2, 2026-02-02T03:36:12. -->
<!-- Written by QtCreator 18.0.2, 2026-02-02T21:58:57. -->
<qtcreator>
<data>
<variable>EnvironmentId</variable>
@@ -99,13 +99,15 @@
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">{2b4075d3-005b-46f6-8fd5-3bd8a94071db}</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveBuildConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">2</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.0">
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2</value>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">Python.PysideBuildStep</value>
<value type="QString" key="Python.PySideProjectTool">/home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2/bin/pyside6-project</value>
<value type="QString" key="Python.PySideUic">/home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2/bin/pyside6-uic</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">构建</value>
@@ -126,7 +128,7 @@
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Python 3.14.2 Virtual Environment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">Python.PySideBuildConfiguration</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">2</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
@@ -224,7 +226,70 @@
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">4</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.4">
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
<valuelist type="QVariantList" key="CustomOutputParsers"/>
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">dialogs.py</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">PythonEditor.RunConfiguration.</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">/home/jing/qtpj/diskmanager/dialogs.py</value>
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.UniqueId"></value>
<value type="QString" key="PythonEditor.RunConfiguation.Script">/home/jing/qtpj/diskmanager/dialogs.py</value>
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.5">
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
<valuelist type="QVariantList" key="CustomOutputParsers"/>
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">raid_operations.py</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">PythonEditor.RunConfiguration.</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">/home/jing/qtpj/diskmanager/raid_operations.py</value>
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.UniqueId"></value>
<value type="QString" key="PythonEditor.RunConfiguation.Script">/home/jing/qtpj/diskmanager/raid_operations.py</value>
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.6">
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
<valuelist type="QVariantList" key="CustomOutputParsers"/>
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">lvm_operations.py</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">PythonEditor.RunConfiguration.</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">/home/jing/qtpj/diskmanager/lvm_operations.py</value>
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.UniqueId"></value>
<value type="QString" key="PythonEditor.RunConfiguation.Script">/home/jing/qtpj/diskmanager/lvm_operations.py</value>
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">7</value>
<value type="QString" key="python">/home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2/bin/python</value>
<value type="QString" key="venv">/home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2</value>
</valuemap>
@@ -326,7 +391,70 @@
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">4</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.4">
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
<valuelist type="QVariantList" key="CustomOutputParsers"/>
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">dialogs.py</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">PythonEditor.RunConfiguration.</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">/home/jing/qtpj/diskmanager/dialogs.py</value>
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.UniqueId"></value>
<value type="QString" key="PythonEditor.RunConfiguation.Script">/home/jing/qtpj/diskmanager/dialogs.py</value>
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.5">
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
<valuelist type="QVariantList" key="CustomOutputParsers"/>
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">raid_operations.py</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">PythonEditor.RunConfiguration.</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">/home/jing/qtpj/diskmanager/raid_operations.py</value>
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.UniqueId"></value>
<value type="QString" key="PythonEditor.RunConfiguation.Script">/home/jing/qtpj/diskmanager/raid_operations.py</value>
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.6">
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
<valuelist type="QVariantList" key="CustomOutputParsers"/>
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">lvm_operations.py</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">PythonEditor.RunConfiguration.</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">/home/jing/qtpj/diskmanager/lvm_operations.py</value>
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.UniqueId"></value>
<value type="QString" key="PythonEditor.RunConfiguation.Script">/home/jing/qtpj/diskmanager/lvm_operations.py</value>
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/jing/qtpj/diskmanager</value>
<value type="QString" key="RunConfiguration.X11Forwarding">:0</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">7</value>
</valuemap>
</data>
<data>

Binary file not shown.

Binary file not shown.

View File

@@ -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):

View File

@@ -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

20
form.ui
View File

@@ -14,7 +14,14 @@
<string>Linux 存储管理工具</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,4,1">
<item>
<widget class="QPushButton" name="refreshButton">
<property name="text">
<string>刷新数据</string>
</property>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
@@ -24,7 +31,7 @@
<attribute name="title">
<string>块设备概览</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeWidget" name="treeWidget_block_devices">
<column>
@@ -130,13 +137,6 @@
</widget>
</widget>
</item>
<item>
<widget class="QPushButton" name="refreshButton">
<property name="text">
<string>刷新数据</string>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="logOutputTextEdit">
<property name="readOnly">
@@ -152,7 +152,7 @@
<x>0</x>
<y>0</y>
<width>1000</width>
<height>23</height>
<height>30</height>
</rect>
</property>
</widget>

View File

@@ -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:

View File

@@ -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} 并清除了成员设备超级块。")

View File

@@ -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 是文件系统 UUIDfstype 是文件系统类型
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

View File

@@ -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