添加解除占用功能
This commit is contained in:
171
mainwindow.py
171
mainwindow.py
@@ -6,7 +6,7 @@ import os
|
||||
|
||||
from PySide6.QtWidgets import (QApplication, QMainWindow, QTreeWidgetItem,
|
||||
QMessageBox, QHeaderView, QMenu, QInputDialog, QDialog)
|
||||
from PySide6.QtCore import Qt, QPoint, QThread # <--- 确保导入 QThread
|
||||
from PySide6.QtCore import Qt, QPoint, QThread
|
||||
|
||||
# 导入自动生成的 UI 文件
|
||||
from ui_form import Ui_MainWindow
|
||||
@@ -20,6 +20,8 @@ from disk_operations import DiskOperations
|
||||
from raid_operations import RaidOperations
|
||||
# 导入 LVM 操作模块
|
||||
from lvm_operations import LvmOperations
|
||||
# 导入新的 resolver 类
|
||||
from occupation_resolver import OccupationResolver
|
||||
# 导入自定义对话框
|
||||
from dialogs import (CreatePartitionDialog, MountDialog, CreateRaidDialog,
|
||||
CreatePvDialog, CreateVgDialog, CreateLvDialog)
|
||||
@@ -34,13 +36,13 @@ class MainWindow(QMainWindow):
|
||||
setup_logging(self.ui.logOutputTextEdit)
|
||||
logger.info("应用程序启动。")
|
||||
|
||||
# 初始化管理器和操作类
|
||||
# 初始化管理器和操作类 (恢复原始的初始化顺序和依赖关系)
|
||||
self.system_manager = SystemInfoManager()
|
||||
# Correct order for dependency injection:
|
||||
self.lvm_ops = LvmOperations() # Initialize LVM first
|
||||
self.disk_ops = DiskOperations(self.system_manager, self.lvm_ops) # DiskOperations needs system_manager and lvm_ops
|
||||
self.lvm_ops.disk_ops = self.disk_ops # Inject disk_ops into lvm_ops
|
||||
self.raid_ops = RaidOperations(self.system_manager, self.disk_ops) # RaidOperations needs system_manager and disk_ops
|
||||
self.lvm_ops = LvmOperations()
|
||||
# DiskOperations 仍然需要 lvm_ops 的 _execute_shell_command 方法
|
||||
self.disk_ops = DiskOperations(self.system_manager, self.lvm_ops)
|
||||
# RaidOperations 仍然需要 system_manager
|
||||
self.raid_ops = RaidOperations(self.system_manager)
|
||||
|
||||
|
||||
# 连接刷新按钮的信号到槽函数
|
||||
@@ -59,10 +61,16 @@ class MainWindow(QMainWindow):
|
||||
self.ui.treeWidget_lvm.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.ui.treeWidget_lvm.customContextMenuRequested.connect(self.show_lvm_context_menu)
|
||||
|
||||
# <--- 新增: 连接 DiskOperations 的格式化完成信号
|
||||
# 连接 DiskOperations 的格式化完成信号
|
||||
self.disk_ops.formatting_finished.connect(self.on_disk_formatting_finished)
|
||||
# 可选:连接格式化开始信号,用于显示进度或禁用相关操作
|
||||
# self.disk_ops.formatting_started.connect(self.on_disk_formatting_started)
|
||||
|
||||
# 实例化 OccupationResolver
|
||||
# 将 lvm_ops._execute_shell_command 和 system_manager.get_mountpoint_for_device 传递给它
|
||||
# 确保 lvm_ops 和 system_manager 已经被正确初始化
|
||||
self.occupation_resolver = OccupationResolver(
|
||||
shell_executor_func=self.lvm_ops._execute_shell_command,
|
||||
mount_info_getter_func=self.system_manager.get_mountpoint_for_device
|
||||
)
|
||||
|
||||
# 初始化时刷新所有数据
|
||||
self.refresh_all_info()
|
||||
@@ -158,10 +166,8 @@ class MainWindow(QMainWindow):
|
||||
if device_type == 'disk':
|
||||
create_partition_action = menu.addAction(f"创建分区 {device_path}...")
|
||||
create_partition_action.triggered.connect(lambda: self._handle_create_partition(device_path, dev_data))
|
||||
# --- 新增功能:擦除分区表 ---
|
||||
wipe_action = menu.addAction(f"擦除分区表 {device_path}...")
|
||||
wipe_action.triggered.connect(lambda: self._handle_wipe_partition_table(device_path))
|
||||
# --- 新增功能结束 ---
|
||||
menu.addSeparator()
|
||||
|
||||
if device_type == 'part':
|
||||
@@ -178,22 +184,70 @@ class MainWindow(QMainWindow):
|
||||
|
||||
format_action = menu.addAction(f"格式化分区 {device_path}...")
|
||||
format_action.triggered.connect(lambda: self._handle_format_partition(device_path))
|
||||
menu.addSeparator() # Add separator before new occupation options
|
||||
|
||||
# --- 新增:解除设备占用选项 ---
|
||||
# This option is available for any disk or partition that might be occupied
|
||||
if device_type in ['disk', 'part']:
|
||||
resolve_occupation_action = menu.addAction(f"解除占用 {device_path}")
|
||||
resolve_occupation_action.triggered.connect(lambda: self._handle_resolve_device_occupation(device_path))
|
||||
|
||||
# --- 新增:关闭交换分区选项 ---
|
||||
# This option is only available for active swap partitions
|
||||
if device_type == 'part' and mount_point == '[SWAP]':
|
||||
deactivate_swap_action = menu.addAction(f"关闭交换分区 {device_path}")
|
||||
deactivate_swap_action.triggered.connect(lambda: self._handle_deactivate_swap(device_path))
|
||||
|
||||
if menu.actions():
|
||||
menu.exec(self.ui.treeWidget_block_devices.mapToGlobal(pos))
|
||||
else:
|
||||
logger.info("右键点击了空白区域或设备没有可用的操作。")
|
||||
|
||||
# --- 新增方法:处理擦除分区表 ---
|
||||
# --- 新增:处理解除设备占用 ---
|
||||
def _handle_resolve_device_occupation(self, device_path: str) -> bool:
|
||||
"""
|
||||
处理设备占用问题,通过调用 OccupationResolver 进行全面检查和修复。
|
||||
"""
|
||||
success = self.occupation_resolver.resolve_occupation(device_path)
|
||||
if success:
|
||||
# 如果成功解除占用,刷新 UI 显示最新的设备状态
|
||||
self.refresh_all_info()
|
||||
return success
|
||||
|
||||
# --- 新增:处理关闭交换分区 ---
|
||||
def _handle_deactivate_swap(self, device_path):
|
||||
"""
|
||||
处理关闭交换分区的操作。
|
||||
"""
|
||||
reply = QMessageBox.question(
|
||||
None,
|
||||
"确认关闭交换分区",
|
||||
f"您确定要关闭交换分区 {device_path} 吗?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
if reply == QMessageBox.No:
|
||||
logger.info(f"用户取消了关闭交换分区 {device_path}。")
|
||||
return False
|
||||
|
||||
logger.info(f"尝试关闭交换分区 {device_path}。")
|
||||
success, _, stderr = self.lvm_ops._execute_shell_command(
|
||||
["swapoff", device_path],
|
||||
f"关闭交换分区 {device_path} 失败",
|
||||
show_dialog=True
|
||||
)
|
||||
if success:
|
||||
QMessageBox.information(None, "成功", f"交换分区 {device_path} 已成功关闭。")
|
||||
self.refresh_all_info()
|
||||
return True
|
||||
else:
|
||||
logger.error(f"关闭交换分区 {device_path} 失败: {stderr}")
|
||||
return False
|
||||
|
||||
def _handle_wipe_partition_table(self, device_path):
|
||||
"""
|
||||
处理擦除物理盘分区表的操作。
|
||||
"""
|
||||
# NEW: 尝试解决磁盘占用问题
|
||||
if not self.disk_ops._resolve_device_occupation(device_path, action_description=f"擦除 {device_path} 上的分区表"):
|
||||
logger.info(f"用户取消或未能解决磁盘 {device_path} 的占用问题,取消擦除分区表。")
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"确认擦除分区表",
|
||||
@@ -208,10 +262,8 @@ class MainWindow(QMainWindow):
|
||||
return
|
||||
|
||||
logger.warning(f"尝试擦除 {device_path} 上的分区表。")
|
||||
# 使用 LvmOperations 中通用的 _execute_shell_command 来执行 parted 命令
|
||||
# 这里不需要 show_dialog=False,因为这个操作本身就是同步且需要用户确认的
|
||||
success, _, stderr = self.lvm_ops._execute_shell_command(
|
||||
["parted", "-s", device_path, "mklabel", "gpt"], # 默认使用 gpt 分区表类型
|
||||
["parted", "-s", device_path, "mklabel", "gpt"],
|
||||
f"擦除 {device_path} 上的分区表失败"
|
||||
)
|
||||
|
||||
@@ -219,9 +271,7 @@ class MainWindow(QMainWindow):
|
||||
QMessageBox.information(self, "成功", f"设备 {device_path} 上的分区表已成功擦除。")
|
||||
self.refresh_all_info()
|
||||
else:
|
||||
# 错误信息已由 _execute_shell_command 处理并显示
|
||||
pass
|
||||
# --- 新增方法结束 ---
|
||||
|
||||
def _handle_create_partition(self, disk_path, dev_data):
|
||||
total_disk_mib = 0.0
|
||||
@@ -260,7 +310,6 @@ class MainWindow(QMainWindow):
|
||||
|
||||
if dev_data.get('children'):
|
||||
logger.debug(f"磁盘 {disk_path} 存在现有分区,尝试计算下一个分区起始位置。")
|
||||
# 假设 disk_ops.get_disk_free_space_info_mib 能够正确处理
|
||||
calculated_start_mib, largest_free_space_mib = self.disk_ops.get_disk_free_space_info_mib(disk_path, total_disk_mib)
|
||||
if calculated_start_mib is None or largest_free_space_mib is None:
|
||||
QMessageBox.critical(self, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置。")
|
||||
@@ -271,8 +320,8 @@ class MainWindow(QMainWindow):
|
||||
max_available_mib = 0.0
|
||||
else:
|
||||
logger.debug(f"磁盘 {disk_path} 没有现有分区,假定从 0 MiB 开始,最大可用空间为整个磁盘。")
|
||||
max_available_mib = max(0.0, total_disk_mib - 1.0) # 留一点空间,避免边界问题
|
||||
start_position_mib = 1.0 # 现代分区表通常从1MB或更大偏移开始
|
||||
max_available_mib = max(0.0, total_disk_mib - 1.0)
|
||||
start_position_mib = 1.0
|
||||
logger.debug(f"磁盘 {disk_path} 没有现有分区,假定从 {start_position_mib} MiB 开始,最大可用空间为 {max_available_mib} MiB。")
|
||||
|
||||
dialog = CreatePartitionDialog(self, disk_path, total_disk_mib, max_available_mib)
|
||||
@@ -308,18 +357,14 @@ class MainWindow(QMainWindow):
|
||||
"""
|
||||
调用 DiskOperations 的异步格式化方法。
|
||||
"""
|
||||
# DiskOperations.format_partition 会处理用户确认和启动后台线程
|
||||
self.disk_ops.format_partition(device_path)
|
||||
# 界面刷新将在格式化完成后由 on_disk_formatting_finished 槽函数触发,所以这里不需要 refresh_all_info()
|
||||
|
||||
# <--- 新增: 格式化完成后的槽函数
|
||||
def on_disk_formatting_finished(self, success, device_path, stdout, stderr):
|
||||
"""
|
||||
接收 DiskOperations 发出的格式化完成信号,并刷新界面。
|
||||
"""
|
||||
logger.info(f"格式化完成信号接收: 设备 {device_path}, 成功: {success}")
|
||||
# QMessageBox 已经在 DiskOperations 的 _on_formatting_finished 中处理
|
||||
self.refresh_all_info() # 刷新所有信息以显示更新后的文件系统类型等
|
||||
self.refresh_all_info()
|
||||
|
||||
# --- RAID 管理 Tab ---
|
||||
def refresh_raid_info(self):
|
||||
@@ -336,7 +381,7 @@ class MainWindow(QMainWindow):
|
||||
self.ui.treeWidget_raid.header().setSectionResizeMode(i, QHeaderView.ResizeToContents)
|
||||
|
||||
try:
|
||||
raid_arrays = self.system_manager.get_mdadm_arrays() # 现在会返回所有阵列,包括停止的
|
||||
raid_arrays = self.system_manager.get_mdadm_arrays()
|
||||
if not raid_arrays:
|
||||
item = QTreeWidgetItem(self.ui.treeWidget_raid)
|
||||
item.setText(0, "未找到RAID阵列。")
|
||||
@@ -350,14 +395,13 @@ class MainWindow(QMainWindow):
|
||||
logger.warning(f"RAID阵列 '{array.get('name', '未知')}' 的设备路径无效,跳过。")
|
||||
continue
|
||||
|
||||
# 对于停止状态的阵列,挂载点可能不存在或不相关
|
||||
current_mount_point = ""
|
||||
if array.get('state') != 'Stopped (Configured)':
|
||||
current_mount_point = self.system_manager.get_mountpoint_for_device(array_path)
|
||||
|
||||
array_item.setText(0, array_path)
|
||||
array_item.setText(1, array.get('level', 'N/A'))
|
||||
array_item.setText(2, array.get('state', 'N/A')) # 显示实际状态
|
||||
array_item.setText(2, array.get('state', 'N/A'))
|
||||
array_item.setText(3, array.get('array_size', 'N/A'))
|
||||
array_item.setText(4, array.get('active_devices', 'N/A'))
|
||||
array_item.setText(5, array.get('failed_devices', 'N/A'))
|
||||
@@ -369,17 +413,14 @@ class MainWindow(QMainWindow):
|
||||
array_item.setText(11, current_mount_point if current_mount_point else "")
|
||||
array_item.setExpanded(True)
|
||||
|
||||
# 存储完整的阵列数据,包括状态,供上下文菜单使用
|
||||
array_item.setData(0, Qt.UserRole, array)
|
||||
|
||||
# 对于停止状态的阵列,成员设备可能为空,跳过显示子项
|
||||
if array.get('state') != 'Stopped (Configured)':
|
||||
for member in array.get('member_devices', []):
|
||||
member_item = QTreeWidgetItem(array_item)
|
||||
member_item.setText(0, f" {member.get('device_path', 'N/A')}")
|
||||
member_item.setText(1, f"成员: {member.get('raid_device', 'N/A')}") # 假设 raid_device 字段存在
|
||||
member_item.setText(1, f"成员: {member.get('raid_device', 'N/A')}")
|
||||
member_item.setText(2, member.get('state', 'N/A'))
|
||||
# member_item.setText(3, f"Major: {member.get('major', 'N/A')}, Minor: {member.get('minor', 'N/A')}") # 假设 major/minor 字段存在
|
||||
|
||||
for i in range(len(raid_headers)):
|
||||
self.ui.treeWidget_raid.resizeColumnToContents(i)
|
||||
@@ -397,7 +438,7 @@ class MainWindow(QMainWindow):
|
||||
create_raid_action.triggered.connect(self._handle_create_raid_array)
|
||||
menu.addSeparator()
|
||||
|
||||
if item and item.parent() is None: # 只针对顶层阵列项
|
||||
if item and item.parent() is None:
|
||||
array_data = item.data(0, Qt.UserRole)
|
||||
if not array_data:
|
||||
logger.warning(f"无法获取 RAID 阵列 {item.text(0)} 的详细数据。")
|
||||
@@ -406,7 +447,7 @@ class MainWindow(QMainWindow):
|
||||
array_path = array_data.get('device')
|
||||
array_state = array_data.get('state', 'N/A')
|
||||
member_devices = [m.get('device_path') for m in array_data.get('member_devices', [])]
|
||||
array_uuid = array_data.get('uuid') # <--- 获取 UUID
|
||||
array_uuid = array_data.get('uuid')
|
||||
|
||||
if not array_path or array_path == 'N/A':
|
||||
logger.warning(f"RAID阵列 '{array_data.get('name', '未知')}' 的设备路径无效,无法显示操作。")
|
||||
@@ -415,11 +456,9 @@ class MainWindow(QMainWindow):
|
||||
if array_state == 'Stopped (Configured)':
|
||||
activate_action = menu.addAction(f"激活阵列 {array_path}")
|
||||
activate_action.triggered.connect(lambda: self._handle_activate_raid_array(array_path))
|
||||
# <--- 新增:删除停止状态阵列的配置条目
|
||||
delete_config_action = menu.addAction(f"删除配置文件条目 (UUID: {array_uuid})")
|
||||
delete_config_action.triggered.connect(lambda: self._handle_delete_configured_raid_array(array_uuid))
|
||||
# 停止状态的阵列不显示卸载、停止、删除、格式化等操作
|
||||
else: # 活动或降级状态的阵列
|
||||
else:
|
||||
current_mount_point = self.system_manager.get_mountpoint_for_device(array_path)
|
||||
|
||||
if current_mount_point and current_mount_point != '[SWAP]' and current_mount_point != '':
|
||||
@@ -430,10 +469,15 @@ class MainWindow(QMainWindow):
|
||||
mount_action.triggered.connect(lambda: self._handle_mount(array_path))
|
||||
menu.addSeparator()
|
||||
|
||||
# --- 新增:RAID 阵列解除占用选项 ---
|
||||
resolve_occupation_action = menu.addAction(f"解除占用 {array_path}")
|
||||
resolve_occupation_action.triggered.connect(lambda: self._handle_resolve_device_occupation(array_path))
|
||||
menu.addSeparator() # 在解除占用后添加分隔符
|
||||
|
||||
|
||||
stop_action = menu.addAction(f"停止阵列 {array_path}")
|
||||
stop_action.triggered.connect(lambda: self._handle_stop_raid_array(array_path))
|
||||
|
||||
# <--- 修改:删除活动阵列的动作,传递 UUID
|
||||
delete_action = menu.addAction(f"删除阵列 {array_path}")
|
||||
delete_action.triggered.connect(lambda: self._handle_delete_active_raid_array(array_path, member_devices, array_uuid))
|
||||
|
||||
@@ -449,8 +493,8 @@ class MainWindow(QMainWindow):
|
||||
"""
|
||||
处理激活已停止的 RAID 阵列。
|
||||
"""
|
||||
item = self.ui.treeWidget_raid.currentItem() # 获取当前选中的项
|
||||
if not item or item.parent() is not None: # 确保是顶层阵列项
|
||||
item = self.ui.treeWidget_raid.currentItem()
|
||||
if not item or item.parent() is not None:
|
||||
logger.warning("未选择有效的 RAID 阵列进行激活。")
|
||||
return
|
||||
|
||||
@@ -477,7 +521,7 @@ class MainWindow(QMainWindow):
|
||||
logger.info(f"用户取消了激活 RAID 阵列 {array_path} 的操作。")
|
||||
return
|
||||
|
||||
if self.raid_ops.activate_raid_array(array_path, array_uuid): # 传递 UUID
|
||||
if self.raid_ops.activate_raid_array(array_path, array_uuid):
|
||||
self.refresh_all_info()
|
||||
|
||||
def _handle_create_raid_array(self):
|
||||
@@ -498,16 +542,16 @@ class MainWindow(QMainWindow):
|
||||
if self.raid_ops.stop_raid_array(array_path):
|
||||
self.refresh_all_info()
|
||||
|
||||
def _handle_delete_active_raid_array(self, array_path, member_devices, uuid): # <--- 修改方法名并添加 uuid 参数
|
||||
def _handle_delete_active_raid_array(self, array_path, member_devices, uuid):
|
||||
"""
|
||||
处理删除一个活动的 RAID 阵列的操作。
|
||||
"""
|
||||
if self.raid_ops.delete_active_raid_array(array_path, member_devices, uuid): # <--- 调用修改后的方法
|
||||
if self.raid_ops.delete_active_raid_array(array_path, member_devices, uuid):
|
||||
self.refresh_all_info()
|
||||
|
||||
def _handle_delete_configured_raid_array(self, uuid): # <--- 新增方法
|
||||
def _handle_delete_configured_raid_array(self, uuid):
|
||||
"""
|
||||
处理删除停止状态 RAID 阵列的配置文件条目。
|
||||
处理停止状态 RAID 阵列的配置文件条目。
|
||||
"""
|
||||
if self.raid_ops.delete_configured_raid_array(uuid):
|
||||
self.refresh_all_info()
|
||||
@@ -516,9 +560,7 @@ class MainWindow(QMainWindow):
|
||||
"""
|
||||
处理 RAID 阵列的格式化。
|
||||
"""
|
||||
# 这将调用 DiskOperations 的异步格式化方法
|
||||
self.disk_ops.format_partition(array_path)
|
||||
# 刷新操作会在 on_disk_formatting_finished 中触发,所以这里不需要 refresh_all_info()
|
||||
|
||||
# --- LVM 管理 Tab ---
|
||||
def refresh_lvm_info(self):
|
||||
@@ -697,6 +739,11 @@ class MainWindow(QMainWindow):
|
||||
mount_lv_action.triggered.connect(lambda: self._handle_mount(lv_path))
|
||||
menu.addSeparator()
|
||||
|
||||
# --- 新增:逻辑卷解除占用选项 ---
|
||||
resolve_occupation_action = menu.addAction(f"解除占用 {lv_path}")
|
||||
resolve_occupation_action.triggered.connect(lambda: self._handle_resolve_device_occupation(lv_path))
|
||||
menu.addSeparator() # 在解除占用后添加分隔符
|
||||
|
||||
delete_lv_action = menu.addAction(f"删除逻辑卷 {lv_name}")
|
||||
delete_lv_action.triggered.connect(lambda: self._handle_delete_lv(lv_name, vg_name))
|
||||
|
||||
@@ -739,7 +786,7 @@ class MainWindow(QMainWindow):
|
||||
available_pvs.append(f"/dev/{pv_name}")
|
||||
|
||||
if not available_pvs:
|
||||
QMessageBox.warning(self, "警告", "没有可用于创建卷组的物理卷。请确保有未分配给任何卷组的物理卷。")
|
||||
QMessageBox.warning(self, "警告", "没有可用于创建卷组的物理卷。")
|
||||
return
|
||||
|
||||
dialog = CreateVgDialog(self, available_pvs)
|
||||
@@ -758,27 +805,23 @@ class MainWindow(QMainWindow):
|
||||
if vg_name and vg_name != 'N/A':
|
||||
available_vgs.append(vg_name)
|
||||
|
||||
# 将字符串转换为小写并去除可能存在的 '<' 符号,以便更准确地匹配
|
||||
free_size_str = vg.get('vg_free', '0B').strip().lower()
|
||||
current_vg_size_gb = 0.0
|
||||
|
||||
# 修正正则表达式:
|
||||
# 1. 允许可选的 '<' 符号在数字前
|
||||
# 2. 确保单位匹配正确,如果单位缺失,默认按 GB 处理 (LVM 的 vg_free 常常默认 GB)
|
||||
match = re.match(r'<?(\d+\.?\d*)\s*([gmktb])?', free_size_str)
|
||||
if match:
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) # unit will be 'g', 'm', 't', 'k', 'b' or None
|
||||
unit = match.group(2)
|
||||
|
||||
if unit == 'k':
|
||||
current_vg_size_gb = value / (1024 * 1024) # KB to GB
|
||||
current_vg_size_gb = value / (1024 * 1024)
|
||||
elif unit == 'm':
|
||||
current_vg_size_gb = value / 1024 # MB to GB
|
||||
elif unit == 'g' or unit is None: # 如果单位是 'g' 或没有指定单位,则认为是 GB
|
||||
current_vg_size_gb = value / 1024
|
||||
elif unit == 'g' or unit is None:
|
||||
current_vg_size_gb = value
|
||||
elif unit == 't':
|
||||
current_vg_size_gb = value * 1024 # TB to GB
|
||||
elif unit == 'b': # Bytes to GB
|
||||
current_vg_size_gb = value * 1024
|
||||
elif unit == 'b':
|
||||
current_vg_size_gb = value / (1024 * 1024 * 1024)
|
||||
else:
|
||||
logger.warning(f"未知LVM单位: '{unit}' for '{free_size_str}'")
|
||||
|
||||
Reference in New Issue
Block a user