添加解除占用功能
This commit is contained in:
Binary file not shown.
Binary file not shown.
BIN
__pycache__/occupation_resolver.cpython-314.pyc
Normal file
BIN
__pycache__/occupation_resolver.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
@@ -9,7 +9,7 @@ from system_info import SystemInfoManager
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# --- 新增: 后台格式化工作线程 ---
|
# --- 后台格式化工作线程 ---
|
||||||
class FormatWorker(QObject):
|
class FormatWorker(QObject):
|
||||||
# 定义信号,用于向主线程发送格式化结果
|
# 定义信号,用于向主线程发送格式化结果
|
||||||
finished = Signal(bool, str, str, str) # 成功状态, 设备路径, 标准输出, 标准错误
|
finished = Signal(bool, str, str, str) # 成功状态, 设备路径, 标准输出, 标准错误
|
||||||
@@ -19,7 +19,6 @@ class FormatWorker(QObject):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.device_path = device_path
|
self.device_path = device_path
|
||||||
self.fs_type = fs_type
|
self.fs_type = fs_type
|
||||||
# 接收一个可调用的函数,用于执行shell命令 (这里是 DiskOperations._execute_shell_command)
|
|
||||||
self._execute_shell_command_func = execute_shell_command_func
|
self._execute_shell_command_func = execute_shell_command_func
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
@@ -48,18 +47,18 @@ class FormatWorker(QObject):
|
|||||||
command_list,
|
command_list,
|
||||||
f"格式化设备 {self.device_path} 为 {self.fs_type} 失败",
|
f"格式化设备 {self.device_path} 为 {self.fs_type} 失败",
|
||||||
root_privilege=True,
|
root_privilege=True,
|
||||||
show_dialog=False # <--- 关键:阻止工作线程弹出 QMessageBox
|
show_dialog=False # 阻止工作线程弹出 QMessageBox
|
||||||
)
|
)
|
||||||
self.finished.emit(success, self.device_path, stdout, stderr)
|
self.finished.emit(success, self.device_path, stdout, stderr)
|
||||||
logger.info(f"后台格式化完成: 设备 {self.device_path}, 成功: {success}")
|
logger.info(f"后台格式化完成: 设备 {self.device_path}, 成功: {success}")
|
||||||
|
|
||||||
|
|
||||||
class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
class DiskOperations(QObject): # 继承 QObject 以便发出信号
|
||||||
# 定义信号,用于通知主线程格式化操作的开始和结束
|
# 定义信号,用于通知主线程格式化操作的开始和结束
|
||||||
formatting_finished = Signal(bool, str, str, str) # 成功状态, 设备路径, 标准输出, 标准错误
|
formatting_finished = Signal(bool, str, str, str) # 成功状态, 设备路径, 标准输出, 标准错误
|
||||||
formatting_started = Signal(str) # 设备路径
|
formatting_started = Signal(str) # 设备路径
|
||||||
|
|
||||||
def __init__(self, system_manager: SystemInfoManager, lvm_ops, parent=None): # <--- 构造函数现在接收 lvm_ops 实例
|
def __init__(self, system_manager: SystemInfoManager, lvm_ops, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.system_manager = system_manager
|
self.system_manager = system_manager
|
||||||
self._lvm_ops = lvm_ops # 保存 LvmOperations 实例,以便调用其 _execute_shell_command
|
self._lvm_ops = lvm_ops # 保存 LvmOperations 实例,以便调用其 _execute_shell_command
|
||||||
@@ -89,8 +88,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 检查 fstab 中是否已存在相同 UUID 的条目
|
# 检查 fstab 中是否已存在相同 UUID 的条目
|
||||||
# 使用 _execute_shell_command 来读取 fstab,尽管通常不需要 sudo
|
|
||||||
# 这里为了简化,直接读取文件,但写入时使用 _execute_shell_command
|
|
||||||
with open(fstab_path, 'r') as f:
|
with open(fstab_path, 'r') as f:
|
||||||
fstab_content = f.readlines()
|
fstab_content = f.readlines()
|
||||||
|
|
||||||
@@ -101,7 +98,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
return True # 认为成功,因为目标已达成
|
return True # 认为成功,因为目标已达成
|
||||||
|
|
||||||
# 如果不存在,则追加到 fstab
|
# 如果不存在,则追加到 fstab
|
||||||
# 使用 _execute_shell_command for sudo write
|
|
||||||
success, _, stderr = self._execute_shell_command(
|
success, _, stderr = self._execute_shell_command(
|
||||||
["sh", "-c", f"echo '{fstab_entry}' >> {fstab_path}"],
|
["sh", "-c", f"echo '{fstab_entry}' >> {fstab_path}"],
|
||||||
f"将 {device_path} 添加到 {fstab_path} 失败",
|
f"将 {device_path} 添加到 {fstab_path} 失败",
|
||||||
@@ -123,7 +119,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
从 /etc/fstab 中移除指定设备的条目。
|
从 /etc/fstab 中移除指定设备的条目。
|
||||||
此方法需要 SystemInfoManager 来获取设备的 UUID。
|
此方法需要 SystemInfoManager 来获取设备的 UUID。
|
||||||
"""
|
"""
|
||||||
# NEW: 使用 system_manager 获取 UUID,它会处理路径解析
|
|
||||||
device_details = self.system_manager.get_device_details_by_path(device_path)
|
device_details = self.system_manager.get_device_details_by_path(device_path)
|
||||||
if not device_details or not device_details.get('uuid'):
|
if not device_details or not device_details.get('uuid'):
|
||||||
logger.warning(f"无法获取设备 {device_path} 的 UUID,无法从 fstab 中移除。")
|
logger.warning(f"无法获取设备 {device_path} 的 UUID,无法从 fstab 中移除。")
|
||||||
@@ -131,9 +126,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
uuid = device_details.get('uuid')
|
uuid = device_details.get('uuid')
|
||||||
|
|
||||||
fstab_path = "/etc/fstab"
|
fstab_path = "/etc/fstab"
|
||||||
# Use sed for robust removal with sudo
|
|
||||||
# suppress_critical_dialog_on_stderr_match is added to handle cases where fstab might not exist
|
|
||||||
# or sed reports no changes, which is not a critical error for removal.
|
|
||||||
command = ["sed", "-i", f"/{re.escape(uuid)}/d", fstab_path] # Use re.escape for UUID
|
command = ["sed", "-i", f"/{re.escape(uuid)}/d", fstab_path] # Use re.escape for UUID
|
||||||
success, _, stderr = self._execute_shell_command(
|
success, _, stderr = self._execute_shell_command(
|
||||||
command,
|
command,
|
||||||
@@ -149,10 +141,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
logger.info(f"已从 {fstab_path} 中删除 UUID={uuid} 的条目。")
|
logger.info(f"已从 {fstab_path} 中删除 UUID={uuid} 的条目。")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# If sed failed, it might be because the entry wasn't found, which is fine.
|
|
||||||
# _execute_shell_command would have suppressed the dialog if it matched the suppress_critical_dialog_on_stderr_match.
|
|
||||||
# So, if we reach here, it's either a real error (dialog shown by _execute_shell_command)
|
|
||||||
# or a suppressed "no changes" type of error. In both cases, if no real error, we return True.
|
|
||||||
if any(s in stderr for s in (
|
if any(s in stderr for s in (
|
||||||
f"sed: {fstab_path}: No such file or directory",
|
f"sed: {fstab_path}: No such file or directory",
|
||||||
f"sed: {fstab_path}: 没有那个文件或目录",
|
f"sed: {fstab_path}: 没有那个文件或目录",
|
||||||
@@ -160,109 +148,9 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
"No such file or directory" # more general check
|
"No such file or directory" # more general check
|
||||||
)):
|
)):
|
||||||
logger.info(f"fstab 中未找到 UUID={uuid} 的条目,无需删除。")
|
logger.info(f"fstab 中未找到 UUID={uuid} 的条目,无需删除。")
|
||||||
return True # Consider it a success if the entry is not there
|
return True
|
||||||
return False # Other errors are already handled by _execute_shell_command
|
|
||||||
|
|
||||||
def _resolve_device_occupation(self, device_path, action_description="操作"):
|
|
||||||
"""
|
|
||||||
尝试解决设备占用问题。
|
|
||||||
检查:
|
|
||||||
1. 是否是交换分区,如果是则提示用户关闭。
|
|
||||||
2. 是否有进程占用,如果有则提示用户终止。
|
|
||||||
:param device_path: 要检查的设备路径。
|
|
||||||
:param action_description: 正在尝试的操作描述,用于用户提示。
|
|
||||||
:return: True 如果占用已解决或没有占用,False 如果用户取消或解决失败。
|
|
||||||
"""
|
|
||||||
logger.info(f"尝试解决设备 {device_path} 的占用问题,以便进行 {action_description}。")
|
|
||||||
|
|
||||||
# 1. 检查是否是交换分区
|
|
||||||
block_devices = self.system_manager.get_block_devices()
|
|
||||||
device_info = self.system_manager._find_device_by_path_recursive(block_devices, device_path)
|
|
||||||
|
|
||||||
# Check if it's a swap partition and currently active
|
|
||||||
if device_info and device_info.get('fstype') == 'swap' and device_info.get('mountpoint') == '[SWAP]':
|
|
||||||
reply = QMessageBox.question(
|
|
||||||
None,
|
|
||||||
"设备占用 - 交换分区",
|
|
||||||
f"设备 {device_path} 是一个活跃的交换分区。为了进行 {action_description},需要关闭它。您要继续吗?",
|
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
|
||||||
QMessageBox.No
|
|
||||||
)
|
|
||||||
if reply == QMessageBox.No:
|
|
||||||
logger.info(f"用户取消了关闭交换分区 {device_path}。")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.info(f"尝试关闭交换分区 {device_path}。")
|
|
||||||
success, _, stderr = self._execute_shell_command(
|
|
||||||
["swapoff", device_path],
|
|
||||||
f"关闭交换分区 {device_path} 失败",
|
|
||||||
show_dialog=True # Show dialog for swapoff failure
|
|
||||||
)
|
|
||||||
if not success:
|
|
||||||
logger.error(f"关闭交换分区 {device_path} 失败: {stderr}")
|
|
||||||
QMessageBox.critical(None, "错误", f"关闭交换分区 {device_path} 失败。无法进行 {action_description}。")
|
|
||||||
return False
|
|
||||||
QMessageBox.information(None, "信息", f"交换分区 {device_path} 已成功关闭。")
|
|
||||||
# After swapoff, it might still be reported by fuser, so continue to fuser check.
|
|
||||||
|
|
||||||
# 2. 检查是否有进程占用 (使用 fuser)
|
|
||||||
success_fuser, stdout_fuser, stderr_fuser = self._execute_shell_command(
|
|
||||||
["fuser", "-vm", device_path],
|
|
||||||
f"检查设备 {device_path} 占用失败",
|
|
||||||
show_dialog=False, # Don't show dialog if fuser fails (e.g., device not busy)
|
|
||||||
suppress_critical_dialog_on_stderr_match=(
|
|
||||||
"No such file or directory", # if device doesn't exist
|
|
||||||
"not found", # if fuser itself isn't found
|
|
||||||
"Usage:" # if fuser gets invalid args, though unlikely here
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
pids = []
|
|
||||||
process_info_lines = []
|
|
||||||
# Parse fuser output only if it was successful and has stdout
|
|
||||||
if success_fuser and stdout_fuser:
|
|
||||||
for line in stdout_fuser.splitlines():
|
|
||||||
# Example: "/dev/sdb1: root 12078 .rce. gpg-agent"
|
|
||||||
match = re.match(r'^\S+:\s+\S+\s+(\d+)\s+.*', line)
|
|
||||||
if match:
|
|
||||||
pid = match.group(1)
|
|
||||||
pids.append(pid)
|
|
||||||
process_info_lines.append(line.strip())
|
|
||||||
|
|
||||||
if pids:
|
|
||||||
process_list_str = "\n".join(process_info_lines)
|
|
||||||
reply = QMessageBox.question(
|
|
||||||
None,
|
|
||||||
"设备占用 - 进程",
|
|
||||||
f"设备 {device_path} 正在被以下进程占用:\n{process_list_str}\n\n您要强制终止这些进程吗?这可能会导致数据丢失或系统不稳定!",
|
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
|
||||||
QMessageBox.No
|
|
||||||
)
|
|
||||||
if reply == QMessageBox.No:
|
|
||||||
logger.info(f"用户取消了终止占用设备 {device_path} 的进程。")
|
|
||||||
return False
|
|
||||||
|
|
||||||
logger.info(f"尝试终止占用设备 {device_path} 的进程: {', '.join(pids)}。")
|
|
||||||
all_killed = True
|
|
||||||
for pid in pids:
|
|
||||||
kill_success, _, kill_stderr = self._execute_shell_command(
|
|
||||||
["kill", "-9", pid],
|
|
||||||
f"终止进程 {pid} 失败",
|
|
||||||
show_dialog=True # Show dialog for kill failure
|
|
||||||
)
|
|
||||||
if not kill_success:
|
|
||||||
logger.error(f"终止进程 {pid} 失败: {kill_stderr}")
|
|
||||||
all_killed = False
|
|
||||||
if not all_killed:
|
|
||||||
QMessageBox.critical(None, "错误", f"未能终止所有占用设备 {device_path} 的进程。无法进行 {action_description}。")
|
|
||||||
return False
|
|
||||||
QMessageBox.information(None, "信息", f"已尝试终止占用设备 {device_path} 的所有进程。")
|
|
||||||
else:
|
|
||||||
logger.info(f"设备 {device_path} 未被任何进程占用。")
|
|
||||||
|
|
||||||
logger.info(f"设备 {device_path} 的占用问题已尝试解决。")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def mount_partition(self, device_path, mount_point, add_to_fstab=False):
|
def mount_partition(self, device_path, mount_point, add_to_fstab=False):
|
||||||
"""
|
"""
|
||||||
挂载指定设备到指定挂载点。
|
挂载指定设备到指定挂载点。
|
||||||
@@ -288,7 +176,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
if success:
|
if success:
|
||||||
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功挂载到 {mount_point}。")
|
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功挂载到 {mount_point}。")
|
||||||
if add_to_fstab:
|
if add_to_fstab:
|
||||||
# NEW: 使用 self.system_manager 获取 fstype 和 UUID
|
|
||||||
device_details = self.system_manager.get_device_details_by_path(device_path)
|
device_details = self.system_manager.get_device_details_by_path(device_path)
|
||||||
if device_details:
|
if device_details:
|
||||||
fstype = device_details.get('fstype')
|
fstype = device_details.get('fstype')
|
||||||
@@ -306,66 +193,32 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def unmount_partition(self, device_path, show_dialog_on_error=True):
|
def unmount_partition(self, device_path, show_dialog_on_error=True):
|
||||||
"""
|
"""
|
||||||
卸载指定设备。
|
卸载指定设备。
|
||||||
:param device_path: 要卸载的设备路径。
|
:param device_path: 要卸载的设备路径。
|
||||||
:param show_dialog_on_error: 是否在发生错误时显示对话框。
|
:param show_dialog_on_error: 是否在发生错误时显示对话框。
|
||||||
:return: True 如果成功,否则 False。
|
:return: True 如果成功,否则 False。
|
||||||
"""
|
"""
|
||||||
logger.info(f"尝试卸载设备 {device_path}。")
|
logger.info(f"尝试卸载设备 {device_path}。")
|
||||||
# 定义表示设备已未挂载的错误信息(中英文)
|
already_unmounted_errors = ("not mounted", "未挂载", "未指定挂载点")
|
||||||
already_unmounted_errors = ("not mounted", "未挂载", "未指定挂载点")
|
|
||||||
device_busy_errors = ("device is busy", "设备或资源忙", "Device or resource busy") # Common "device busy" messages
|
|
||||||
|
|
||||||
# First attempt to unmount
|
success, _, stderr = self._execute_shell_command(
|
||||||
success, _, stderr = self._execute_shell_command(
|
["umount", device_path],
|
||||||
["umount", device_path],
|
f"卸载设备 {device_path} 失败",
|
||||||
f"卸载设备 {device_path} 失败",
|
suppress_critical_dialog_on_stderr_match=already_unmounted_errors,
|
||||||
suppress_critical_dialog_on_stderr_match=already_unmounted_errors + device_busy_errors, # Suppress both types of errors for initial attempt
|
show_dialog=show_dialog_on_error
|
||||||
show_dialog=False # Don't show dialog on first attempt, we'll handle it
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。")
|
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。")
|
||||||
return True
|
return True
|
||||||
else:
|
|
||||||
# Check if it failed because it was already unmounted
|
|
||||||
if any(s in stderr for s in already_unmounted_errors):
|
|
||||||
logger.info(f"设备 {device_path} 已经处于未挂载状态(或挂载点未指定),无需重复卸载。")
|
|
||||||
return True # Consider it a success
|
|
||||||
|
|
||||||
# Check if it failed because the device was busy
|
|
||||||
if any(s in stderr for s in device_busy_errors):
|
|
||||||
logger.warning(f"卸载设备 {device_path} 失败,设备忙。尝试解决占用问题。")
|
|
||||||
# Call the new helper to resolve occupation
|
|
||||||
if self._resolve_device_occupation(device_path, action_description=f"卸载 {device_path}"):
|
|
||||||
logger.info(f"设备 {device_path} 占用问题已解决,重试卸载。")
|
|
||||||
# Retry unmount after resolving occupation
|
|
||||||
retry_success, _, retry_stderr = self._execute_shell_command(
|
|
||||||
["umount", device_path],
|
|
||||||
f"重试卸载设备 {device_path} 失败",
|
|
||||||
suppress_critical_dialog_on_stderr_match=already_unmounted_errors,
|
|
||||||
show_dialog=show_dialog_on_error # Show dialog if retry fails for other reasons
|
|
||||||
)
|
|
||||||
if retry_success:
|
|
||||||
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.error(f"重试卸载设备 {device_path} 失败: {retry_stderr}")
|
|
||||||
if show_dialog_on_error and not any(s in retry_stderr for s in already_unmounted_errors):
|
|
||||||
QMessageBox.critical(None, "错误", f"重试卸载设备 {device_path} 失败。\n错误详情: {retry_stderr}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
logger.info(f"用户取消或未能解决设备 {device_path} 的占用问题。")
|
|
||||||
if show_dialog_on_error:
|
|
||||||
QMessageBox.critical(None, "错误", f"未能解决设备 {device_path} 的占用问题,无法卸载。")
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
# Other types of unmount failures
|
is_already_unmounted_error = any(s in stderr for s in already_unmounted_errors)
|
||||||
logger.error(f"卸载设备 {device_path} 失败: {stderr}")
|
if is_already_unmounted_error:
|
||||||
if show_dialog_on_error:
|
logger.info(f"设备 {device_path} 已经处于未挂载状态(或挂载点未指定),无需重复卸载。")
|
||||||
QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 失败。\n错误详情: {stderr}")
|
return True
|
||||||
return False
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def get_disk_free_space_info_mib(self, disk_path, total_disk_mib):
|
def get_disk_free_space_info_mib(self, disk_path, total_disk_mib):
|
||||||
"""
|
"""
|
||||||
@@ -376,8 +229,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
如果磁盘有分区表但没有空闲空间,返回 (None, None)。
|
如果磁盘有分区表但没有空闲空间,返回 (None, None)。
|
||||||
"""
|
"""
|
||||||
logger.debug(f"尝试获取磁盘 {disk_path} 的空闲空间信息 (MiB)。")
|
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(
|
success, stdout, stderr = self._execute_shell_command(
|
||||||
["parted", "-s", disk_path, "unit", "MiB", "print", "free"],
|
["parted", "-s", disk_path, "unit", "MiB", "print", "free"],
|
||||||
f"获取磁盘 {disk_path} 分区信息失败",
|
f"获取磁盘 {disk_path} 分区信息失败",
|
||||||
@@ -386,9 +237,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not success:
|
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}")
|
logger.error(f"获取磁盘 {disk_path} 空闲空间信息失败: {stderr}")
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
@@ -396,18 +244,14 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
|
|
||||||
free_spaces = []
|
free_spaces = []
|
||||||
lines = stdout.splitlines()
|
lines = stdout.splitlines()
|
||||||
# Regex to capture StartMiB and SizeMiB from a "Free Space" line
|
|
||||||
# It's made more flexible to match "Free Space", "空闲空间", and "可用空间"
|
|
||||||
# Example: " 0.02MiB 8192MiB 8192MiB 可用空间"
|
|
||||||
# We need to capture the first numeric value (Start) and the third numeric value (Size).
|
|
||||||
free_space_line_pattern = re.compile(r'^\s*(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(?:Free Space|空闲空间|可用空间)')
|
free_space_line_pattern = re.compile(r'^\s*(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(?:Free Space|空闲空间|可用空间)')
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
match = free_space_line_pattern.match(line)
|
match = free_space_line_pattern.match(line)
|
||||||
if match:
|
if match:
|
||||||
try:
|
try:
|
||||||
start_mib = float(match.group(1)) # Capture StartMiB
|
start_mib = float(match.group(1))
|
||||||
size_mib = float(match.group(3)) # Capture SizeMiB (the third numeric value)
|
size_mib = float(match.group(3))
|
||||||
free_spaces.append({'start_mib': start_mib, 'size_mib': size_mib})
|
free_spaces.append({'start_mib': start_mib, 'size_mib': size_mib})
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
logger.warning(f"解析 parted free space 行 '{line}' 失败: {ve}")
|
logger.warning(f"解析 parted free space 行 '{line}' 失败: {ve}")
|
||||||
@@ -417,7 +261,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
logger.warning(f"在磁盘 {disk_path} 上未找到空闲空间。")
|
logger.warning(f"在磁盘 {disk_path} 上未找到空闲空间。")
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
# 找到最大的空闲空间块
|
|
||||||
largest_free_space = max(free_spaces, key=lambda x: x['size_mib'])
|
largest_free_space = max(free_spaces, key=lambda x: x['size_mib'])
|
||||||
start_mib = largest_free_space['start_mib']
|
start_mib = largest_free_space['start_mib']
|
||||||
size_mib = largest_free_space['size_mib']
|
size_mib = largest_free_space['size_mib']
|
||||||
@@ -440,39 +283,28 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
QMessageBox.critical(None, "错误", "无效的磁盘路径或分区表类型。")
|
QMessageBox.critical(None, "错误", "无效的磁盘路径或分区表类型。")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# NEW: 尝试解决磁盘占用问题
|
|
||||||
if not self._resolve_device_occupation(disk_path, action_description=f"在 {disk_path} 上创建分区"):
|
|
||||||
logger.info(f"用户取消或未能解决磁盘 {disk_path} 的占用问题,取消创建分区。")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 1. 检查磁盘是否有分区表
|
# 1. 检查磁盘是否有分区表
|
||||||
has_partition_table = False
|
has_partition_table = False
|
||||||
# Use suppress_critical_dialog_on_stderr_match for "unrecognized disk label" when checking
|
|
||||||
success_check, stdout_check, stderr_check = self._execute_shell_command(
|
success_check, stdout_check, stderr_check = self._execute_shell_command(
|
||||||
["parted", "-s", disk_path, "print"],
|
["parted", "-s", disk_path, "print"],
|
||||||
f"检查磁盘 {disk_path} 分区表失败",
|
f"检查磁盘 {disk_path} 分区表失败",
|
||||||
suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label"),
|
suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label"),
|
||||||
root_privilege=True
|
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:
|
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")):
|
if any(s in stderr_check for s in ("无法辨识的磁盘卷标", "unrecognized disk label")):
|
||||||
logger.info(f"磁盘 {disk_path} 没有可识别的分区表。")
|
logger.info(f"磁盘 {disk_path} 没有可识别的分区表。")
|
||||||
has_partition_table = False # Explicitly set to False
|
has_partition_table = False
|
||||||
else:
|
else:
|
||||||
logger.error(f"检查磁盘 {disk_path} 分区表时发生非预期的错误: {stderr_check}")
|
logger.error(f"检查磁盘 {disk_path} 分区表时发生非预期的错误: {stderr_check}")
|
||||||
return False # Other critical error, stop operation.
|
return False
|
||||||
else: # success_check is True
|
else:
|
||||||
if "Partition Table: unknown" not in stdout_check and "分区表:unknown" not in stdout_check:
|
if "Partition Table: unknown" not in stdout_check and "分区表:unknown" not in stdout_check:
|
||||||
has_partition_table = True
|
has_partition_table = True
|
||||||
else:
|
else:
|
||||||
logger.info(f"parted print 报告磁盘 {disk_path} 的分区表为 'unknown'。")
|
logger.info(f"parted print 报告磁盘 {disk_path} 的分区表为 'unknown'。")
|
||||||
has_partition_table = False
|
has_partition_table = False
|
||||||
|
|
||||||
|
|
||||||
actual_start_mib_for_parted = 0.0
|
actual_start_mib_for_parted = 0.0
|
||||||
|
|
||||||
# 2. 如果没有分区表,则创建分区表
|
# 2. 如果没有分区表,则创建分区表
|
||||||
@@ -492,7 +324,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
)
|
)
|
||||||
if not success:
|
if not success:
|
||||||
return False
|
return False
|
||||||
# 对于新创建分区表的磁盘,第一个分区通常从 1MiB 开始以确保兼容性和对齐
|
|
||||||
actual_start_mib_for_parted = 1.0
|
actual_start_mib_for_parted = 1.0
|
||||||
else:
|
else:
|
||||||
# 如果有分区表,获取下一个可用分区的起始位置
|
# 如果有分区表,获取下一个可用分区的起始位置
|
||||||
@@ -501,10 +332,7 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
QMessageBox.critical(None, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置或没有空闲空间。")
|
QMessageBox.critical(None, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置或没有空闲空间。")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 如果 parted 报告的起始位置非常接近 0 MiB (例如 0.0 MiB 或 0.02 MiB),
|
if start_mib_from_parted < 1.0:
|
||||||
# 为了安全和兼容性,也将其调整为 1.0 MiB。
|
|
||||||
# 否则,使用 parted 报告的精确起始位置。
|
|
||||||
if start_mib_from_parted < 1.0: # covers 0.0MiB and values like 0.017MiB (17.4kB)
|
|
||||||
actual_start_mib_for_parted = 1.0
|
actual_start_mib_for_parted = 1.0
|
||||||
else:
|
else:
|
||||||
actual_start_mib_for_parted = start_mib_from_parted
|
actual_start_mib_for_parted = start_mib_from_parted
|
||||||
@@ -514,12 +342,7 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
end_pos = "100%"
|
end_pos = "100%"
|
||||||
size_for_log = "最大可用空间"
|
size_for_log = "最大可用空间"
|
||||||
else:
|
else:
|
||||||
# 计算结束 MiB
|
|
||||||
# 注意:这里计算的 end_mib 是基于用户请求的大小,
|
|
||||||
# parted 会根据实际可用空间和对齐进行微调。
|
|
||||||
end_mib = actual_start_mib_for_parted + size_gb * 1024
|
end_mib = actual_start_mib_for_parted + size_gb * 1024
|
||||||
|
|
||||||
# 简单检查,如果请求的大小导致结束位置超出磁盘总大小,则使用最大可用
|
|
||||||
if end_mib > total_disk_mib:
|
if end_mib > total_disk_mib:
|
||||||
QMessageBox.warning(None, "警告", f"请求的分区大小 ({size_gb}GB) 超出了可用空间。将调整为最大可用空间。")
|
QMessageBox.warning(None, "警告", f"请求的分区大小 ({size_gb}GB) 超出了可用空间。将调整为最大可用空间。")
|
||||||
end_pos = "100%"
|
end_pos = "100%"
|
||||||
@@ -547,11 +370,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
:param device_path: 要删除的分区路径。
|
:param device_path: 要删除的分区路径。
|
||||||
:return: True 如果成功,否则 False。
|
:return: True 如果成功,否则 False。
|
||||||
"""
|
"""
|
||||||
# NEW: 尝试解决分区占用问题
|
|
||||||
if not self._resolve_device_occupation(device_path, action_description=f"删除分区 {device_path}"):
|
|
||||||
logger.info(f"用户取消或未能解决分区 {device_path} 的占用问题,取消删除分区。")
|
|
||||||
return False
|
|
||||||
|
|
||||||
reply = QMessageBox.question(None, "确认删除分区",
|
reply = QMessageBox.question(None, "确认删除分区",
|
||||||
f"您确定要删除分区 {device_path} 吗?此操作将擦除分区上的所有数据!",
|
f"您确定要删除分区 {device_path} 吗?此操作将擦除分区上的所有数据!",
|
||||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||||||
@@ -559,15 +377,13 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
logger.info(f"用户取消了删除分区 {device_path} 的操作。")
|
logger.info(f"用户取消了删除分区 {device_path} 的操作。")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 尝试卸载分区 (此方法内部会尝试解决占用问题)
|
# 尝试卸载分区
|
||||||
self.unmount_partition(device_path, show_dialog_on_error=False)
|
self.unmount_partition(device_path, show_dialog_on_error=False)
|
||||||
|
|
||||||
# 从 fstab 中移除条目
|
# 从 fstab 中移除条目
|
||||||
# _remove_fstab_entry also handles "not found" gracefully.
|
|
||||||
self._remove_fstab_entry(device_path)
|
self._remove_fstab_entry(device_path)
|
||||||
|
|
||||||
# 获取父磁盘和分区号
|
# 获取父磁盘和分区号
|
||||||
# 例如 /dev/sdb1 -> /dev/sdb, 1
|
|
||||||
match = re.match(r'(/dev/[a-z]+)(\d+)', device_path)
|
match = re.match(r'(/dev/[a-z]+)(\d+)', device_path)
|
||||||
if not match:
|
if not match:
|
||||||
QMessageBox.critical(None, "错误", f"无法解析设备路径 {device_path}。")
|
QMessageBox.critical(None, "错误", f"无法解析设备路径 {device_path}。")
|
||||||
@@ -602,11 +418,6 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
logger.info("用户取消了文件系统选择。")
|
logger.info("用户取消了文件系统选择。")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# NEW: 尝试解决设备占用问题
|
|
||||||
if not self._resolve_device_occupation(device_path, action_description=f"格式化设备 {device_path}"):
|
|
||||||
logger.info(f"用户取消或未能解决设备 {device_path} 的占用问题,取消格式化。")
|
|
||||||
return False
|
|
||||||
|
|
||||||
reply = QMessageBox.question(None, "确认格式化",
|
reply = QMessageBox.question(None, "确认格式化",
|
||||||
f"您确定要格式化设备 {device_path} 为 {fstype} 吗?此操作将擦除所有数据!",
|
f"您确定要格式化设备 {device_path} 为 {fstype} 吗?此操作将擦除所有数据!",
|
||||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||||||
@@ -626,20 +437,19 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
|
|
||||||
# 创建 QThread 和 FormatWorker 实例
|
# 创建 QThread 和 FormatWorker 实例
|
||||||
thread = QThread()
|
thread = QThread()
|
||||||
# 将 self._execute_shell_command (它内部调用 LvmOperations._execute_shell_command) 传递给工作线程
|
|
||||||
worker = FormatWorker(device_path, fstype, self._execute_shell_command)
|
worker = FormatWorker(device_path, fstype, self._execute_shell_command)
|
||||||
worker.moveToThread(thread)
|
worker.moveToThread(thread)
|
||||||
|
|
||||||
# 连接信号和槽
|
# 连接信号和槽
|
||||||
thread.started.connect(worker.run) # 线程启动时执行 worker 的 run 方法
|
thread.started.connect(worker.run)
|
||||||
worker.finished.connect(lambda s, dp, o, e: self._on_formatting_finished(s, dp, o, e)) # worker 完成时调用处理函数
|
worker.finished.connect(lambda s, dp, o, e: self._on_formatting_finished(s, dp, o, e))
|
||||||
worker.finished.connect(thread.quit) # worker 完成时退出线程
|
worker.finished.connect(thread.quit)
|
||||||
worker.finished.connect(worker.deleteLater) # worker 完成时自动删除 worker 对象
|
worker.finished.connect(worker.deleteLater)
|
||||||
thread.finished.connect(thread.deleteLater) # 线程退出时自动删除线程对象
|
thread.finished.connect(thread.deleteLater)
|
||||||
|
|
||||||
self.active_format_workers[device_path] = thread # 存储线程以便管理
|
self.active_format_workers[device_path] = thread
|
||||||
self.formatting_started.emit(device_path) # 发出信号通知主界面格式化已开始
|
self.formatting_started.emit(device_path)
|
||||||
thread.start() # 启动线程
|
thread.start()
|
||||||
|
|
||||||
QMessageBox.information(None, "开始格式化", f"设备 {device_path} 正在后台格式化为 {fstype}。完成后将通知您。")
|
QMessageBox.information(None, "开始格式化", f"设备 {device_path} 正在后台格式化为 {fstype}。完成后将通知您。")
|
||||||
return True
|
return True
|
||||||
@@ -649,13 +459,11 @@ class DiskOperations(QObject): # <--- 继承 QObject 以便发出信号
|
|||||||
处理格式化工作线程完成后的结果。此槽函数在主线程中执行。
|
处理格式化工作线程完成后的结果。此槽函数在主线程中执行。
|
||||||
"""
|
"""
|
||||||
if device_path in self.active_format_workers:
|
if device_path in self.active_format_workers:
|
||||||
del self.active_format_workers[device_path] # 从跟踪列表中移除
|
del self.active_format_workers[device_path]
|
||||||
|
|
||||||
# 在主线程中显示最终结果的消息框
|
|
||||||
if success:
|
if success:
|
||||||
QMessageBox.information(None, "格式化成功", f"设备 {device_path} 已成功格式化。")
|
QMessageBox.information(None, "格式化成功", f"设备 {device_path} 已成功格式化。")
|
||||||
else:
|
else:
|
||||||
QMessageBox.critical(None, "格式化失败", f"格式化设备 {device_path} 失败。\n错误详情: {stderr}")
|
QMessageBox.critical(None, "格式化失败", f"格式化设备 {device_path} 失败。\n错误详情: {stderr}")
|
||||||
|
|
||||||
self.formatting_finished.emit(success, device_path, stdout, stderr) # 发出信号通知 MainWindow 刷新界面
|
self.formatting_finished.emit(success, device_path, stdout, stderr)
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ from PySide6.QtWidgets import QMessageBox
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class LvmOperations:
|
class LvmOperations:
|
||||||
def __init__(self, disk_ops=None): # <--- Add disk_ops parameter
|
def __init__(self):
|
||||||
self.disk_ops = disk_ops # Store disk_ops instance
|
pass
|
||||||
|
|
||||||
def _execute_shell_command(self, command_list, error_message, root_privilege=True,
|
def _execute_shell_command(self, command_list, error_message, root_privilege=True,
|
||||||
suppress_critical_dialog_on_stderr_match=None, input_data=None,
|
suppress_critical_dialog_on_stderr_match=None, input_data=None,
|
||||||
show_dialog=True): # <--- 新增 show_dialog 参数,默认为 True
|
show_dialog=True, expected_non_zero_exit_codes=None):
|
||||||
"""
|
"""
|
||||||
通用地运行一个 shell 命令,并处理错误。
|
通用地运行一个 shell 命令,并处理错误。
|
||||||
:param command_list: 命令及其参数的列表。
|
:param command_list: 命令及其参数的列表。
|
||||||
@@ -21,11 +21,12 @@ class LvmOperations:
|
|||||||
可以是字符串或字符串元组/列表。
|
可以是字符串或字符串元组/列表。
|
||||||
:param input_data: 传递给命令stdin的数据 (str)。
|
:param input_data: 传递给命令stdin的数据 (str)。
|
||||||
:param show_dialog: 如果为 False,则不显示关键错误对话框。
|
:param show_dialog: 如果为 False,则不显示关键错误对话框。
|
||||||
|
:param expected_non_zero_exit_codes: 预期为非零但仍视为成功的退出码列表。
|
||||||
:return: (True/False, stdout_str, stderr_str)
|
:return: (True/False, stdout_str, stderr_str)
|
||||||
"""
|
"""
|
||||||
if not all(isinstance(arg, str) for arg in command_list):
|
if not all(isinstance(arg, str) for arg in command_list):
|
||||||
logger.error(f"命令列表包含非字符串元素: {command_list}")
|
logger.error(f"命令列表包含非字符串元素: {command_list}")
|
||||||
if show_dialog: # <--- 根据 show_dialog 决定是否弹出对话框
|
if show_dialog:
|
||||||
QMessageBox.critical(None, "错误", f"内部错误:尝试执行的命令包含无效参数。\n命令详情: {command_list}")
|
QMessageBox.critical(None, "错误", f"内部错误:尝试执行的命令包含无效参数。\n命令详情: {command_list}")
|
||||||
return False, "", "内部错误:命令参数类型不正确。"
|
return False, "", "内部错误:命令参数类型不正确。"
|
||||||
|
|
||||||
@@ -53,6 +54,12 @@ class LvmOperations:
|
|||||||
logger.error(f"标准输出: {e.stdout.strip()}")
|
logger.error(f"标准输出: {e.stdout.strip()}")
|
||||||
logger.error(f"标准错误: {stderr_output}")
|
logger.error(f"标准错误: {stderr_output}")
|
||||||
|
|
||||||
|
# NEW LOGIC: Check if the exit code is expected as non-critical
|
||||||
|
if expected_non_zero_exit_codes and e.returncode in expected_non_zero_exit_codes:
|
||||||
|
logger.info(f"命令 '{full_cmd_str}' 以预期非零退出码 {e.returncode} 结束,仍视为成功。")
|
||||||
|
return True, e.stdout.strip(), stderr_output # Treat as success
|
||||||
|
|
||||||
|
|
||||||
should_suppress_dialog = False
|
should_suppress_dialog = False
|
||||||
if suppress_critical_dialog_on_stderr_match:
|
if suppress_critical_dialog_on_stderr_match:
|
||||||
if isinstance(suppress_critical_dialog_on_stderr_match, str):
|
if isinstance(suppress_critical_dialog_on_stderr_match, str):
|
||||||
@@ -64,23 +71,21 @@ class LvmOperations:
|
|||||||
should_suppress_dialog = True
|
should_suppress_dialog = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if show_dialog and not should_suppress_dialog: # <--- 根据 show_dialog 决定是否弹出对话框
|
if show_dialog and not should_suppress_dialog:
|
||||||
QMessageBox.critical(None, "错误", f"{error_message}\n错误详情: {stderr_output}")
|
QMessageBox.critical(None, "错误", f"{error_message}\n错误详情: {stderr_output}")
|
||||||
|
|
||||||
return False, e.stdout.strip(), stderr_output
|
return False, e.stdout.strip(), stderr_output
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
if show_dialog: # <--- 根据 show_dialog 决定是否弹出对话框
|
if show_dialog:
|
||||||
QMessageBox.critical(None, "错误", f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
|
QMessageBox.critical(None, "错误", f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
|
||||||
logger.error(f"命令 '{command_list[0]}' 未找到。")
|
logger.error(f"命令 '{command_list[0]}' 未找到。")
|
||||||
return False, "", f"命令 '{command_list[0]}' 未找到。"
|
return False, "", f"命令 '{command_list[0]}' 未找到。"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if show_dialog: # <--- 根据 show_dialog 决定是否弹出对话框
|
if show_dialog:
|
||||||
QMessageBox.critical(None, "错误", f"执行命令时发生未知错误: {e}")
|
QMessageBox.critical(None, "错误", f"执行命令时发生未知错误: {e}")
|
||||||
logger.error(f"执行命令 {full_cmd_str} 时发生未知错误: {e}")
|
logger.error(f"执行命令 {full_cmd_str} 时发生未知错误: {e}")
|
||||||
return False, "", str(e)
|
return False, "", str(e)
|
||||||
|
|
||||||
# 以下是 LvmOperations 中其他方法的代码,保持不变。
|
|
||||||
# ... (create_pv, delete_pv, create_vg, delete_vg, create_lv, delete_lv, activate_lv, deactivate_lv) ...
|
|
||||||
def create_pv(self, device_path):
|
def create_pv(self, device_path):
|
||||||
"""
|
"""
|
||||||
创建物理卷 (PV)。
|
创建物理卷 (PV)。
|
||||||
@@ -92,15 +97,6 @@ class LvmOperations:
|
|||||||
QMessageBox.critical(None, "错误", "无效的设备路径。")
|
QMessageBox.critical(None, "错误", "无效的设备路径。")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# NEW: 尝试解决设备占用问题
|
|
||||||
if self.disk_ops: # Ensure disk_ops is available
|
|
||||||
if not self.disk_ops._resolve_device_occupation(device_path, action_description=f"在 {device_path} 上创建物理卷"):
|
|
||||||
logger.info(f"用户取消或未能解决设备 {device_path} 的占用问题,取消创建物理卷。")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
logger.warning("DiskOperations 实例未传递给 LvmOperations,无法解决设备占用问题。")
|
|
||||||
|
|
||||||
|
|
||||||
reply = QMessageBox.question(None, "确认创建物理卷",
|
reply = QMessageBox.question(None, "确认创建物理卷",
|
||||||
f"您确定要在设备 {device_path} 上创建物理卷吗?此操作将覆盖设备上的数据。",
|
f"您确定要在设备 {device_path} 上创建物理卷吗?此操作将覆盖设备上的数据。",
|
||||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||||||
@@ -109,20 +105,17 @@ class LvmOperations:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
logger.info(f"尝试在 {device_path} 上创建物理卷。")
|
logger.info(f"尝试在 {device_path} 上创建物理卷。")
|
||||||
# 第一次尝试创建物理卷,抑制 "device is partitioned" 错误,因为我们将在代码中处理它
|
|
||||||
success, _, stderr = self._execute_shell_command(
|
success, _, stderr = self._execute_shell_command(
|
||||||
["pvcreate", "-y", device_path], # -y 自动确认
|
["pvcreate", "-y", device_path],
|
||||||
f"在 {device_path} 上创建物理卷失败",
|
f"在 {device_path} 上创建物理卷失败",
|
||||||
suppress_critical_dialog_on_stderr_match="device is partitioned" # 抑制此特定错误
|
suppress_critical_dialog_on_stderr_match="device is partitioned"
|
||||||
)
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
QMessageBox.information(None, "成功", f"物理卷已在 {device_path} 上成功创建。")
|
QMessageBox.information(None, "成功", f"物理卷已在 {device_path} 上成功创建。")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# 如果 pvcreate 失败,检查是否是 "device is partitioned" 错误
|
|
||||||
if "device is partitioned" in stderr:
|
if "device is partitioned" in stderr:
|
||||||
# 提示用户是否要擦除分区表
|
|
||||||
wipe_reply = QMessageBox.question(
|
wipe_reply = QMessageBox.question(
|
||||||
None, "设备已分区",
|
None, "设备已分区",
|
||||||
f"设备 {device_path} 已分区。您是否要擦除设备上的所有分区表,"
|
f"设备 {device_path} 已分区。您是否要擦除设备上的所有分区表,"
|
||||||
@@ -131,35 +124,28 @@ class LvmOperations:
|
|||||||
)
|
)
|
||||||
if wipe_reply == QMessageBox.Yes:
|
if wipe_reply == QMessageBox.Yes:
|
||||||
logger.warning(f"用户选择擦除 {device_path} 上的分区表并创建物理卷。")
|
logger.warning(f"用户选择擦除 {device_path} 上的分区表并创建物理卷。")
|
||||||
# 尝试擦除分区表 (使用 GPT 作为默认,通常更现代且支持大容量磁盘)
|
|
||||||
# 注意:这里需要 parted 命令,确保系统已安装 parted
|
|
||||||
mklabel_success, _, mklabel_stderr = self._execute_shell_command(
|
mklabel_success, _, mklabel_stderr = self._execute_shell_command(
|
||||||
["parted", "-s", device_path, "mklabel", "gpt"], # 使用 gpt 分区表
|
["parted", "-s", device_path, "mklabel", "gpt"],
|
||||||
f"擦除 {device_path} 上的分区表失败"
|
f"擦除 {device_path} 上的分区表失败"
|
||||||
)
|
)
|
||||||
if mklabel_success:
|
if mklabel_success:
|
||||||
logger.info(f"已成功擦除 {device_path} 上的分区表。重试创建物理卷。")
|
logger.info(f"已成功擦除 {device_path} 上的分区表。重试创建物理卷。")
|
||||||
# 再次尝试创建物理卷
|
|
||||||
retry_success, _, retry_stderr = self._execute_shell_command(
|
retry_success, _, retry_stderr = self._execute_shell_command(
|
||||||
["pvcreate", "-y", device_path],
|
["pvcreate", "-y", device_path],
|
||||||
f"在 {device_path} 上创建物理卷失败 (重试)"
|
f"在 {device_path} 上创建物理卷失败 (重试)"
|
||||||
# 重试时不再抑制,如果再次失败,_execute_shell_command 会显示错误
|
|
||||||
)
|
)
|
||||||
if retry_success:
|
if retry_success:
|
||||||
QMessageBox.information(None, "成功", f"物理卷已在 {device_path} 上成功创建。")
|
QMessageBox.information(None, "成功", f"物理卷已在 {device_path} 上成功创建。")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# 如果重试失败,_execute_shell_command 已经显示错误
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
# mklabel 失败,_execute_shell_command 已经显示错误
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
logger.info(f"用户取消了擦除 {device_path} 分区表的操作。")
|
logger.info(f"用户取消了擦除 {device_path} 分区表的操作。")
|
||||||
QMessageBox.information(None, "信息", f"未在已分区设备 {device_path} 上创建物理卷。")
|
QMessageBox.information(None, "信息", f"未在已分区设备 {device_path} 上创建物理卷。")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
# 对于其他类型的 pvcreate 失败,_execute_shell_command 已经显示了错误
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_pv(self, device_path):
|
def delete_pv(self, device_path):
|
||||||
@@ -374,4 +360,3 @@ class LvmOperations:
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
171
mainwindow.py
171
mainwindow.py
@@ -6,7 +6,7 @@ import os
|
|||||||
|
|
||||||
from PySide6.QtWidgets import (QApplication, QMainWindow, QTreeWidgetItem,
|
from PySide6.QtWidgets import (QApplication, QMainWindow, QTreeWidgetItem,
|
||||||
QMessageBox, QHeaderView, QMenu, QInputDialog, QDialog)
|
QMessageBox, QHeaderView, QMenu, QInputDialog, QDialog)
|
||||||
from PySide6.QtCore import Qt, QPoint, QThread # <--- 确保导入 QThread
|
from PySide6.QtCore import Qt, QPoint, QThread
|
||||||
|
|
||||||
# 导入自动生成的 UI 文件
|
# 导入自动生成的 UI 文件
|
||||||
from ui_form import Ui_MainWindow
|
from ui_form import Ui_MainWindow
|
||||||
@@ -20,6 +20,8 @@ from disk_operations import DiskOperations
|
|||||||
from raid_operations import RaidOperations
|
from raid_operations import RaidOperations
|
||||||
# 导入 LVM 操作模块
|
# 导入 LVM 操作模块
|
||||||
from lvm_operations import LvmOperations
|
from lvm_operations import LvmOperations
|
||||||
|
# 导入新的 resolver 类
|
||||||
|
from occupation_resolver import OccupationResolver
|
||||||
# 导入自定义对话框
|
# 导入自定义对话框
|
||||||
from dialogs import (CreatePartitionDialog, MountDialog, CreateRaidDialog,
|
from dialogs import (CreatePartitionDialog, MountDialog, CreateRaidDialog,
|
||||||
CreatePvDialog, CreateVgDialog, CreateLvDialog)
|
CreatePvDialog, CreateVgDialog, CreateLvDialog)
|
||||||
@@ -34,13 +36,13 @@ class MainWindow(QMainWindow):
|
|||||||
setup_logging(self.ui.logOutputTextEdit)
|
setup_logging(self.ui.logOutputTextEdit)
|
||||||
logger.info("应用程序启动。")
|
logger.info("应用程序启动。")
|
||||||
|
|
||||||
# 初始化管理器和操作类
|
# 初始化管理器和操作类 (恢复原始的初始化顺序和依赖关系)
|
||||||
self.system_manager = SystemInfoManager()
|
self.system_manager = SystemInfoManager()
|
||||||
# Correct order for dependency injection:
|
self.lvm_ops = LvmOperations()
|
||||||
self.lvm_ops = LvmOperations() # Initialize LVM first
|
# DiskOperations 仍然需要 lvm_ops 的 _execute_shell_command 方法
|
||||||
self.disk_ops = DiskOperations(self.system_manager, self.lvm_ops) # DiskOperations needs system_manager and lvm_ops
|
self.disk_ops = DiskOperations(self.system_manager, self.lvm_ops)
|
||||||
self.lvm_ops.disk_ops = self.disk_ops # Inject disk_ops into lvm_ops
|
# RaidOperations 仍然需要 system_manager
|
||||||
self.raid_ops = RaidOperations(self.system_manager, self.disk_ops) # RaidOperations needs system_manager and disk_ops
|
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.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
self.ui.treeWidget_lvm.customContextMenuRequested.connect(self.show_lvm_context_menu)
|
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_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()
|
self.refresh_all_info()
|
||||||
@@ -158,10 +166,8 @@ class MainWindow(QMainWindow):
|
|||||||
if device_type == 'disk':
|
if device_type == 'disk':
|
||||||
create_partition_action = menu.addAction(f"创建分区 {device_path}...")
|
create_partition_action = menu.addAction(f"创建分区 {device_path}...")
|
||||||
create_partition_action.triggered.connect(lambda: self._handle_create_partition(device_path, dev_data))
|
create_partition_action.triggered.connect(lambda: self._handle_create_partition(device_path, dev_data))
|
||||||
# --- 新增功能:擦除分区表 ---
|
|
||||||
wipe_action = menu.addAction(f"擦除分区表 {device_path}...")
|
wipe_action = menu.addAction(f"擦除分区表 {device_path}...")
|
||||||
wipe_action.triggered.connect(lambda: self._handle_wipe_partition_table(device_path))
|
wipe_action.triggered.connect(lambda: self._handle_wipe_partition_table(device_path))
|
||||||
# --- 新增功能结束 ---
|
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
|
|
||||||
if device_type == 'part':
|
if device_type == 'part':
|
||||||
@@ -178,22 +184,70 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
format_action = menu.addAction(f"格式化分区 {device_path}...")
|
format_action = menu.addAction(f"格式化分区 {device_path}...")
|
||||||
format_action.triggered.connect(lambda: self._handle_format_partition(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():
|
if menu.actions():
|
||||||
menu.exec(self.ui.treeWidget_block_devices.mapToGlobal(pos))
|
menu.exec(self.ui.treeWidget_block_devices.mapToGlobal(pos))
|
||||||
else:
|
else:
|
||||||
logger.info("右键点击了空白区域或设备没有可用的操作。")
|
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):
|
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(
|
reply = QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
"确认擦除分区表",
|
"确认擦除分区表",
|
||||||
@@ -208,10 +262,8 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
|
|
||||||
logger.warning(f"尝试擦除 {device_path} 上的分区表。")
|
logger.warning(f"尝试擦除 {device_path} 上的分区表。")
|
||||||
# 使用 LvmOperations 中通用的 _execute_shell_command 来执行 parted 命令
|
|
||||||
# 这里不需要 show_dialog=False,因为这个操作本身就是同步且需要用户确认的
|
|
||||||
success, _, stderr = self.lvm_ops._execute_shell_command(
|
success, _, stderr = self.lvm_ops._execute_shell_command(
|
||||||
["parted", "-s", device_path, "mklabel", "gpt"], # 默认使用 gpt 分区表类型
|
["parted", "-s", device_path, "mklabel", "gpt"],
|
||||||
f"擦除 {device_path} 上的分区表失败"
|
f"擦除 {device_path} 上的分区表失败"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -219,9 +271,7 @@ class MainWindow(QMainWindow):
|
|||||||
QMessageBox.information(self, "成功", f"设备 {device_path} 上的分区表已成功擦除。")
|
QMessageBox.information(self, "成功", f"设备 {device_path} 上的分区表已成功擦除。")
|
||||||
self.refresh_all_info()
|
self.refresh_all_info()
|
||||||
else:
|
else:
|
||||||
# 错误信息已由 _execute_shell_command 处理并显示
|
|
||||||
pass
|
pass
|
||||||
# --- 新增方法结束 ---
|
|
||||||
|
|
||||||
def _handle_create_partition(self, disk_path, dev_data):
|
def _handle_create_partition(self, disk_path, dev_data):
|
||||||
total_disk_mib = 0.0
|
total_disk_mib = 0.0
|
||||||
@@ -260,7 +310,6 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
if dev_data.get('children'):
|
if dev_data.get('children'):
|
||||||
logger.debug(f"磁盘 {disk_path} 存在现有分区,尝试计算下一个分区起始位置。")
|
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)
|
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:
|
if calculated_start_mib is None or largest_free_space_mib is None:
|
||||||
QMessageBox.critical(self, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置。")
|
QMessageBox.critical(self, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置。")
|
||||||
@@ -271,8 +320,8 @@ class MainWindow(QMainWindow):
|
|||||||
max_available_mib = 0.0
|
max_available_mib = 0.0
|
||||||
else:
|
else:
|
||||||
logger.debug(f"磁盘 {disk_path} 没有现有分区,假定从 0 MiB 开始,最大可用空间为整个磁盘。")
|
logger.debug(f"磁盘 {disk_path} 没有现有分区,假定从 0 MiB 开始,最大可用空间为整个磁盘。")
|
||||||
max_available_mib = max(0.0, total_disk_mib - 1.0) # 留一点空间,避免边界问题
|
max_available_mib = max(0.0, total_disk_mib - 1.0)
|
||||||
start_position_mib = 1.0 # 现代分区表通常从1MB或更大偏移开始
|
start_position_mib = 1.0
|
||||||
logger.debug(f"磁盘 {disk_path} 没有现有分区,假定从 {start_position_mib} MiB 开始,最大可用空间为 {max_available_mib} MiB。")
|
logger.debug(f"磁盘 {disk_path} 没有现有分区,假定从 {start_position_mib} MiB 开始,最大可用空间为 {max_available_mib} MiB。")
|
||||||
|
|
||||||
dialog = CreatePartitionDialog(self, disk_path, total_disk_mib, max_available_mib)
|
dialog = CreatePartitionDialog(self, disk_path, total_disk_mib, max_available_mib)
|
||||||
@@ -308,18 +357,14 @@ class MainWindow(QMainWindow):
|
|||||||
"""
|
"""
|
||||||
调用 DiskOperations 的异步格式化方法。
|
调用 DiskOperations 的异步格式化方法。
|
||||||
"""
|
"""
|
||||||
# DiskOperations.format_partition 会处理用户确认和启动后台线程
|
|
||||||
self.disk_ops.format_partition(device_path)
|
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):
|
def on_disk_formatting_finished(self, success, device_path, stdout, stderr):
|
||||||
"""
|
"""
|
||||||
接收 DiskOperations 发出的格式化完成信号,并刷新界面。
|
接收 DiskOperations 发出的格式化完成信号,并刷新界面。
|
||||||
"""
|
"""
|
||||||
logger.info(f"格式化完成信号接收: 设备 {device_path}, 成功: {success}")
|
logger.info(f"格式化完成信号接收: 设备 {device_path}, 成功: {success}")
|
||||||
# QMessageBox 已经在 DiskOperations 的 _on_formatting_finished 中处理
|
self.refresh_all_info()
|
||||||
self.refresh_all_info() # 刷新所有信息以显示更新后的文件系统类型等
|
|
||||||
|
|
||||||
# --- RAID 管理 Tab ---
|
# --- RAID 管理 Tab ---
|
||||||
def refresh_raid_info(self):
|
def refresh_raid_info(self):
|
||||||
@@ -336,7 +381,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.ui.treeWidget_raid.header().setSectionResizeMode(i, QHeaderView.ResizeToContents)
|
self.ui.treeWidget_raid.header().setSectionResizeMode(i, QHeaderView.ResizeToContents)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raid_arrays = self.system_manager.get_mdadm_arrays() # 现在会返回所有阵列,包括停止的
|
raid_arrays = self.system_manager.get_mdadm_arrays()
|
||||||
if not raid_arrays:
|
if not raid_arrays:
|
||||||
item = QTreeWidgetItem(self.ui.treeWidget_raid)
|
item = QTreeWidgetItem(self.ui.treeWidget_raid)
|
||||||
item.setText(0, "未找到RAID阵列。")
|
item.setText(0, "未找到RAID阵列。")
|
||||||
@@ -350,14 +395,13 @@ class MainWindow(QMainWindow):
|
|||||||
logger.warning(f"RAID阵列 '{array.get('name', '未知')}' 的设备路径无效,跳过。")
|
logger.warning(f"RAID阵列 '{array.get('name', '未知')}' 的设备路径无效,跳过。")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 对于停止状态的阵列,挂载点可能不存在或不相关
|
|
||||||
current_mount_point = ""
|
current_mount_point = ""
|
||||||
if array.get('state') != 'Stopped (Configured)':
|
if array.get('state') != 'Stopped (Configured)':
|
||||||
current_mount_point = self.system_manager.get_mountpoint_for_device(array_path)
|
current_mount_point = self.system_manager.get_mountpoint_for_device(array_path)
|
||||||
|
|
||||||
array_item.setText(0, array_path)
|
array_item.setText(0, array_path)
|
||||||
array_item.setText(1, array.get('level', 'N/A'))
|
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(3, array.get('array_size', 'N/A'))
|
||||||
array_item.setText(4, array.get('active_devices', 'N/A'))
|
array_item.setText(4, array.get('active_devices', 'N/A'))
|
||||||
array_item.setText(5, array.get('failed_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.setText(11, current_mount_point if current_mount_point else "")
|
||||||
array_item.setExpanded(True)
|
array_item.setExpanded(True)
|
||||||
|
|
||||||
# 存储完整的阵列数据,包括状态,供上下文菜单使用
|
|
||||||
array_item.setData(0, Qt.UserRole, array)
|
array_item.setData(0, Qt.UserRole, array)
|
||||||
|
|
||||||
# 对于停止状态的阵列,成员设备可能为空,跳过显示子项
|
|
||||||
if array.get('state') != 'Stopped (Configured)':
|
if array.get('state') != 'Stopped (Configured)':
|
||||||
for member in array.get('member_devices', []):
|
for member in array.get('member_devices', []):
|
||||||
member_item = QTreeWidgetItem(array_item)
|
member_item = QTreeWidgetItem(array_item)
|
||||||
member_item.setText(0, f" {member.get('device_path', 'N/A')}")
|
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(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)):
|
for i in range(len(raid_headers)):
|
||||||
self.ui.treeWidget_raid.resizeColumnToContents(i)
|
self.ui.treeWidget_raid.resizeColumnToContents(i)
|
||||||
@@ -397,7 +438,7 @@ class MainWindow(QMainWindow):
|
|||||||
create_raid_action.triggered.connect(self._handle_create_raid_array)
|
create_raid_action.triggered.connect(self._handle_create_raid_array)
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
|
|
||||||
if item and item.parent() is None: # 只针对顶层阵列项
|
if item and item.parent() is None:
|
||||||
array_data = item.data(0, Qt.UserRole)
|
array_data = item.data(0, Qt.UserRole)
|
||||||
if not array_data:
|
if not array_data:
|
||||||
logger.warning(f"无法获取 RAID 阵列 {item.text(0)} 的详细数据。")
|
logger.warning(f"无法获取 RAID 阵列 {item.text(0)} 的详细数据。")
|
||||||
@@ -406,7 +447,7 @@ class MainWindow(QMainWindow):
|
|||||||
array_path = array_data.get('device')
|
array_path = array_data.get('device')
|
||||||
array_state = array_data.get('state', 'N/A')
|
array_state = array_data.get('state', 'N/A')
|
||||||
member_devices = [m.get('device_path') for m in array_data.get('member_devices', [])]
|
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':
|
if not array_path or array_path == 'N/A':
|
||||||
logger.warning(f"RAID阵列 '{array_data.get('name', '未知')}' 的设备路径无效,无法显示操作。")
|
logger.warning(f"RAID阵列 '{array_data.get('name', '未知')}' 的设备路径无效,无法显示操作。")
|
||||||
@@ -415,11 +456,9 @@ class MainWindow(QMainWindow):
|
|||||||
if array_state == 'Stopped (Configured)':
|
if array_state == 'Stopped (Configured)':
|
||||||
activate_action = menu.addAction(f"激活阵列 {array_path}")
|
activate_action = menu.addAction(f"激活阵列 {array_path}")
|
||||||
activate_action.triggered.connect(lambda: self._handle_activate_raid_array(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 = menu.addAction(f"删除配置文件条目 (UUID: {array_uuid})")
|
||||||
delete_config_action.triggered.connect(lambda: self._handle_delete_configured_raid_array(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)
|
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 != '':
|
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))
|
mount_action.triggered.connect(lambda: self._handle_mount(array_path))
|
||||||
menu.addSeparator()
|
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 = menu.addAction(f"停止阵列 {array_path}")
|
||||||
stop_action.triggered.connect(lambda: self._handle_stop_raid_array(array_path))
|
stop_action.triggered.connect(lambda: self._handle_stop_raid_array(array_path))
|
||||||
|
|
||||||
# <--- 修改:删除活动阵列的动作,传递 UUID
|
|
||||||
delete_action = menu.addAction(f"删除阵列 {array_path}")
|
delete_action = menu.addAction(f"删除阵列 {array_path}")
|
||||||
delete_action.triggered.connect(lambda: self._handle_delete_active_raid_array(array_path, member_devices, array_uuid))
|
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 阵列。
|
处理激活已停止的 RAID 阵列。
|
||||||
"""
|
"""
|
||||||
item = self.ui.treeWidget_raid.currentItem() # 获取当前选中的项
|
item = self.ui.treeWidget_raid.currentItem()
|
||||||
if not item or item.parent() is not None: # 确保是顶层阵列项
|
if not item or item.parent() is not None:
|
||||||
logger.warning("未选择有效的 RAID 阵列进行激活。")
|
logger.warning("未选择有效的 RAID 阵列进行激活。")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -477,7 +521,7 @@ class MainWindow(QMainWindow):
|
|||||||
logger.info(f"用户取消了激活 RAID 阵列 {array_path} 的操作。")
|
logger.info(f"用户取消了激活 RAID 阵列 {array_path} 的操作。")
|
||||||
return
|
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()
|
self.refresh_all_info()
|
||||||
|
|
||||||
def _handle_create_raid_array(self):
|
def _handle_create_raid_array(self):
|
||||||
@@ -498,16 +542,16 @@ class MainWindow(QMainWindow):
|
|||||||
if self.raid_ops.stop_raid_array(array_path):
|
if self.raid_ops.stop_raid_array(array_path):
|
||||||
self.refresh_all_info()
|
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 阵列的操作。
|
处理删除一个活动的 RAID 阵列的操作。
|
||||||
"""
|
"""
|
||||||
if self.raid_ops.delete_active_raid_array(array_path, member_devices, uuid): # <--- 调用修改后的方法
|
if self.raid_ops.delete_active_raid_array(array_path, member_devices, uuid):
|
||||||
self.refresh_all_info()
|
self.refresh_all_info()
|
||||||
|
|
||||||
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):
|
if self.raid_ops.delete_configured_raid_array(uuid):
|
||||||
self.refresh_all_info()
|
self.refresh_all_info()
|
||||||
@@ -516,9 +560,7 @@ class MainWindow(QMainWindow):
|
|||||||
"""
|
"""
|
||||||
处理 RAID 阵列的格式化。
|
处理 RAID 阵列的格式化。
|
||||||
"""
|
"""
|
||||||
# 这将调用 DiskOperations 的异步格式化方法
|
|
||||||
self.disk_ops.format_partition(array_path)
|
self.disk_ops.format_partition(array_path)
|
||||||
# 刷新操作会在 on_disk_formatting_finished 中触发,所以这里不需要 refresh_all_info()
|
|
||||||
|
|
||||||
# --- LVM 管理 Tab ---
|
# --- LVM 管理 Tab ---
|
||||||
def refresh_lvm_info(self):
|
def refresh_lvm_info(self):
|
||||||
@@ -697,6 +739,11 @@ class MainWindow(QMainWindow):
|
|||||||
mount_lv_action.triggered.connect(lambda: self._handle_mount(lv_path))
|
mount_lv_action.triggered.connect(lambda: self._handle_mount(lv_path))
|
||||||
menu.addSeparator()
|
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 = menu.addAction(f"删除逻辑卷 {lv_name}")
|
||||||
delete_lv_action.triggered.connect(lambda: self._handle_delete_lv(lv_name, vg_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}")
|
available_pvs.append(f"/dev/{pv_name}")
|
||||||
|
|
||||||
if not available_pvs:
|
if not available_pvs:
|
||||||
QMessageBox.warning(self, "警告", "没有可用于创建卷组的物理卷。请确保有未分配给任何卷组的物理卷。")
|
QMessageBox.warning(self, "警告", "没有可用于创建卷组的物理卷。")
|
||||||
return
|
return
|
||||||
|
|
||||||
dialog = CreateVgDialog(self, available_pvs)
|
dialog = CreateVgDialog(self, available_pvs)
|
||||||
@@ -758,27 +805,23 @@ class MainWindow(QMainWindow):
|
|||||||
if vg_name and vg_name != 'N/A':
|
if vg_name and vg_name != 'N/A':
|
||||||
available_vgs.append(vg_name)
|
available_vgs.append(vg_name)
|
||||||
|
|
||||||
# 将字符串转换为小写并去除可能存在的 '<' 符号,以便更准确地匹配
|
|
||||||
free_size_str = vg.get('vg_free', '0B').strip().lower()
|
free_size_str = vg.get('vg_free', '0B').strip().lower()
|
||||||
current_vg_size_gb = 0.0
|
current_vg_size_gb = 0.0
|
||||||
|
|
||||||
# 修正正则表达式:
|
|
||||||
# 1. 允许可选的 '<' 符号在数字前
|
|
||||||
# 2. 确保单位匹配正确,如果单位缺失,默认按 GB 处理 (LVM 的 vg_free 常常默认 GB)
|
|
||||||
match = re.match(r'<?(\d+\.?\d*)\s*([gmktb])?', free_size_str)
|
match = re.match(r'<?(\d+\.?\d*)\s*([gmktb])?', free_size_str)
|
||||||
if match:
|
if match:
|
||||||
value = float(match.group(1))
|
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':
|
if unit == 'k':
|
||||||
current_vg_size_gb = value / (1024 * 1024) # KB to GB
|
current_vg_size_gb = value / (1024 * 1024)
|
||||||
elif unit == 'm':
|
elif unit == 'm':
|
||||||
current_vg_size_gb = value / 1024 # MB to GB
|
current_vg_size_gb = value / 1024
|
||||||
elif unit == 'g' or unit is None: # 如果单位是 'g' 或没有指定单位,则认为是 GB
|
elif unit == 'g' or unit is None:
|
||||||
current_vg_size_gb = value
|
current_vg_size_gb = value
|
||||||
elif unit == 't':
|
elif unit == 't':
|
||||||
current_vg_size_gb = value * 1024 # TB to GB
|
current_vg_size_gb = value * 1024
|
||||||
elif unit == 'b': # Bytes to GB
|
elif unit == 'b':
|
||||||
current_vg_size_gb = value / (1024 * 1024 * 1024)
|
current_vg_size_gb = value / (1024 * 1024 * 1024)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"未知LVM单位: '{unit}' for '{free_size_str}'")
|
logger.warning(f"未知LVM单位: '{unit}' for '{free_size_str}'")
|
||||||
|
|||||||
307
occupation_resolver.py
Normal file
307
occupation_resolver.py
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
# occupation_resolver.py
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from typing import List, Tuple, Callable, Optional
|
||||||
|
|
||||||
|
# 假设 QApplication 和 QMessageBox 在主 UI 线程中可用,
|
||||||
|
# 并且我们在这里直接使用它们进行用户交互。
|
||||||
|
# 在更严格的架构中,UI交互应该通过回调或事件机制与核心逻辑分离。
|
||||||
|
# from PyQt5.QtWidgets import QMessageBox
|
||||||
|
from PySide6.QtWidgets import (QApplication, QMainWindow, QTreeWidgetItem,
|
||||||
|
QMessageBox, QHeaderView, QMenu, QInputDialog, QDialog)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class OccupationResolver:
|
||||||
|
"""
|
||||||
|
负责检查和解除设备占用的类。
|
||||||
|
处理包括内核挂载、嵌套挂载和用户进程占用等情况。
|
||||||
|
"""
|
||||||
|
def __init__(self, shell_executor_func: Callable, mount_info_getter_func: Callable):
|
||||||
|
"""
|
||||||
|
初始化设备占用解决器。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
shell_executor_func: 一个可调用对象,用于执行 shell 命令。
|
||||||
|
期望签名: (cmd: List[str], error_msg: str, show_dialog: bool,
|
||||||
|
suppress_critical_dialog_on_stderr_match: Tuple,
|
||||||
|
expected_non_zero_exit_codes: List[int]) -> Tuple[bool, str, str]
|
||||||
|
mount_info_getter_func: 一个可调用对象,用于获取设备的挂载点。
|
||||||
|
期望签名: (device_path: str) -> Optional[str]
|
||||||
|
"""
|
||||||
|
self._execute_shell_command = shell_executor_func
|
||||||
|
self._get_mountpoint_for_device = mount_info_getter_func
|
||||||
|
|
||||||
|
def _run_command(self, cmd: List[str], error_msg: str, **kwargs) -> Tuple[bool, str, str]:
|
||||||
|
"""
|
||||||
|
执行 shell 命令的包装器。
|
||||||
|
"""
|
||||||
|
return self._execute_shell_command(cmd, error_msg, **kwargs)
|
||||||
|
|
||||||
|
def get_all_mounts_under_path(self, base_path: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
获取指定路径下所有嵌套的挂载点。
|
||||||
|
返回一个挂载点列表,从最深层到最浅层排序。
|
||||||
|
"""
|
||||||
|
mounts = []
|
||||||
|
# 尝试使用 findmnt 命令更可靠地获取嵌套挂载信息
|
||||||
|
# -l: 列出所有挂载
|
||||||
|
# -o TARGET: 只输出目标路径
|
||||||
|
# -n: 不显示标题行
|
||||||
|
# -r: 原始输出,方便解析
|
||||||
|
# --target <base_path>: 限制只查找 base_path 下的挂载
|
||||||
|
# -t noautofs,nosnapfs,nofuse,nocifs,nonfs,notmpfs,nobind: 排除一些不相关的或虚拟的挂载
|
||||||
|
findmnt_cmd = ["findmnt", "-l", "-o", "TARGET", "-n", "-r", "--target", base_path,
|
||||||
|
"-t", "noautofs,nosnapfs,nofuse,nocifs,nonfs,notmpfs,nobind"]
|
||||||
|
success, stdout, stderr = self._run_command(findmnt_cmd, f"获取 {base_path} 下的挂载信息失败", show_dialog=False)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
logger.warning(f"findmnt 命令失败,尝试解析 'mount' 命令输出: {stderr}")
|
||||||
|
# 如果 findmnt 失败或不可用,则回退到解析 'mount' 命令输出
|
||||||
|
success, stdout, stderr = self._run_command(["mount"], "获取挂载信息失败", show_dialog=False)
|
||||||
|
if not success:
|
||||||
|
logger.error(f"获取挂载信息失败: {stderr}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
for line in stdout.splitlines():
|
||||||
|
match = re.search(r' on (\S+) type ', line)
|
||||||
|
if match:
|
||||||
|
mount_point = match.group(1)
|
||||||
|
# 确保是嵌套挂载点而不是基础挂载点本身
|
||||||
|
if mount_point.startswith(base_path) and mount_point != base_path:
|
||||||
|
mounts.append(mount_point)
|
||||||
|
else:
|
||||||
|
for line in stdout.splitlines():
|
||||||
|
mount_point = line.strip()
|
||||||
|
# findmnt --target 已经过滤了,这里只需确保不是空行
|
||||||
|
if mount_point and mount_point.startswith(base_path) and mount_point != base_path:
|
||||||
|
mounts.append(mount_point)
|
||||||
|
|
||||||
|
# 排序:从最深层到最浅层,以便正确卸载。路径越长,通常越深层。
|
||||||
|
mounts.sort(key=lambda x: len(x.split('/')), reverse=True)
|
||||||
|
logger.debug(f"在 {base_path} 下找到的嵌套挂载点(从深到浅): {mounts}")
|
||||||
|
return mounts
|
||||||
|
|
||||||
|
def _unmount_nested_mounts(self, base_mount_point: str) -> bool:
|
||||||
|
"""
|
||||||
|
尝试卸载指定基础挂载点下的所有嵌套挂载。
|
||||||
|
"""
|
||||||
|
nested_mounts = self.get_all_mounts_under_path(base_mount_point)
|
||||||
|
if not nested_mounts:
|
||||||
|
logger.debug(f"在 {base_mount_point} 下没有找到嵌套挂载点。")
|
||||||
|
return True # 没有需要卸载的嵌套挂载
|
||||||
|
|
||||||
|
logger.info(f"开始卸载 {base_mount_point} 下的嵌套挂载点: {nested_mounts}")
|
||||||
|
all_nested_unmounted = True
|
||||||
|
for mount_point in nested_mounts:
|
||||||
|
if not self._unmount_target(mount_point):
|
||||||
|
logger.error(f"未能卸载嵌套挂载点 {mount_point}。")
|
||||||
|
all_nested_unmounted = False
|
||||||
|
# 即使一个失败,也尝试卸载其他,但标记整体失败
|
||||||
|
return all_nested_unmounted
|
||||||
|
|
||||||
|
def _unmount_target(self, target_path: str) -> bool:
|
||||||
|
"""
|
||||||
|
尝试卸载一个目标(设备或挂载点)。
|
||||||
|
先尝试普通卸载,如果失败则尝试强制卸载,再失败则尝试懒惰卸载。
|
||||||
|
"""
|
||||||
|
logger.info(f"尝试卸载 {target_path}。")
|
||||||
|
success, stdout, stderr = self._run_command(
|
||||||
|
["umount", target_path],
|
||||||
|
f"卸载 {target_path} 失败",
|
||||||
|
show_dialog=False,
|
||||||
|
expected_non_zero_exit_codes=[1] # umount 返回 1 表示未挂载或忙碌
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
logger.info(f"成功卸载 {target_path}。")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# 检查是否因为未挂载而失败
|
||||||
|
if "not mounted" in stderr.lower() or "未挂载" in stderr:
|
||||||
|
logger.info(f"目标 {target_path} 未挂载,无需卸载。")
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.warning(f"卸载 {target_path} 失败: {stderr}")
|
||||||
|
# 尝试强制卸载 -f
|
||||||
|
logger.info(f"尝试强制卸载 {target_path} (umount -f)。")
|
||||||
|
success_force, stdout_force, stderr_force = self._run_command(
|
||||||
|
["umount", "-f", target_path],
|
||||||
|
f"强制卸载 {target_path} 失败",
|
||||||
|
show_dialog=False,
|
||||||
|
expected_non_zero_exit_codes=[1]
|
||||||
|
)
|
||||||
|
if success_force:
|
||||||
|
logger.info(f"成功强制卸载 {target_path}。")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"强制卸载 {target_path} 失败: {stderr_force}")
|
||||||
|
# 尝试懒惰卸载 -l
|
||||||
|
logger.info(f"尝试懒惰卸载 {target_path} (umount -l)。")
|
||||||
|
success_lazy, stdout_lazy, stderr_lazy = self._run_command(
|
||||||
|
["umount", "-l", target_path],
|
||||||
|
f"懒惰卸载 {target_path} 失败",
|
||||||
|
show_dialog=False,
|
||||||
|
expected_non_zero_exit_codes=[1]
|
||||||
|
)
|
||||||
|
if success_lazy:
|
||||||
|
logger.info(f"成功懒惰卸载 {target_path}。")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"懒惰卸载 {target_path} 失败: {stderr_lazy}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _kill_pids(self, pids: List[str]) -> bool:
|
||||||
|
"""
|
||||||
|
终止指定PID的进程。
|
||||||
|
"""
|
||||||
|
all_killed = True
|
||||||
|
for pid in pids:
|
||||||
|
logger.info(f"尝试终止进程 {pid}。")
|
||||||
|
kill_success, _, kill_stderr = self._run_command(
|
||||||
|
["kill", "-9", pid],
|
||||||
|
f"终止进程 {pid} 失败",
|
||||||
|
show_dialog=False # UI 交互在更高层处理
|
||||||
|
)
|
||||||
|
if not kill_success:
|
||||||
|
logger.error(f"终止进程 {pid} 失败: {kill_stderr}")
|
||||||
|
all_killed = False
|
||||||
|
else:
|
||||||
|
logger.info(f"成功终止进程 {pid}。")
|
||||||
|
return all_killed
|
||||||
|
|
||||||
|
def resolve_occupation(self, device_path: str) -> bool:
|
||||||
|
"""
|
||||||
|
尝试解除设备占用,包括处理嵌套挂载和终止进程。
|
||||||
|
返回 True 如果成功解除占用,否则返回 False。
|
||||||
|
"""
|
||||||
|
logger.info(f"开始尝试解除设备 {device_path} 的占用。")
|
||||||
|
|
||||||
|
# 1. 获取设备的当前主挂载点
|
||||||
|
main_mount_point = self._get_mountpoint_for_device(device_path)
|
||||||
|
|
||||||
|
# 2. 如果设备有主挂载点,先尝试卸载所有嵌套挂载
|
||||||
|
if main_mount_point and main_mount_point != 'N/A' and main_mount_point != '[SWAP]':
|
||||||
|
logger.debug(f"设备 {device_path} 的主挂载点是 {main_mount_point}。")
|
||||||
|
|
||||||
|
# 尝试卸载嵌套挂载,并循环几次,因为某些挂载可能需要多次尝试
|
||||||
|
max_unmount_attempts = 3
|
||||||
|
for attempt in range(max_unmount_attempts):
|
||||||
|
logger.info(f"尝试卸载嵌套挂载点 (第 {attempt + 1} 次尝试)。")
|
||||||
|
if self._unmount_nested_mounts(main_mount_point):
|
||||||
|
logger.info(f"所有嵌套挂载点已成功卸载或已不存在。")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.warning(f"卸载嵌套挂载点失败 (第 {attempt + 1} 次尝试),可能需要重试。")
|
||||||
|
if attempt == max_unmount_attempts - 1:
|
||||||
|
QMessageBox.critical(None, "错误", f"未能卸载设备 {device_path} 下的所有嵌套挂载点。")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 尝试卸载主挂载点本身
|
||||||
|
logger.info(f"尝试卸载主挂载点 {main_mount_point}。")
|
||||||
|
if self._unmount_target(main_mount_point):
|
||||||
|
QMessageBox.information(None, "成功", f"设备 {device_path} 及其所有挂载点已成功卸载。")
|
||||||
|
return True # 主挂载点已卸载,设备应该已空闲
|
||||||
|
|
||||||
|
# 3. 检查设备本身或其(可能已不存在的)主挂载点是否仍被占用 (fuser)
|
||||||
|
# 即使主挂载点已卸载,也可能存在进程直接占用设备文件的情况,或者之前卸载失败。
|
||||||
|
fuser_targets = [device_path]
|
||||||
|
if main_mount_point and main_mount_point != 'N/A' and main_mount_point != '[SWAP]' and main_mount_point not in fuser_targets:
|
||||||
|
fuser_targets.append(main_mount_point)
|
||||||
|
|
||||||
|
pids_to_kill = set()
|
||||||
|
kernel_mounts_found = False
|
||||||
|
occupation_info_lines = []
|
||||||
|
|
||||||
|
for target in fuser_targets:
|
||||||
|
logger.debug(f"执行 fuser -vm {target} 检查占用。")
|
||||||
|
success_fuser, stdout_fuser, stderr_fuser = self._run_command(
|
||||||
|
["fuser", "-vm", target],
|
||||||
|
f"检查 {target} 占用失败",
|
||||||
|
show_dialog=False,
|
||||||
|
suppress_critical_dialog_on_stderr_match=(
|
||||||
|
"No such file or directory", "not found", "Usage:", "not mounted"
|
||||||
|
),
|
||||||
|
expected_non_zero_exit_codes=[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
if success_fuser and stdout_fuser:
|
||||||
|
logger.debug(f"fuser -vm {target} 输出:\n{stdout_fuser}")
|
||||||
|
for line in stdout_fuser.splitlines():
|
||||||
|
occupation_info_lines.append(line.strip())
|
||||||
|
|
||||||
|
if "kernel" in line:
|
||||||
|
kernel_mounts_found = True
|
||||||
|
|
||||||
|
match = re.match(r'^\S+:\s+\S+\s+(?P<id>\d+)\s+.*', line)
|
||||||
|
if match:
|
||||||
|
pids_to_kill.add(match.group('id'))
|
||||||
|
|
||||||
|
pids_to_kill_list = list(pids_to_kill)
|
||||||
|
|
||||||
|
if not kernel_mounts_found and not pids_to_kill_list:
|
||||||
|
QMessageBox.information(None, "信息", f"设备 {device_path} 未被任何进程占用。")
|
||||||
|
logger.info(f"设备 {device_path} 未被任何进程占用。")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 4. 处理剩余的内核挂载(如果嵌套挂载和主挂载卸载失败,这里会再次捕捉到)
|
||||||
|
if kernel_mounts_found:
|
||||||
|
process_list_str = "\n".join(sorted(list(set(occupation_info_lines))))
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
None,
|
||||||
|
"设备占用 - 内核挂载",
|
||||||
|
f"设备 {device_path} 仍被内核挂载占用:\n{process_list_str}\n\n您确定要尝试卸载此设备吗?",
|
||||||
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.No
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.No:
|
||||||
|
logger.info(f"用户取消了卸载设备 {device_path}。")
|
||||||
|
QMessageBox.information(None, "信息", f"已取消卸载设备 {device_path}。")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info(f"再次尝试卸载设备 {device_path}。")
|
||||||
|
if self._unmount_target(device_path): # 尝试直接卸载设备路径
|
||||||
|
QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(None, "错误", f"未能卸载设备 {device_path}。")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 5. 处理用户进程占用
|
||||||
|
if pids_to_kill_list:
|
||||||
|
process_list_str = "\n".join(sorted(list(set(occupation_info_lines))))
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
None,
|
||||||
|
"设备占用 - 进程",
|
||||||
|
f"设备 {device_path} 正在被以下进程占用:\n{process_list_str}\n\n您要强制终止这些进程吗?这可能会导致数据丢失或系统不稳定!",
|
||||||
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.No
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.No:
|
||||||
|
logger.info(f"用户取消了终止占用设备 {device_path} 的进程。")
|
||||||
|
QMessageBox.information(None, "信息", f"已取消终止占用设备 {device_path} 的进程。")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._kill_pids(pids_to_kill_list):
|
||||||
|
QMessageBox.information(None, "成功", f"已尝试终止占用设备 {device_path} 的所有进程。")
|
||||||
|
# 进程终止后,设备可能仍然被挂载,因此再次尝试卸载。
|
||||||
|
if main_mount_point and main_mount_point != 'N/A' and main_mount_point != '[SWAP]':
|
||||||
|
logger.info(f"进程终止后,尝试再次卸载主挂载点 {main_mount_point}。")
|
||||||
|
if self._unmount_target(main_mount_point):
|
||||||
|
QMessageBox.information(None, "信息", f"设备 {device_path} 已成功卸载。")
|
||||||
|
return True
|
||||||
|
else: # 如果没有主挂载点,检查设备本身
|
||||||
|
logger.info(f"进程终止后,尝试再次卸载设备 {device_path}。")
|
||||||
|
if self._unmount_target(device_path):
|
||||||
|
QMessageBox.information(None, "信息", f"设备 {device_path} 已成功卸载。")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 如果进程终止后仍未能卸载
|
||||||
|
QMessageBox.warning(None, "警告", f"进程已终止,但未能成功卸载设备 {device_path}。可能仍有其他问题。")
|
||||||
|
return False # 表示部分成功或需要进一步手动干预
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(None, "错误", f"未能终止所有占用设备 {device_path} 的进程。")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.warning(f"设备 {device_path} 占用解决逻辑未能完全处理。")
|
||||||
|
return False
|
||||||
|
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
name = "PySide Widgets Project"
|
name = "PySide Widgets Project"
|
||||||
|
|
||||||
[tool.pyside6-project]
|
[tool.pyside6-project]
|
||||||
files = ["dialogs.py", "disk_operations.py", "form.ui", "logger_config.py", "lvm_operations.py", "mainwindow.py", "raid_operations.py", "system_info.py"]
|
files = ["dialogs.py", "disk_operations.py", "form.ui", "logger_config.py", "lvm_operations.py", "mainwindow.py", "occupation_resolver.py", "raid_operations.py", "system_info.py"]
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
# raid_operations.py
|
# raid_operations.py
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
from PySide6.QtWidgets import QMessageBox, QInputDialog, QLineEdit # 导入 QInputDialog 和 QLineEdit
|
from PySide6.QtWidgets import QMessageBox, QInputDialog, QLineEdit
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from system_info import SystemInfoManager
|
from system_info import SystemInfoManager
|
||||||
import re
|
import re
|
||||||
import pexpect # <--- 新增导入 pexpect
|
import pexpect
|
||||||
import sys
|
import sys
|
||||||
# NEW: 导入 DiskOperations
|
|
||||||
from disk_operations import DiskOperations
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class RaidOperations:
|
class RaidOperations:
|
||||||
def __init__(self, system_manager: SystemInfoManager, disk_ops: DiskOperations): # <--- Add system_manager and disk_ops parameters
|
def __init__(self, system_manager: SystemInfoManager):
|
||||||
self.system_manager = system_manager
|
self.system_manager = system_manager
|
||||||
self.disk_ops = disk_ops # Store disk_ops instance
|
|
||||||
|
|
||||||
def _execute_shell_command(self, command_list, error_msg_prefix, suppress_critical_dialog_on_stderr_match=None, input_to_command=None):
|
def _execute_shell_command(self, command_list, error_msg_prefix, suppress_critical_dialog_on_stderr_match=None, input_to_command=None):
|
||||||
"""
|
"""
|
||||||
@@ -35,10 +32,9 @@ class RaidOperations:
|
|||||||
logger.info(f"执行命令: {full_cmd_str}")
|
logger.info(f"执行命令: {full_cmd_str}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# SystemInfoManager._run_command 应该负责在 root_privilege=True 时添加 sudo
|
|
||||||
stdout, stderr = self.system_manager._run_command(
|
stdout, stderr = self.system_manager._run_command(
|
||||||
command_list,
|
command_list,
|
||||||
root_privilege=True, # 假设所有 RAID 操作都需要 root 权限
|
root_privilege=True,
|
||||||
check_output=True,
|
check_output=True,
|
||||||
input_data=input_to_command
|
input_data=input_to_command
|
||||||
)
|
)
|
||||||
@@ -54,7 +50,6 @@ class RaidOperations:
|
|||||||
logger.error(f"标准输出: {e.stdout.strip()}")
|
logger.error(f"标准输出: {e.stdout.strip()}")
|
||||||
logger.error(f"标准错误: {stderr_output}")
|
logger.error(f"标准错误: {stderr_output}")
|
||||||
|
|
||||||
# --- 修改开始 ---
|
|
||||||
should_suppress_dialog = False
|
should_suppress_dialog = False
|
||||||
if suppress_critical_dialog_on_stderr_match:
|
if suppress_critical_dialog_on_stderr_match:
|
||||||
if isinstance(suppress_critical_dialog_on_stderr_match, str):
|
if isinstance(suppress_critical_dialog_on_stderr_match, str):
|
||||||
@@ -64,13 +59,12 @@ class RaidOperations:
|
|||||||
for pattern in suppress_critical_dialog_on_stderr_match:
|
for pattern in suppress_critical_dialog_on_stderr_match:
|
||||||
if pattern in stderr_output:
|
if pattern in stderr_output:
|
||||||
should_suppress_dialog = True
|
should_suppress_dialog = True
|
||||||
break # 找到一个匹配就足够了
|
break
|
||||||
|
|
||||||
if should_suppress_dialog:
|
if should_suppress_dialog:
|
||||||
logger.info(f"特定错误 '{stderr_output}' 匹配抑制条件,已抑制错误对话框。")
|
logger.info(f"特定错误 '{stderr_output}' 匹配抑制条件,已抑制错误对话框。")
|
||||||
else:
|
else:
|
||||||
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: {stderr_output}")
|
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: {stderr_output}")
|
||||||
# --- 修改结束 ---
|
|
||||||
|
|
||||||
return False, e.stdout, stderr_output
|
return False, e.stdout, stderr_output
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -87,25 +81,19 @@ class RaidOperations:
|
|||||||
专门处理 mdadm --create 命令的交互式执行。
|
专门处理 mdadm --create 命令的交互式执行。
|
||||||
通过 pexpect 监听提示并弹出 QMessageBox 让用户选择。
|
通过 pexpect 监听提示并弹出 QMessageBox 让用户选择。
|
||||||
"""
|
"""
|
||||||
# pexpect.spawn 需要一个字符串命令,并且 sudo 应该包含在内
|
|
||||||
# 假设 SystemInfoManager._run_command 在 root_privilege=True 时会添加 sudo
|
|
||||||
# 但 pexpect.spawn 需要完整的命令字符串,所以这里手动添加 sudo
|
|
||||||
full_cmd_str = "sudo " + ' '.join(command_list)
|
full_cmd_str = "sudo " + ' '.join(command_list)
|
||||||
logger.info(f"执行交互式命令: {full_cmd_str}")
|
logger.info(f"执行交互式命令: {full_cmd_str}")
|
||||||
|
|
||||||
stdout_buffer = []
|
stdout_buffer = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 增加 timeout,因为用户交互可能需要时间
|
child = pexpect.spawn(full_cmd_str, encoding='utf-8', timeout=300)
|
||||||
child = pexpect.spawn(full_cmd_str, encoding='utf-8', timeout=300) # 增加超时到 5 分钟
|
|
||||||
child.logfile = sys.stdout
|
child.logfile = sys.stdout
|
||||||
|
|
||||||
# --- 1. 优先处理 sudo 密码提示 ---
|
|
||||||
try:
|
try:
|
||||||
# 尝试匹配 sudo 密码提示,设置较短的超时,如果没出现就继续
|
|
||||||
index = child.expect(['[pP]assword for', pexpect.TIMEOUT], timeout=5)
|
index = child.expect(['[pP]assword for', pexpect.TIMEOUT], timeout=5)
|
||||||
if index == 0: # 匹配到密码提示
|
if index == 0:
|
||||||
stdout_buffer.append(child.before) # 捕获提示前的输出
|
stdout_buffer.append(child.before)
|
||||||
password, ok = QInputDialog.getText(
|
password, ok = QInputDialog.getText(
|
||||||
None, "Sudo 密码", "请输入您的 sudo 密码:", QLineEdit.Password
|
None, "Sudo 密码", "请输入您的 sudo 密码:", QLineEdit.Password
|
||||||
)
|
)
|
||||||
@@ -114,45 +102,36 @@ class RaidOperations:
|
|||||||
child.close()
|
child.close()
|
||||||
return False, "".join(stdout_buffer), "Sudo 密码未提供或取消。"
|
return False, "".join(stdout_buffer), "Sudo 密码未提供或取消。"
|
||||||
child.sendline(password)
|
child.sendline(password)
|
||||||
stdout_buffer.append(child.after) # 捕获发送密码后的输出
|
stdout_buffer.append(child.after)
|
||||||
logger.info("已发送 sudo 密码。")
|
logger.info("已发送 sudo 密码。")
|
||||||
elif index == 1: # 超时,未出现密码提示 (可能是 NOPASSWD 或已缓存)
|
elif index == 1:
|
||||||
logger.info("Sudo 未提示输入密码(可能已配置 NOPASSWD 或密码已缓存)。")
|
logger.info("Sudo 未提示输入密码(可能已配置 NOPASSWD 或密码已缓存)。")
|
||||||
stdout_buffer.append(child.before) # 捕获任何初始输出
|
stdout_buffer.append(child.before)
|
||||||
except pexpect.exceptions.EOF:
|
except pexpect.exceptions.EOF:
|
||||||
stdout_buffer.append(child.before)
|
stdout_buffer.append(child.before)
|
||||||
logger.error("命令在 sudo 密码提示前退出。")
|
logger.error("命令在 sudo 密码提示前退出。")
|
||||||
child.close()
|
child.close()
|
||||||
return False, "".join(stdout_buffer), "命令在 sudo 密码提示前退出。"
|
return False, "".join(stdout_buffer), "命令在 sudo 密码提示前退出。"
|
||||||
except pexpect.exceptions.TIMEOUT:
|
except pexpect.exceptions.TIMEOUT:
|
||||||
# 再次捕获超时,确保日志记录
|
|
||||||
logger.info("Sudo 密码提示超时,可能已配置 NOPASSWD 或密码已缓存。")
|
logger.info("Sudo 密码提示超时,可能已配置 NOPASSWD 或密码已缓存。")
|
||||||
stdout_buffer.append(child.before)
|
stdout_buffer.append(child.before)
|
||||||
|
|
||||||
|
prompts_data = [
|
||||||
# 定义交互式提示及其处理方式
|
|
||||||
prompts_data = [ # 重命名为 prompts_data 以避免与循环变量混淆
|
|
||||||
(r"To optimalize recovery speed, .* write-(?:intent|indent) bitmap, do you want to enable it now\? \[y/N\]\s*",
|
(r"To optimalize recovery speed, .* write-(?:intent|indent) bitmap, do you want to enable it now\? \[y/N\]\s*",
|
||||||
"mdadm 建议启用写入意图位图以优化恢复速度。您希望现在启用它吗?"),
|
"mdadm 建议启用写入意图位图以优化恢复速度。您希望现在启用它吗?"),
|
||||||
(r"Continue creating array \[y/N\]\s*",
|
(r"Continue creating array \[y/N\]\s*",
|
||||||
"mdadm 警告:检测到驱动器大小不一致或分区表。创建阵列将覆盖数据。是否仍要继续创建阵列?")
|
"mdadm 警告:检测到驱动器大小不一致或分区表。创建阵列将覆盖数据。是否仍要继续创建阵列?")
|
||||||
]
|
]
|
||||||
|
|
||||||
# 提取所有模式,以便 pexpect.expect 可以同时监听它们
|
|
||||||
patterns_to_expect = [p for p, _ in prompts_data]
|
patterns_to_expect = [p for p, _ in prompts_data]
|
||||||
|
|
||||||
# 循环处理所有可能的提示,直到没有更多提示或命令结束
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
# 尝试匹配任何一个预定义的提示
|
index = child.expect(patterns_to_expect, timeout=10)
|
||||||
# pexpect 会等待直到匹配到其中一个模式,或者超时,或者遇到 EOF
|
stdout_buffer.append(child.before)
|
||||||
index = child.expect(patterns_to_expect, timeout=10) # 每次等待10秒
|
|
||||||
stdout_buffer.append(child.before) # 捕获到提示前的输出
|
|
||||||
|
|
||||||
# 获取匹配到的提示的描述
|
|
||||||
matched_description = prompts_data[index][1]
|
matched_description = prompts_data[index][1]
|
||||||
|
|
||||||
# 弹出对话框询问用户
|
|
||||||
reply = QMessageBox.question(None, "mdadm 提示", matched_description,
|
reply = QMessageBox.question(None, "mdadm 提示", matched_description,
|
||||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
||||||
|
|
||||||
@@ -164,28 +143,20 @@ class RaidOperations:
|
|||||||
logger.info(f"用户对提示 '{matched_description}' 选择 '否'")
|
logger.info(f"用户对提示 '{matched_description}' 选择 '否'")
|
||||||
|
|
||||||
child.sendline(user_choice)
|
child.sendline(user_choice)
|
||||||
stdout_buffer.append(user_choice + '\n') # 记录用户输入
|
stdout_buffer.append(user_choice + '\n')
|
||||||
|
|
||||||
# 等待一小段时间,确保 mdadm 接收并处理了输入,并清空缓冲区
|
|
||||||
# 避免在下一次 expect 之前,旧的输出再次被匹配
|
|
||||||
# child.expect(pexpect.TIMEOUT, timeout=1) # This line might consume output needed for next prompt
|
|
||||||
|
|
||||||
except pexpect.exceptions.TIMEOUT:
|
except pexpect.exceptions.TIMEOUT:
|
||||||
# 如果在等待任何提示时超时,说明没有更多提示了,或者命令已完成。
|
|
||||||
logger.debug("在等待 mdadm 提示时超时,可能没有更多交互。")
|
logger.debug("在等待 mdadm 提示时超时,可能没有更多交互。")
|
||||||
stdout_buffer.append(child.before) # 捕获当前为止的输出
|
stdout_buffer.append(child.before)
|
||||||
break # 跳出 while 循环,进入等待 EOF 阶段
|
break
|
||||||
except pexpect.exceptions.EOF:
|
except pexpect.exceptions.EOF:
|
||||||
# 如果在等待提示时遇到 EOF,说明命令已经执行完毕或失败
|
|
||||||
logger.warning("在等待 mdadm 提示时遇到 EOF。")
|
logger.warning("在等待 mdadm 提示时遇到 EOF。")
|
||||||
stdout_buffer.append(child.before)
|
stdout_buffer.append(child.before)
|
||||||
break # 跳出 while 循环,进入等待 EOF 阶段
|
break
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 增加一个合理的超时时间,以防 mdadm 在后台进行长时间初始化
|
child.expect(pexpect.EOF, timeout=120)
|
||||||
child.expect(pexpect.EOF, timeout=120) # 例如,等待 120 秒
|
stdout_buffer.append(child.before)
|
||||||
stdout_buffer.append(child.before) # 捕获命令结束前的任何剩余输出
|
|
||||||
logger.info("mdadm 命令已正常结束 (通过 EOF 捕获)。")
|
logger.info("mdadm 命令已正常结束 (通过 EOF 捕获)。")
|
||||||
except pexpect.exceptions.TIMEOUT:
|
except pexpect.exceptions.TIMEOUT:
|
||||||
stdout_buffer.append(child.before)
|
stdout_buffer.append(child.before)
|
||||||
@@ -197,12 +168,11 @@ class RaidOperations:
|
|||||||
stdout_buffer.append(child.before)
|
stdout_buffer.append(child.before)
|
||||||
logger.info("mdadm 命令已正常结束 (通过 EOF 捕获)。")
|
logger.info("mdadm 命令已正常结束 (通过 EOF 捕获)。")
|
||||||
|
|
||||||
|
|
||||||
child.close()
|
child.close()
|
||||||
final_output = "".join(stdout_buffer)
|
final_output = "".join(stdout_buffer)
|
||||||
if child.exitstatus == 0:
|
if child.exitstatus == 0:
|
||||||
logger.info(f"交互式命令 {full_cmd_str} 成功完成。")
|
logger.info(f"交互式命令 {full_cmd_str} 成功完成。")
|
||||||
return True, final_output, "" # pexpect 很难区分 stdout/stderr,都视为 stdout
|
return True, final_output, ""
|
||||||
else:
|
else:
|
||||||
logger.error(f"交互式命令 {full_cmd_str} 失败,退出码: {child.exitstatus}")
|
logger.error(f"交互式命令 {full_cmd_str} 失败,退出码: {child.exitstatus}")
|
||||||
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: 命令执行失败,退出码 {child.exitstatus}\n输出: {final_output}")
|
QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: 命令执行失败,退出码 {child.exitstatus}\n输出: {final_output}")
|
||||||
@@ -247,7 +217,6 @@ class RaidOperations:
|
|||||||
:param level: RAID 级别,例如 'raid0', 'raid1', 'raid5' (也可以是整数 0, 1, 5)
|
:param level: RAID 级别,例如 'raid0', 'raid1', 'raid5' (也可以是整数 0, 1, 5)
|
||||||
:param chunk_size: Chunk 大小 (KB)
|
:param chunk_size: Chunk 大小 (KB)
|
||||||
"""
|
"""
|
||||||
# --- 标准化 RAID 级别输入 ---
|
|
||||||
if isinstance(level, int):
|
if isinstance(level, int):
|
||||||
level_str = f"raid{level}"
|
level_str = f"raid{level}"
|
||||||
elif isinstance(level, str):
|
elif isinstance(level, str):
|
||||||
@@ -259,13 +228,11 @@ class RaidOperations:
|
|||||||
QMessageBox.critical(None, "错误", f"不支持的 RAID 级别类型: {type(level)}")
|
QMessageBox.critical(None, "错误", f"不支持的 RAID 级别类型: {type(level)}")
|
||||||
return False
|
return False
|
||||||
level = level_str
|
level = level_str
|
||||||
# --- 标准化 RAID 级别输入结束 ---
|
|
||||||
|
|
||||||
if not devices or len(devices) < 1: # RAID0 理论上可以一个设备,但通常至少两个
|
if not devices or len(devices) < 1:
|
||||||
QMessageBox.critical(None, "错误", "创建 RAID 阵列至少需要一个设备(RAID0)。")
|
QMessageBox.critical(None, "错误", "创建 RAID 阵列至少需要一个设备(RAID0)。")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 检查其他 RAID 级别所需的设备数量
|
|
||||||
if level == "raid1" and len(devices) < 2:
|
if level == "raid1" and len(devices) < 2:
|
||||||
QMessageBox.critical(None, "错误", "RAID1 至少需要两个设备。")
|
QMessageBox.critical(None, "错误", "RAID1 至少需要两个设备。")
|
||||||
return False
|
return False
|
||||||
@@ -275,17 +242,10 @@ class RaidOperations:
|
|||||||
if level == "raid6" and len(devices) < 4:
|
if level == "raid6" and len(devices) < 4:
|
||||||
QMessageBox.critical(None, "错误", "RAID6 至少需要四个设备。")
|
QMessageBox.critical(None, "错误", "RAID6 至少需要四个设备。")
|
||||||
return False
|
return False
|
||||||
if level == "raid10" and len(devices) < 2: # RAID10 至少需要 2 个设备 (例如 1+0)
|
if level == "raid10" and len(devices) < 2:
|
||||||
QMessageBox.critical(None, "错误", "RAID10 至少需要两个设备。")
|
QMessageBox.critical(None, "错误", "RAID10 至少需要两个设备。")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# NEW: 尝试解决所有成员设备的占用问题
|
|
||||||
for dev in devices:
|
|
||||||
if not self.disk_ops._resolve_device_occupation(dev, action_description=f"创建 RAID 阵列,成员 {dev}"):
|
|
||||||
logger.info(f"用户取消或未能解决设备 {dev} 的占用问题,取消创建 RAID 阵列。")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 确认操作
|
|
||||||
reply = QMessageBox.question(None, "确认创建 RAID 阵列",
|
reply = QMessageBox.question(None, "确认创建 RAID 阵列",
|
||||||
f"你确定要使用设备 {', '.join(devices)} 创建 RAID {level} 阵列吗?\n"
|
f"你确定要使用设备 {', '.join(devices)} 创建 RAID {level} 阵列吗?\n"
|
||||||
"此操作将销毁设备上的所有数据!",
|
"此操作将销毁设备上的所有数据!",
|
||||||
@@ -322,23 +282,18 @@ class RaidOperations:
|
|||||||
create_cmd_base = ["mdadm", "--create", array_name, "--level=" + level.replace("raid", ""),
|
create_cmd_base = ["mdadm", "--create", array_name, "--level=" + level.replace("raid", ""),
|
||||||
f"--raid-devices={len(devices)}"]
|
f"--raid-devices={len(devices)}"]
|
||||||
|
|
||||||
# 添加 chunk size (RAID0, RAID5, RAID6, RAID10 通常需要)
|
|
||||||
if level in ["raid0", "raid5", "raid6", "raid10"]:
|
if level in ["raid0", "raid5", "raid6", "raid10"]:
|
||||||
create_cmd_base.append(f"--chunk={chunk_size}K")
|
create_cmd_base.append(f"--chunk={chunk_size}K")
|
||||||
|
|
||||||
create_cmd = create_cmd_base + devices
|
create_cmd = create_cmd_base + devices
|
||||||
|
|
||||||
# 在 --create 命令中添加 --force 选项
|
|
||||||
create_cmd.insert(2, "--force")
|
create_cmd.insert(2, "--force")
|
||||||
|
|
||||||
# 调用新的交互式方法
|
|
||||||
success_create, stdout_create, stderr_create = self._execute_interactive_mdadm_command(
|
success_create, stdout_create, stderr_create = self._execute_interactive_mdadm_command(
|
||||||
create_cmd,
|
create_cmd,
|
||||||
f"创建 RAID 阵列失败"
|
f"创建 RAID 阵列失败"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not success_create:
|
if not success_create:
|
||||||
# pexpect 捕获的输出可能都在 stdout_create 中
|
|
||||||
if "Array name" in stdout_create and "is in use already" in stdout_create:
|
if "Array name" in stdout_create and "is in use already" in stdout_create:
|
||||||
QMessageBox.critical(None, "错误", f"创建 RAID 阵列失败:阵列名称 {array_name} 已被占用。请尝试停止或删除现有阵列。")
|
QMessageBox.critical(None, "错误", f"创建 RAID 阵列失败:阵列名称 {array_name} 已被占用。请尝试停止或删除现有阵列。")
|
||||||
return False
|
return False
|
||||||
@@ -350,7 +305,6 @@ class RaidOperations:
|
|||||||
examine_scan_cmd = ["mdadm", "--examine", "--scan"]
|
examine_scan_cmd = ["mdadm", "--examine", "--scan"]
|
||||||
success_scan, scan_stdout, _ = self._execute_shell_command(examine_scan_cmd, "扫描 mdadm 配置失败")
|
success_scan, scan_stdout, _ = self._execute_shell_command(examine_scan_cmd, "扫描 mdadm 配置失败")
|
||||||
if success_scan:
|
if success_scan:
|
||||||
# --- 新增:确保 /etc 目录存在 ---
|
|
||||||
mkdir_cmd = ["mkdir", "-p", "/etc"]
|
mkdir_cmd = ["mkdir", "-p", "/etc"]
|
||||||
success_mkdir, _, stderr_mkdir = self._execute_shell_command(
|
success_mkdir, _, stderr_mkdir = self._execute_shell_command(
|
||||||
mkdir_cmd,
|
mkdir_cmd,
|
||||||
@@ -359,13 +313,9 @@ class RaidOperations:
|
|||||||
if not success_mkdir:
|
if not success_mkdir:
|
||||||
logger.error(f"无法创建 /etc 目录,更新 mdadm.conf 失败。错误: {stderr_mkdir}")
|
logger.error(f"无法创建 /etc 目录,更新 mdadm.conf 失败。错误: {stderr_mkdir}")
|
||||||
QMessageBox.critical(None, "错误", f"无法创建 /etc 目录,更新 mdadm.conf 失败。\n详细信息: {stderr_mkdir}")
|
QMessageBox.critical(None, "错误", f"无法创建 /etc 目录,更新 mdadm.conf 失败。\n详细信息: {stderr_mkdir}")
|
||||||
return False # 如果连目录都无法创建,则认为创建阵列失败,因为无法保证重启后自动识别
|
return False
|
||||||
logger.info("已确保 /etc 目录存在。")
|
logger.info("已确保 /etc 目录存在。")
|
||||||
# --- 新增结束 ---
|
|
||||||
|
|
||||||
# 这里需要确保 /etc/mdadm.conf 存在且可写入
|
|
||||||
# _execute_shell_command 会自动添加 sudo
|
|
||||||
# 使用 tee -a 而不是 >> 来确保 sudo 权限下的写入
|
|
||||||
success_append, _, _ = self._execute_shell_command(
|
success_append, _, _ = self._execute_shell_command(
|
||||||
["bash", "-c", f"echo '{scan_stdout.strip()}' | tee -a /etc/mdadm.conf > /dev/null"],
|
["bash", "-c", f"echo '{scan_stdout.strip()}' | tee -a /etc/mdadm.conf > /dev/null"],
|
||||||
"更新 /etc/mdadm/mdadm.conf 失败"
|
"更新 /etc/mdadm/mdadm.conf 失败"
|
||||||
@@ -395,11 +345,12 @@ class RaidOperations:
|
|||||||
|
|
||||||
logger.info(f"尝试停止 RAID 阵列: {array_path}")
|
logger.info(f"尝试停止 RAID 阵列: {array_path}")
|
||||||
|
|
||||||
# NEW: 尝试卸载阵列(如果已挂载),利用 DiskOperations 的增强卸载功能
|
# Original umount call (not using disk_ops.unmount_partition)
|
||||||
# unmount_partition 内部会尝试解决设备占用问题
|
self._execute_shell_command(
|
||||||
if not self.disk_ops.unmount_partition(array_path, show_dialog_on_error=False):
|
["umount", array_path],
|
||||||
logger.warning(f"未能成功卸载 RAID 阵列 {array_path}。尝试继续停止操作。")
|
f"尝试卸载 {array_path} 失败",
|
||||||
# 即使卸载失败,我们仍然尝试停止 mdadm 阵列,因为有时 mdadm --stop 可以在设备忙时强制停止。
|
suppress_critical_dialog_on_stderr_match=("not mounted", "未挂载")
|
||||||
|
)
|
||||||
|
|
||||||
if not self._execute_shell_command(["mdadm", "--stop", array_path], f"停止 RAID 阵列 {array_path} 失败")[0]:
|
if not self._execute_shell_command(["mdadm", "--stop", array_path], f"停止 RAID 阵列 {array_path} 失败")[0]:
|
||||||
return False
|
return False
|
||||||
@@ -408,7 +359,7 @@ class RaidOperations:
|
|||||||
QMessageBox.information(None, "成功", f"成功停止 RAID 阵列 {array_path}。")
|
QMessageBox.information(None, "成功", f"成功停止 RAID 阵列 {array_path}。")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def delete_active_raid_array(self, array_path, member_devices, uuid): # <--- 修改方法名并添加 uuid 参数
|
def delete_active_raid_array(self, array_path, member_devices, uuid):
|
||||||
"""
|
"""
|
||||||
删除一个活动的 RAID 阵列。
|
删除一个活动的 RAID 阵列。
|
||||||
此操作将停止阵列、清除成员设备上的超级块,并删除 mdadm.conf 中的配置。
|
此操作将停止阵列、清除成员设备上的超级块,并删除 mdadm.conf 中的配置。
|
||||||
@@ -427,7 +378,6 @@ class RaidOperations:
|
|||||||
logger.info(f"尝试删除活动 RAID 阵列: {array_path} (UUID: {uuid})")
|
logger.info(f"尝试删除活动 RAID 阵列: {array_path} (UUID: {uuid})")
|
||||||
|
|
||||||
# 1. 停止阵列
|
# 1. 停止阵列
|
||||||
# stop_raid_array 内部会调用 self.disk_ops.unmount_partition 来尝试卸载并解决占用
|
|
||||||
if not self.stop_raid_array(array_path):
|
if not self.stop_raid_array(array_path):
|
||||||
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 失败,因为无法停止阵列。")
|
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 失败,因为无法停止阵列。")
|
||||||
return False
|
return False
|
||||||
@@ -459,7 +409,7 @@ class RaidOperations:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 的配置失败: {e}")
|
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 的配置失败: {e}")
|
||||||
logger.error(f"删除 RAID 阵列 {array_path} 的配置失败: {e}")
|
logger.error(f"删除 RAID 阵列 {array_path} 的配置失败: {e}")
|
||||||
return False # 如果配置文件删除失败,也视为整体操作失败
|
return False
|
||||||
|
|
||||||
if success_all_cleared:
|
if success_all_cleared:
|
||||||
logger.info(f"成功删除 RAID 阵列 {array_path} 并清除了成员设备超级块。")
|
logger.info(f"成功删除 RAID 阵列 {array_path} 并清除了成员设备超级块。")
|
||||||
@@ -469,7 +419,7 @@ class RaidOperations:
|
|||||||
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 完成,但部分成员设备超级块未能完全清除。")
|
QMessageBox.critical(None, "错误", f"删除 RAID 阵列 {array_path} 完成,但部分成员设备超级块未能完全清除。")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_configured_raid_array(self, uuid): # <--- 新增方法
|
def delete_configured_raid_array(self, uuid):
|
||||||
"""
|
"""
|
||||||
只删除 mdadm.conf 中停止状态 RAID 阵列的配置条目。
|
只删除 mdadm.conf 中停止状态 RAID 阵列的配置条目。
|
||||||
:param uuid: 要删除的 RAID 阵列的 UUID。
|
:param uuid: 要删除的 RAID 阵列的 UUID。
|
||||||
@@ -494,7 +444,7 @@ class RaidOperations:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def activate_raid_array(self, array_path, array_uuid): # 添加 array_uuid 参数
|
def activate_raid_array(self, array_path, array_uuid):
|
||||||
"""
|
"""
|
||||||
激活一个已停止的 RAID 阵列。
|
激活一个已停止的 RAID 阵列。
|
||||||
使用 mdadm --assemble <device> --uuid=<uuid>
|
使用 mdadm --assemble <device> --uuid=<uuid>
|
||||||
@@ -506,12 +456,9 @@ class RaidOperations:
|
|||||||
|
|
||||||
logger.info(f"尝试激活 RAID 阵列: {array_path} (UUID: {array_uuid})")
|
logger.info(f"尝试激活 RAID 阵列: {array_path} (UUID: {array_uuid})")
|
||||||
|
|
||||||
# 使用 mdadm --assemble <device> --uuid=<uuid>
|
|
||||||
# 这样可以确保只激活用户选择的特定阵列
|
|
||||||
success, stdout, stderr = self._execute_shell_command(
|
success, stdout, stderr = self._execute_shell_command(
|
||||||
["mdadm", "--assemble", array_path, "--uuid", array_uuid],
|
["mdadm", "--assemble", array_path, "--uuid", array_uuid],
|
||||||
f"激活 RAID 阵列 {array_path} 失败",
|
f"激活 RAID 阵列 {array_path} 失败",
|
||||||
# 抑制一些常见的非致命错误,例如阵列已经激活
|
|
||||||
suppress_critical_dialog_on_stderr_match=("mdadm: device /dev/md", "already active", "No such file or directory")
|
suppress_critical_dialog_on_stderr_match=("mdadm: device /dev/md", "already active", "No such file or directory")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -522,4 +469,3 @@ class RaidOperations:
|
|||||||
else:
|
else:
|
||||||
logger.error(f"激活 RAID 阵列 {array_path} 失败。")
|
logger.error(f"激活 RAID 阵列 {array_path} 失败。")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user