change tk

This commit is contained in:
zj
2026-02-09 03:14:35 +08:00
parent 8c53b9c0a0
commit 1a3a4746a3
13 changed files with 3595 additions and 221 deletions

326
AGENTS.md
View File

@@ -1,275 +1,161 @@
# Linux 存储管理器 (Linux Storage Manager) - AI Agent Guide # Linux 存储管理器 (Linux Storage Manager)
## 项目概述 ## 项目概述
这是一个基于 PySide6 开发的 Linux 存储管理图形界面工具,提供磁盘管理、逻辑卷管理(LVM)和软阵列管理(RAID)三大核心功能 这是一个基于 PySide6 开发的 Linux 图形化存储管理工具,提供磁盘管理、逻辑卷管理(LVM)和软 RAID 管理功能。应用程序通过调用系统命令(如 `lsblk`, `parted`, `mdadm`, `lvm` 等)与 Linux 存储子系统交互
**主要功能模块** ### 主要功能模块
1. **磁盘管理** - 分区创建/删除、格式化(ext4/xfs/ntfs/fat32)、挂载/卸载、分区表擦除
2. **LVM 管理** - 物理卷(PV)、卷组(VG)、逻辑卷(LV)的创建和管理 1. **块设备管理** - 查看磁盘信息、创建分区、格式化文件系统(ext4/xfs/ntfs/fat32)、挂载/卸载设备
3. **RAID 管理** - 软阵列创建、管理、停止/启动阵列 (支持 RAID0/1/5/6/10) 2. **LVM 管理** - 物理卷(PV)、卷组(VG)、逻辑卷(LV)的创建、删除和扩容
3. **RAID 管理** - 软 RAID 阵列的创建、删除和监控(支持 RAID0/1/5/6/10
## 技术栈 ## 技术栈
| 组件 | 说明 | - **GUI 框架**: PySide6 (Qt for Python)
|------|------| - **UI 设计**: Qt Designer (form.ui)
| Python 3.14+ | 主要编程语言 | - **构建工具**: PyInstaller
| PySide6 | Qt6 的 Python 绑定,用于 GUI | - **依赖库**:
| Qt Designer (.ui 文件) | UI 设计文件 | - PySide6 - GUI 框架
| PyInstaller | 打包工具,生成独立可执行文件 | - pexpect - 交互式命令执行(用于 RAID 创建时的密码输入)
| 系统工具 | parted, mkfs.*, mount, umount, lvm2, mdadm, lsblk, findmnt |
## 项目结构 ## 项目结构
``` ```
. .
├── mainwindow.py # 主窗口UI 与业务逻辑控制器 ├── mainwindow.py # 应用程序入口,主窗口逻辑,连接各模块
├── ui_form.py # 自动生成的 UI 代码 (从 form.ui 生成) ├── ui_form.py # 由 form.ui 自动生成的 UI 类
├── form.ui # Qt Designer UI 定义文件 ├── form.ui # Qt Designer UI 定义文件(主界面布局)
├── system_info.py # 系统信息获取模块 (块设备、RAID、挂载点) ├── dialogs.py # 自定义对话框分区创建、挂载、RAID/LVM 创建等)
├── disk_operations.py # 磁盘操作 (分区、格式化、挂载) ├── system_info.py # 系统信息获取模块lsblk 解析等)
├── lvm_operations.py # LVM 操作 (PV/VG/LV 管理) ├── disk_operations.py # 磁盘操作(分区、格式化、挂载管理
├── raid_operations.py # RAID 操作 (mdadm 管理) ├── lvm_operations.py # LVM 操作PV/VG/LV 管理
├── dialogs.py # 自定义对话框 (创建分区、RAID、PV/VG/LV 等) ├── raid_operations.py # RAID 操作mdadm 命令封装)
├── occupation_resolver.py # 设备占用解除模块 (处理挂载、进程占用) ├── occupation_resolver.py # 设备占用检测与解除(嵌套挂载、进程占用
├── logger_config.py # 日志配置 (输出到 QTextEdit 和控制台) ├── logger_config.py # 日志配置输出到 QTextEdit 控件)
├── pyproject.toml # PySide6 项目配置 ├── pyproject.toml # PySide6 项目配置文件
├── disk-manager.spec # PyInstaller 打包配置 (英文版) ├── disk-manager.spec # PyInstaller 打包配置
── Linux存儲管理器.spec # PyInstaller 打包配置 (中文版) ── build-app.sh # 构建脚本
└── build-app.sh # 打包脚本
``` ```
## 代码组织 ## 模块依赖关系
### 模块依赖关系
``` ```
mainwindow.py (入口) mainwindow.py
├── ui_form.py (自动生成的 UI) ├── ui_form.py (UI 布局)
├── system_info.py (系统信息) ├── system_info.py (系统信息)
├── logger_config.py (日志)
├── disk_operations.py (磁盘操作) ├── disk_operations.py (磁盘操作)
│ └── 依赖: system_info.py, lvm_operations.py └── 依赖 system_info.py, lvm_operations.py
├── lvm_operations.py (LVM 操作,提供 _execute_shell_command)
├── raid_operations.py (RAID 操作) ├── raid_operations.py (RAID 操作)
│ └── 依赖: system_info.py └── 依赖 system_info.py
├── occupation_resolver.py (占用解除) ├── lvm_operations.py (LVM 操作)
│ └── 依赖: lvm_operations.py, system_info.py ├── occupation_resolver.py (占用解决)
├── dialogs.py (对话框) │ └── 依赖 lvm_operations.py, system_info.py
└── logger_config.py (日志) └── dialogs.py (对话框)
└── 依赖 logger_config.py
``` ```
### 核心类说明 ## 代码组织规范
| 类名 | 文件 | 职责 | ### 文件命名
|------|------|------| - 模块文件使用下划线命名法:`disk_operations.py`, `lvm_operations.py`
| `MainWindow` | mainwindow.py | 主窗口,整合所有功能,处理用户交互 | - UI 相关文件:`form.ui`(设计源文件), `ui_form.py`(自动生成)
| `SystemInfoManager` | system_info.py | 获取系统块设备信息、RAID 信息、挂载点 |
| `DiskOperations` | disk_operations.py | 磁盘分区、格式化、挂载操作 |
| `LvmOperations` | lvm_operations.py | LVM 相关操作,提供统一的 shell 命令执行接口 |
| `RaidOperations` | raid_operations.py | RAID 阵列管理 |
| `OccupationResolver` | occupation_resolver.py | 解除设备占用 (嵌套挂载、进程占用) |
| `FormatWorker` | disk_operations.py | 后台格式化工作线程 |
| `QTextEditLogger` | logger_config.py | 自定义日志处理器,输出到 UI |
### 对话框类 (dialogs.py) ### 类命名规范
- 主窗口类:`MainWindow`
- UI 类:`Ui_MainWindow`Qt 自动生成)
- 操作类:`DiskOperations`, `LvmOperations`, `RaidOperations`
- 对话框类:`CreatePartitionDialog`, `MountDialog`, `CreateRaidDialog`
- 工具类:`SystemInfoManager`, `OccupationResolver`
- `CreatePartitionDialog` - 创建分区对话框 ### 命令执行模式
- `MountDialog` - 挂载分区对话框 所有系统命令执行都通过 `_execute_shell_command``_run_command` 方法封装,统一处理:
- `CreateRaidDialog` - 创建 RAID 阵列对话框 - sudo 权限提升
- `CreatePvDialog` - 创建物理卷对话框 - 错误处理和日志记录
- `CreateVgDialog` - 创建卷组对话框 - QMessageBox 错误提示(可抑制)
- `CreateLvDialog` - 创建逻辑卷对话框
## 构建和打包 ## 构建和运行
### 依赖安装 ### 运行环境要求
- Linux 操作系统(需要 root/sudo 权限执行存储管理命令)
- Python 3.x
- 系统依赖:`parted`, `mdadm`, `lvm2`, `dosfstools`, `e2fsprogs`, `xfsprogs`, `ntfs-3g`
### 安装依赖
```bash ```bash
pip install PySide6 pyinstaller pexpect pip install PySide6 pexpect
``` ```
### 开发运行 ### 运行应用
```bash ```bash
# 直接运行
python mainwindow.py python mainwindow.py
# 注意: 部分功能需要 root 权限
sudo python mainwindow.py
``` ```
注意:大部分功能需要 root 权限,建议以 sudo 运行或确保用户有 sudo 权限。
### 生成可执行文件 ### 打包可执行文件
```bash ```bash
# 使用 PyInstaller (需要配置虚拟环境路径) # 使用 PyInstaller
sudo /path/to/venv/bin/pyinstaller -F --name "disk-manager" mainwindow.py sudo pyinstaller -F --name "disk-manager" mainwindow.py
# 或使用提供的打包脚本 (需修改路径) # 或使用提供的脚本
bash build-app.sh sudo ./build-app.sh
``` ```
打包配置文件: 打包后的可执行文件位于 `dist/disk-manager`
- `disk-manager.spec` - 英文名称配置
- `Linux存儲管理器.spec` - 中文名称配置
## UI 设计规范 ## 开发注意事项
### 界面布局 (form.ui)
主窗口包含:
1. **刷新按钮** - 刷新所有设备信息
2. **标签页控件** - 三个标签页:
- 块设备概览 (treeWidget_block_devices)
- RAID 管理 (treeWidget_raid)
- LVM 管理 (treeWidget_lvm)
3. **日志输出区** (logOutputTextEdit) - 显示操作日志
### 修改 UI 的流程
### UI 修改流程
1. 使用 Qt Designer 编辑 `form.ui` 1. 使用 Qt Designer 编辑 `form.ui`
2. 使用 `pyside6-uic` 重新生成 `ui_form.py`: 2. 使用 `pyside6-uic form.ui -o ui_form.py` 重新生成 Python UI 文件
```bash 3. 或在 Qt Creator 中配置 PySide6 项目自动处理
pyside6-uic form.ui -o ui_form.py
```
3. 注意:`ui_form.py` 是自动生成的,不要手动修改
## 编码规范 ### 日志系统
- 使用 `logger_config.py` 中配置的全局 logger
- 日志同时输出到控制台和 GUI 的 QTextEdit 控件
- 日志级别:`logging.DEBUG`(开发时可调整)
### 代码风格 ### 线程安全
- 格式化操作使用 `FormatWorker` 在后台线程执行,避免阻塞 UI
- 使用 4 空格缩进 - 通过 Qt Signal/Slot 机制与主线程通信
- 函数和变量使用小写加下划线命名 (snake_case)
- 类名使用大驼峰命名 (PascalCase)
- 注释和日志使用中文
### 错误处理 ### 错误处理
- 所有 shell 命令执行都有统一的错误处理
- 关键错误会弹出 QMessageBox 提示用户
- 详细错误信息写入日志
所有 shell 命令执行通过 `_execute_shell_command` 方法统一处理: ## 系统命令依赖
- 自动处理 sudo 权限
- 统一的错误对话框显示
- 详细的日志记录
示例: 应用依赖以下 Linux 命令行工具:
```python
success, stdout, stderr = self.lvm_ops._execute_shell_command(
["parted", "-s", device_path, "mklabel", "gpt"],
f"擦除 {device_path} 上的分区表失败"
)
```
### 日志记录 | 功能 | 命令 |
使用标准 logging 模块,配置在 `logger_config.py`:
- DEBUG: 详细的调试信息
- INFO: 常规操作信息
- WARNING: 警告信息
- ERROR: 错误信息
日志同时输出到:
1. 控制台 (标准输出)
2. UI 的 QTextEdit 控件 (通过 QTextEditLogger)
## 关键实现细节
### 1. 权限处理
所有需要 root 权限的操作都通过 `sudo` 执行。项目中没有直接处理密码输入,依赖系统的 sudo 配置。
### 2. 异步操作
格式化操作使用 `QThread` + `FormatWorker` 在后台执行,避免阻塞 UI:
```python
# 信号定义
formatting_finished = Signal(bool, str, str, str) # success, device, stdout, stderr
formatting_started = Signal(str)
```
### 3. 设备识别
- 使用 `lsblk -J` 获取块设备信息 (JSON 格式)
- 支持通过设备路径或 maj:min 号识别设备
- 正确处理符号链接 (如 LVM 设备、RAID 别名)
### 4. 占用解除
`OccupationResolver` 类处理复杂的设备占用情况:
- 嵌套挂载点卸载
- 用户进程占用检测和终止
- 交换分区关闭
## 测试策略
当前项目没有自动化测试套件。测试主要依赖:
1. **手动测试** - 在真实 Linux 环境中测试各项功能
2. **日志验证** - 通过日志输出验证操作执行
**测试环境要求:**
- Linux 系统 (建议 Ubuntu/Debian 或 Arch Linux)
- 安装必要工具: `parted`, `lvm2`, `mdadm`, `dosfstools`, `ntfs-3g`
- 测试磁盘建议使用虚拟机或空闲磁盘,避免数据丢失
## 安全注意事项
⚠️ **危险操作警告:**
1. **格式化操作** - 会永久删除数据
2. **分区删除** - 会丢失分区数据
3. **分区表擦除** - 会清除整个磁盘分区信息
4. **RAID 操作** - 错误的操作可能导致阵列损坏
**安全准则:**
- 所有危险操作都有确认对话框
- 操作前会记录警告日志
- 建议只在测试环境或已备份的系统上使用
- 部分操作不可逆,请谨慎操作
## 外部依赖工具
应用程序依赖以下系统工具:
| 工具 | 用途 |
|------|------| |------|------|
| lsblk | 获取块设备信息 | | 块设备信息 | `lsblk` |
| parted | 分区管理 | | 分区操作 | `parted`, `partprobe` |
| mkfs.ext4/mkfs.xfs/mkfs.vfat/mkfs.ntfs | 文件系统格式化 | | 文件系统 | `mkfs.ext4`, `mkfs.xfs`, `mkfs.ntfs`, `mkfs.vfat` |
| mount/umount | 挂载/卸载 | | 挂载管理 | `mount`, `umount`, `findmnt` |
| lvm (pvcreate/vgcreate/lvcreate 等) | LVM 管理 | | RAID 管理 | `mdadm` |
| mdadm | RAID 管理 | | LVM 管理 | `pvcreate`, `vgcreate`, `lvcreate`, `pvs`, `vgs`, `lvs` |
| findmnt | 挂载点查询 | | 进程查询 | `lsof`, `fuser` |
| fuser | 进程占用查询 |
| stat | 设备信息获取 |
| pexpect | 交互式命令执行 (RAID 创建) |
## 配置说明 ## 安全考虑
### pyproject.toml 1. **权限要求**: 应用需要 root/sudo 权限执行存储管理操作
2. **危险操作确认**: 格式化、删除分区/卷等操作都有确认对话框
3. **设备占用检测**: `OccupationResolver` 模块检测并解除设备占用(卸载挂载点、停止进程)后才执行操作
4. **fstab 备份**: 修改 `/etc/fstab` 前建议用户手动备份
PySide6 项目配置文件,定义项目文件列表: ## 已知限制
```toml
[tool.pyside6-project]
files = ["dialogs.py", "disk_operations.py", ...]
```
### .gitignore - 仅支持 Linux 系统
- 需要图形桌面环境
- 部分操作(如 RAID 创建)可能需要密码输入
- 不支持 LVM/RAID 的降级恢复等高级场景
排除目录: ## 调试建议
- /dist/ - PyInstaller 输出
- /build/ - 构建缓存
- /.qtcreator/ - Qt Creator 配置
- /__pycache__/ - Python 缓存
## 开发工作流 1. 查看日志输出GUI 底部 QTextEdit 或控制台)
2. 检查系统命令是否安装:`which parted mdadm lvm`
1. **修改 UI**: 编辑 `form.ui` → 运行 `pyside6-uic` 生成 `ui_form.py` 3. 手动测试命令:`sudo lsblk -J`
2. **添加功能**: 在相应模块添加逻辑,在 `MainWindow` 添加槽函数 4. 检查权限:确保用户有 sudo 权限或已切换到 root
3. **测试**: 在 Linux 环境中运行 `python mainwindow.py`
4. **打包**: 运行 `build-app.sh` 或直接使用 PyInstaller
5. **提交**: 使用 `up+.sh` 脚本辅助 git 提交 (需配置)
## 常见问题
1. **UI 更新不生效** - 检查是否重新生成了 `ui_form.py`
2. **权限错误** - 确保以 sudo 运行或配置 sudo 免密
3. **命令未找到** - 安装对应的系统工具包
4. **RAID 创建卡住** - pexpect 交互处理可能需要调整

169
README_Tkinter.md Normal file
View File

@@ -0,0 +1,169 @@
# Linux 存储管理工具 - Tkinter 版本
这是一个使用 Tkinter 重写的 Linux 存储管理工具,适配低版本系统(如 CentOS 8
## 特性
- **无需 PySide6**: 使用 Python 内置的 Tkinter 模块,无需安装额外的 GUI 库
- **兼容旧系统**: 支持 Python 3.6+,兼容 CentOS 8/RHEL 8 等系统
- **完整功能**: 保留原版本的所有功能,包括:
- 块设备管理(查看、创建分区、格式化、挂载/卸载)
- RAID 管理(创建、停止、删除阵列)
- LVM 管理PV/VG/LV 的创建和删除)
- 设备占用解除
## 系统要求
- Python 3.6 或更高版本
- tkinter 模块(通常随 Python 一起安装)
- 以下系统工具(根据功能需求):
- `lsblk` - 块设备信息
- `parted` - 分区管理
- `mdadm` - RAID 管理
- `lvm2` - LVM 管理
## 安装
### CentOS 8 / RHEL 8
```bash
# 安装 Python 3 和 tkinter
sudo yum install -y python3 python3-tkinter
# 安装必要的存储管理工具
sudo yum install -y parted mdadm lvm2 util-linux
# 可选:安装 pexpect用于交互式命令推荐安装
sudo pip3 install pexpect
```
### Ubuntu 20.04 / Debian 10
```bash
# 安装 Python 3 和 tkinter
sudo apt-get update
sudo apt-get install -y python3 python3-tk
# 安装必要的存储管理工具
sudo apt-get install -y parted mdadm lvm2 util-linux
# 可选:安装 pexpect
sudo pip3 install pexpect
```
## 使用方法
### 方式一:使用启动脚本(推荐)
```bash
chmod +x run_tkinter.sh
./run_tkinter.sh
```
### 方式二:直接运行
```bash
sudo python3 main_tkinter.py
```
### 方式三:创建桌面快捷方式
```bash
# 创建桌面文件
cat > ~/.local/share/applications/linux-storage-manager.desktop << 'EOF'
[Desktop Entry]
Name=Linux 存储管理器
Comment=Linux 存储管理工具
Exec=python3 /path/to/diskmanager/main_tkinter.py
Type=Application
Terminal=false
Icon=drive-harddisk
Categories=System;Filesystem;
EOF
```
## 文件说明
| 文件 | 说明 |
|------|------|
| `main_tkinter.py` | 程序入口 |
| `mainwindow_tkinter.py` | 主窗口实现 |
| `dialogs_tkinter.py` | 对话框组件 |
| `logger_config_tkinter.py` | 日志配置Tkinter 版本)|
| `disk_operations_tkinter.py` | 磁盘操作模块Tkinter 版本)|
| `raid_operations_tkinter.py` | RAID 操作模块Tkinter 版本)|
| `lvm_operations_tkinter.py` | LVM 操作模块Tkinter 版本)|
| `occupation_resolver_tkinter.py` | 设备占用解除模块Tkinter 版本)|
| `system_info.py` | 系统信息获取模块(无需修改)|
| `run_tkinter.sh` | 启动脚本 |
| `verify_tkinter.py` | 环境验证脚本 |
## 与原版本的区别
1. **GUI 框架**: PySide6 → Tkinter
2. **线程机制**: QThread → threading.Thread
3. **信号/槽**: Qt Signal/Slot → Python 回调函数 + queue
4. **对话框**: QDialog → tkinter.Toplevel
5. **消息框**: QMessageBox → tkinter.messagebox
6. **输入框**: QInputDialog → tkinter.simpledialog
## 注意事项
1. **权限**: 程序需要 root 权限才能执行存储管理操作
2. **数据安全**: 删除、格式化等操作会永久删除数据,请谨慎操作
3. **备份**: 执行重要操作前请备份重要数据
4. **兼容性**: 虽然适配了旧系统,但建议使用前先在测试环境验证
## 故障排除
### 问题:无法启动,提示缺少 tkinter
**解决**:
```bash
# CentOS/RHEL
sudo yum install python3-tkinter
# Ubuntu/Debian
sudo apt-get install python3-tk
```
### 问题:部分功能无法使用
**解决**: 确保安装了相应的系统工具:
```bash
# CentOS/RHEL
sudo yum install parted mdadm lvm2
# Ubuntu/Debian
sudo apt-get install parted mdadm lvm2
```
### 问题:界面显示异常
**解决**: 尝试设置环境变量:
```bash
export DISPLAY=:0
python3 main_tkinter.py
```
### 问题:提示缺少 pexpect
**解决**: pexpect 是可选依赖,用于处理交互式命令(如创建 RAID 时的确认提示)。
```bash
sudo pip3 install pexpect
```
如果没有安装 pexpectRAID 创建功能仍能工作,但可能无法处理交互式提示。
## 开发信息
- **目标 Python 版本**: 3.6+
- **GUI 库**: Tkinter (内置)
- **线程库**: threading (内置)
- **依赖**:
- 必需: 无(纯 Python 标准库 + tkinter
- 可选: pexpect用于交互式命令
## 许可证
与原项目相同。

557
dialogs_tkinter.py Normal file
View File

@@ -0,0 +1,557 @@
# dialogs_tkinter.py
import os
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
import logging
logger = logging.getLogger(__name__)
class BaseDialog:
"""对话框基类"""
def __init__(self, parent, title, width=400, height=300):
self.parent = parent
self.result = None
# 创建顶层窗口
self.dialog = tk.Toplevel(parent)
self.dialog.title(title)
self.dialog.geometry(f"{width}x{height}")
self.dialog.transient(parent) # 设置父窗口
self.dialog.grab_set() # 模态对话框
# 居中显示
self._center_window()
# 创建主框架
self.main_frame = ttk.Frame(self.dialog, padding="10")
self.main_frame.pack(fill=tk.BOTH, expand=True)
def _center_window(self):
"""将窗口居中显示"""
self.dialog.update_idletasks()
width = self.dialog.winfo_width()
height = self.dialog.winfo_height()
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
y = (self.dialog.winfo_screenheight() // 2) - (height // 2)
self.dialog.geometry(f"{width}x{height}+{x}+{y}")
def _create_button_box(self):
"""创建确定/取消按钮"""
btn_frame = ttk.Frame(self.main_frame)
btn_frame.pack(fill=tk.X, pady=(10, 0))
self.confirm_btn = ttk.Button(btn_frame, text="确定", command=self._on_confirm)
self.confirm_btn.pack(side=tk.RIGHT, padx=5)
self.cancel_btn = ttk.Button(btn_frame, text="取消", command=self._on_cancel)
self.cancel_btn.pack(side=tk.RIGHT, padx=5)
# 绑定回车键和 ESC 键
self.dialog.bind('<Return>', lambda e: self._on_confirm())
self.dialog.bind('<Escape>', lambda e: self._on_cancel())
def _on_confirm(self):
"""确定按钮回调 - 子类重写"""
pass
def _on_cancel(self):
"""取消按钮回调"""
self.dialog.destroy()
def wait_for_result(self):
"""等待对话框关闭并返回结果"""
self.dialog.wait_window()
return self.result
class CreatePartitionDialog(BaseDialog):
"""创建分区对话框"""
def __init__(self, parent, disk_path="", total_disk_mib=0.0, max_available_mib=0.0):
self.disk_path = disk_path
self.total_disk_mib = total_disk_mib
self.max_available_mib = max_available_mib
super().__init__(parent, "创建分区", 450, 280)
logger.debug(f"CreatePartitionDialog initialized for {disk_path}. Total MiB: {total_disk_mib}, Max Available MiB: {max_available_mib}")
self._setup_ui()
self._initialize_state()
def _setup_ui(self):
"""设置 UI"""
# 创建内容框架
content_frame = ttk.Frame(self.main_frame)
content_frame.pack(fill=tk.BOTH, expand=True)
# 磁盘路径
ttk.Label(content_frame, text="磁盘路径:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
ttk.Label(content_frame, text=self.disk_path).grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
# 分区表类型
ttk.Label(content_frame, text="分区表类型:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
self.part_type_var = tk.StringVar(value="gpt")
self.part_type_combo = ttk.Combobox(content_frame, textvariable=self.part_type_var,
values=["gpt", "msdos"], state="readonly", width=15)
self.part_type_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
# 使用最大空间复选框
self.use_max_var = tk.BooleanVar(value=True)
self.use_max_check = ttk.Checkbutton(content_frame, text="使用最大可用空间",
variable=self.use_max_var,
command=self._toggle_size_input)
self.use_max_check.grid(row=2, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5)
# 分区大小
ttk.Label(content_frame, text="分区大小 (GB):").grid(row=3, column=0, sticky=tk.W, padx=5, pady=5)
calculated_max_gb = self.max_available_mib / 1024.0
self.max_gb = max(0.01, calculated_max_gb)
self.size_var = tk.DoubleVar(value=self.max_gb)
self.size_spin = tk.Spinbox(content_frame, from_=0.01, to=self.max_gb,
increment=0.01, textvariable=self.size_var, width=12)
self.size_spin.grid(row=3, column=1, sticky=tk.W, padx=5, pady=5)
logger.debug(f"Size spinbox max set to: {self.max_gb} GB (calculated from {calculated_max_gb} GB)")
# 按钮
self._create_button_box()
def _initialize_state(self):
"""初始化状态"""
self._toggle_size_input()
def _toggle_size_input(self):
"""切换大小输入框状态"""
is_checked = self.use_max_var.get()
logger.debug(f"[_toggle_size_input] Called. Checkbox isChecked(): {is_checked}")
if is_checked:
logger.debug(f"[_toggle_size_input] Checkbox is CHECKED. Disabling spinbox and setting value to max.")
self.size_spin.configure(state=tk.DISABLED)
self.size_var.set(self.max_gb)
logger.debug(f"[_toggle_size_input] Spinbox value set to max: {self.max_gb} GB.")
else:
logger.debug(f"[_toggle_size_input] Checkbox is UNCHECKED. Enabling spinbox.")
self.size_spin.configure(state=tk.NORMAL)
current_val = self.size_var.get()
if current_val >= self.max_gb:
self.size_var.set(0.01)
logger.debug(f"[_toggle_size_input] Spinbox was at max, reset to min: 0.01 GB.")
else:
logger.debug(f"[_toggle_size_input] Spinbox was not at max, keeping current value: {current_val} GB.")
def _on_confirm(self):
"""确定按钮回调"""
size_gb = self.size_var.get()
use_max_space = self.use_max_var.get()
if not use_max_space and size_gb <= 0:
messagebox.showwarning("输入错误", "分区大小必须大于0。")
return
if not use_max_space and size_gb > self.max_gb:
messagebox.showwarning("输入错误", "分区大小不能超过最大可用空间。")
return
self.result = {
'disk_path': self.disk_path,
'partition_table_type': self.part_type_var.get(),
'size_gb': size_gb,
'total_disk_mib': self.total_disk_mib,
'use_max_space': use_max_space
}
self.dialog.destroy()
def get_partition_info(self):
"""获取分区信息"""
return self.result
class MountDialog(BaseDialog):
"""挂载对话框"""
def __init__(self, parent, device_path=""):
self.device_path = device_path
super().__init__(parent, "挂载分区", 400, 200)
self._setup_ui()
def _setup_ui(self):
"""设置 UI"""
# 创建内容框架
content_frame = ttk.Frame(self.main_frame)
content_frame.pack(fill=tk.BOTH, expand=True)
# 设备路径
ttk.Label(content_frame, text="设备路径:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
ttk.Label(content_frame, text=self.device_path).grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
# 挂载点
ttk.Label(content_frame, text="挂载点:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
self.mount_point_var = tk.StringVar(value=f"/mnt/{os.path.basename(self.device_path)}")
self.mount_point_entry = ttk.Entry(content_frame, textvariable=self.mount_point_var, width=30)
self.mount_point_entry.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
# 开机自动挂载复选框
self.add_to_fstab_var = tk.BooleanVar(value=False)
self.fstab_check = ttk.Checkbutton(content_frame, text="开机自动挂载 (添加到 /etc/fstab)",
variable=self.add_to_fstab_var)
self.fstab_check.grid(row=2, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5)
# 按钮
self._create_button_box()
def _on_confirm(self):
"""确定按钮回调"""
mount_point = self.mount_point_var.get().strip()
if not mount_point:
messagebox.showwarning("输入错误", "挂载点不能为空。")
return
if not mount_point.startswith('/'):
messagebox.showwarning("输入错误", "挂载点必须是绝对路径 (以 '/' 开头)。")
return
self.result = {
'mount_point': mount_point,
'add_to_fstab': self.add_to_fstab_var.get()
}
self.dialog.destroy()
def get_mount_info(self):
"""获取挂载信息"""
return self.result
class CreateRaidDialog(BaseDialog):
"""创建 RAID 阵列对话框"""
def __init__(self, parent, available_devices=None):
self.available_devices = available_devices if available_devices is not None else []
self.selected_devices = []
super().__init__(parent, "创建 RAID 阵列", 450, 450)
self._setup_ui()
def _setup_ui(self):
"""设置 UI"""
# 创建内容框架
content_frame = ttk.Frame(self.main_frame)
content_frame.pack(fill=tk.BOTH, expand=True)
# RAID 级别
ttk.Label(content_frame, text="RAID 级别:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
self.raid_level_var = tk.StringVar(value="raid0")
self.raid_level_combo = ttk.Combobox(content_frame, textvariable=self.raid_level_var,
values=["raid0", "raid1", "raid5", "raid6", "raid10"],
state="readonly", width=12)
self.raid_level_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
# Chunk 大小
ttk.Label(content_frame, text="Chunk 大小 (KB):").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
self.chunk_size_var = tk.IntVar(value=512)
self.chunk_size_spin = tk.Spinbox(content_frame, from_=4, to=1024, increment=4,
textvariable=self.chunk_size_var, width=10)
self.chunk_size_spin.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
# 设备选择
ttk.Label(content_frame, text="选择设备:").grid(row=2, column=0, sticky=tk.NW, padx=5, pady=5)
# 创建设备选择框架
device_frame = ttk.Frame(content_frame)
device_frame.grid(row=2, column=1, sticky=tk.W, padx=5, pady=5)
# 设备复选框列表
self.device_vars = {}
for i, dev in enumerate(self.available_devices):
var = tk.BooleanVar(value=False)
self.device_vars[dev] = var
cb = ttk.Checkbutton(device_frame, text=dev, variable=var)
cb.pack(anchor=tk.W)
# 按钮
self._create_button_box()
def _on_confirm(self):
"""确定按钮回调"""
# 收集选中的设备
self.selected_devices = [dev for dev, var in self.device_vars.items() if var.get()]
if not self.selected_devices:
messagebox.showwarning("输入错误", "请选择至少一个设备来创建 RAID 阵列。")
return
raid_level = self.raid_level_var.get()
num_devices = len(self.selected_devices)
# RAID 级别检查
if raid_level == "raid0" and num_devices < 1:
messagebox.showwarning("输入错误", "RAID0 至少需要一个设备。")
return
elif raid_level == "raid1" and num_devices < 2:
messagebox.showwarning("输入错误", "RAID1 至少需要两个设备。")
return
elif raid_level == "raid5" and num_devices < 3:
messagebox.showwarning("输入错误", "RAID5 至少需要三个设备。")
return
elif raid_level == "raid6" and num_devices < 4:
messagebox.showwarning("输入错误", "RAID6 至少需要四个设备。")
return
elif raid_level == "raid10" and num_devices < 2:
messagebox.showwarning("输入错误", "RAID10 至少需要两个设备。")
return
self.result = {
'devices': self.selected_devices,
'level': raid_level.replace('raid', ''),
'chunk_size': self.chunk_size_var.get()
}
self.dialog.destroy()
def get_raid_info(self):
"""获取 RAID 信息"""
return self.result
class CreatePvDialog(BaseDialog):
"""创建物理卷 (PV) 对话框"""
def __init__(self, parent, available_partitions=None):
self.available_partitions = available_partitions if available_partitions is not None else []
super().__init__(parent, "创建物理卷 (PV)", 400, 200)
self._setup_ui()
def _setup_ui(self):
"""设置 UI"""
# 创建内容框架
content_frame = ttk.Frame(self.main_frame)
content_frame.pack(fill=tk.BOTH, expand=True)
# 设备选择
ttk.Label(content_frame, text="选择设备:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
self.device_var = tk.StringVar()
if self.available_partitions:
self.device_var.set(self.available_partitions[0])
self.device_combo = ttk.Combobox(content_frame, textvariable=self.device_var,
values=self.available_partitions, state="readonly", width=30)
self.device_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
# 按钮
self._create_button_box()
def _on_confirm(self):
"""确定按钮回调"""
device_path = self.device_var.get()
if not device_path:
messagebox.showwarning("输入错误", "请选择一个设备来创建物理卷。")
return
self.result = {'device_path': device_path}
self.dialog.destroy()
def get_pv_info(self):
"""获取 PV 信息"""
return self.result
class CreateVgDialog(BaseDialog):
"""创建卷组 (VG) 对话框"""
def __init__(self, parent, available_pvs=None):
self.available_pvs = available_pvs if available_pvs is not None else []
self.selected_pvs = []
super().__init__(parent, "创建卷组 (VG)", 450, 350)
self._setup_ui()
def _setup_ui(self):
"""设置 UI"""
# 创建内容框架
content_frame = ttk.Frame(self.main_frame)
content_frame.pack(fill=tk.BOTH, expand=True)
# 卷组名称
ttk.Label(content_frame, text="卷组名称:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
self.vg_name_var = tk.StringVar()
self.vg_name_entry = ttk.Entry(content_frame, textvariable=self.vg_name_var, width=25)
self.vg_name_entry.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
# 物理卷选择
ttk.Label(content_frame, text="选择物理卷:").grid(row=1, column=0, sticky=tk.NW, padx=5, pady=5)
pv_frame = ttk.Frame(content_frame)
pv_frame.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
self.pv_vars = {}
for pv in self.available_pvs:
var = tk.BooleanVar(value=False)
self.pv_vars[pv] = var
cb = ttk.Checkbutton(pv_frame, text=pv, variable=var)
cb.pack(anchor=tk.W)
# 按钮
self._create_button_box()
def _on_confirm(self):
"""确定按钮回调"""
vg_name = self.vg_name_var.get().strip()
self.selected_pvs = [pv for pv, var in self.pv_vars.items() if var.get()]
if not vg_name:
messagebox.showwarning("输入错误", "卷组名称不能为空。")
return
if not self.selected_pvs:
messagebox.showwarning("输入错误", "请选择至少一个物理卷来创建卷组。")
return
self.result = {
'vg_name': vg_name,
'pvs': self.selected_pvs
}
self.dialog.destroy()
def get_vg_info(self):
"""获取 VG 信息"""
return self.result
class CreateLvDialog(BaseDialog):
"""创建逻辑卷 (LV) 对话框"""
def __init__(self, parent, available_vgs=None, vg_sizes=None):
self.available_vgs = available_vgs if available_vgs is not None else []
self.vg_sizes = vg_sizes if vg_sizes is not None else {}
super().__init__(parent, "创建逻辑卷", 450, 300)
logger.debug(f"CreateLvDialog initialized. Available VGs: {self.available_vgs}, VG Sizes: {self.vg_sizes}")
self._setup_ui()
self._initialize_state()
def _setup_ui(self):
"""设置 UI"""
# 逻辑卷名称
# 创建内容框架
content_frame = ttk.Frame(self.main_frame)
content_frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(content_frame, text="逻辑卷名称:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
self.lv_name_var = tk.StringVar()
self.lv_name_entry = ttk.Entry(content_frame, textvariable=self.lv_name_var, width=25)
self.lv_name_entry.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
# 卷组选择
ttk.Label(content_frame, text="选择卷组:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
self.vg_var = tk.StringVar()
if self.available_vgs:
self.vg_var.set(self.available_vgs[0])
self.vg_combo = ttk.Combobox(content_frame, textvariable=self.vg_var,
values=self.available_vgs, state="readonly", width=25)
self.vg_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
self.vg_combo.bind('<<ComboboxSelected>>', lambda e: self._update_size_options())
# 使用最大空间复选框
self.use_max_var = tk.BooleanVar(value=True)
self.use_max_check = ttk.Checkbutton(content_frame, text="使用最大可用空间",
variable=self.use_max_var,
command=self._toggle_size_input)
self.use_max_check.grid(row=2, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5)
# 逻辑卷大小
ttk.Label(content_frame, text="大小 (GB):").grid(row=3, column=0, sticky=tk.W, padx=5, pady=5)
self.size_var = tk.DoubleVar(value=1.0)
self.size_spin = tk.Spinbox(content_frame, from_=0.01, to=100000.0, increment=0.01,
textvariable=self.size_var, width=12)
self.size_spin.grid(row=3, column=1, sticky=tk.W, padx=5, pady=5)
# 按钮
self._create_button_box()
def _initialize_state(self):
"""初始化状态"""
self._update_size_options()
def _update_size_options(self):
"""根据选中的卷组更新大小选项"""
selected_vg = self.vg_var.get()
max_size_gb = self.vg_sizes.get(selected_vg, 0.0)
logger.debug(f"[_update_size_options] Selected VG: {selected_vg}, Max available GB: {max_size_gb}")
# 确保最大值至少大于等于最小值
effective_max = max(0.01, max_size_gb)
self.size_spin.configure(to=effective_max)
self.max_gb = effective_max
logger.debug(f"[_update_size_options] Spinbox max set to: {effective_max} GB.")
# 应用复选框状态
self._toggle_size_input()
logger.debug("[_update_size_options] Completed.")
def _toggle_size_input(self):
"""切换大小输入框状态"""
is_checked = self.use_max_var.get()
logger.debug(f"[_toggle_size_input] Called. Checkbox isChecked(): {is_checked}")
if is_checked:
logger.debug(f"[_toggle_size_input] Checkbox is CHECKED. Disabling spinbox and setting value to max.")
self.size_spin.configure(state=tk.DISABLED)
self.size_var.set(self.max_gb)
logger.debug(f"[_toggle_size_input] Spinbox value set to max: {self.max_gb} GB.")
else:
logger.debug(f"[_toggle_size_input] Checkbox is UNCHECKED. Enabling spinbox.")
self.size_spin.configure(state=tk.NORMAL)
current_val = self.size_var.get()
if current_val >= self.max_gb:
self.size_var.set(0.01)
logger.debug(f"[_toggle_size_input] Spinbox was at max, reset to min: 0.01 GB.")
else:
logger.debug(f"[_toggle_size_input] Spinbox was not at max, keeping current value: {current_val} GB.")
logger.debug("[_toggle_size_input] Completed.")
def _on_confirm(self):
"""确定按钮回调"""
lv_name = self.lv_name_var.get().strip()
vg_name = self.vg_var.get()
size_gb = self.size_var.get()
use_max_space = self.use_max_var.get()
if not lv_name:
messagebox.showwarning("输入错误", "逻辑卷名称不能为空。")
return
if not vg_name:
messagebox.showwarning("输入错误", "请选择一个卷组。")
return
if not use_max_space:
if size_gb <= 0:
messagebox.showwarning("输入错误", "逻辑卷大小必须大于0。")
return
if size_gb > self.max_gb:
messagebox.showwarning("输入错误", "逻辑卷大小不能超过卷组的可用空间。")
return
self.result = {
'lv_name': lv_name,
'vg_name': vg_name,
'size_gb': size_gb,
'use_max_space': use_max_space
}
self.dialog.destroy()
def get_lv_info(self):
"""获取 LV 信息"""
return self.result

539
disk_operations_tkinter.py Normal file
View File

@@ -0,0 +1,539 @@
# disk_operations_tkinter.py
import subprocess
import logging
import re
import os
import threading
import queue
from tkinter import messagebox, simpledialog
from system_info import SystemInfoManager
logger = logging.getLogger(__name__)
# --- 后台格式化工作线程 ---
class FormatWorker:
"""后台格式化工作线程"""
def __init__(self, device_path, fs_type, execute_shell_command_func, result_queue):
self.device_path = device_path
self.fs_type = fs_type
self._execute_shell_command_func = execute_shell_command_func
self.result_queue = result_queue
def run(self):
"""在单独线程中执行格式化命令。"""
logger.info(f"后台格式化开始: 设备 {self.device_path}, 文件系统 {self.fs_type}")
# 发送开始信号
self.result_queue.put(('started', self.device_path))
command_list = []
if self.fs_type == "ext4":
command_list = ["mkfs.ext4", "-F", self.device_path]
elif self.fs_type == "xfs":
command_list = ["mkfs.xfs", "-f", self.device_path]
elif self.fs_type == "ntfs":
command_list = ["mkfs.ntfs", "-f", self.device_path]
elif self.fs_type == "fat32":
command_list = ["mkfs.vfat", "-F", "32", self.device_path]
else:
logger.error(f"不支持的文件系统类型: {self.fs_type}")
self.result_queue.put(('finished', False, self.device_path, "", f"不支持的文件系统类型: {self.fs_type}"))
return
# 调用 shell 命令执行函数
success, stdout, stderr = self._execute_shell_command_func(
command_list,
f"格式化设备 {self.device_path}{self.fs_type} 失败",
root_privilege=True,
show_dialog=False
)
self.result_queue.put(('finished', success, self.device_path, stdout, stderr))
logger.info(f"后台格式化完成: 设备 {self.device_path}, 成功: {success}")
class DiskOperations:
"""磁盘操作类 - Tkinter 版本"""
def __init__(self, system_manager: SystemInfoManager, lvm_ops):
self.system_manager = system_manager
self._lvm_ops = lvm_ops
self.active_format_workers = {}
self.result_queue = queue.Queue()
self.formatting_callback = None
self.root = None
def set_formatting_callback(self, callback):
"""设置格式化完成回调函数"""
self.formatting_callback = callback
def start_queue_check(self, root):
"""启动队列检查(应在主线程中调用)"""
self.root = root
self._check_queue()
def _check_queue(self):
"""定期检查队列"""
try:
while True:
msg = self.result_queue.get_nowait()
msg_type = msg[0]
if msg_type == 'started':
device_path = msg[1]
logger.info(f"格式化开始: {device_path}")
elif msg_type == 'finished':
_, success, device_path, stdout, stderr = msg
self._on_formatting_finished(success, device_path, stdout, stderr)
except queue.Empty:
pass
# 继续定时检查
if self.root:
self.root.after(100, self._check_queue)
def _execute_shell_command(self, command_list, error_message, root_privilege=True,
suppress_critical_dialog_on_stderr_match=None, input_data=None,
show_dialog=True):
"""执行 shell 命令"""
return self._lvm_ops._execute_shell_command(
command_list, error_message, root_privilege, suppress_critical_dialog_on_stderr_match, input_data, show_dialog
)
def _add_to_fstab(self, device_path, mount_point, fstype, uuid):
"""将设备的挂载信息添加到 /etc/fstab"""
if not uuid or not mount_point or not fstype:
logger.error(f"无法将设备 {device_path} 添加到 fstab缺少 UUID/挂载点/文件系统类型。")
messagebox.showwarning("警告", f"无法将设备 {device_path} 添加到 fstab缺少 UUID/挂载点/文件系统类型。")
return False
fstab_entry = f"UUID={uuid} {mount_point} {fstype} defaults 0 2"
fstab_path = "/etc/fstab"
try:
# 检查 fstab 中是否已存在相同 UUID 的条目
with open(fstab_path, 'r') as f:
fstab_content = f.readlines()
for line in fstab_content:
if f"UUID={uuid}" in line:
logger.warning(f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。跳过添加。")
messagebox.showinfo("信息", f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。")
return True
# 追加到 fstab
success, _, stderr = self._execute_shell_command(
["sh", "-c", f"echo '{fstab_entry}' >> {fstab_path}"],
f"{device_path} 添加到 {fstab_path} 失败",
root_privilege=True
)
if success:
logger.info(f"已将 {fstab_entry} 添加到 {fstab_path}")
messagebox.showinfo("成功", f"设备 {device_path} 已成功添加到 /etc/fstab。")
return True
else:
return False
except Exception as e:
logger.error(f"处理 {fstab_path} 失败: {e}")
messagebox.showerror("错误", f"处理 {fstab_path} 失败: {e}")
return False
def _remove_fstab_entry(self, device_path):
"""从 /etc/fstab 中移除指定设备的条目"""
device_details = self.system_manager.get_device_details_by_path(device_path)
if not device_details or not device_details.get('uuid'):
logger.warning(f"无法获取设备 {device_path} 的 UUID无法从 fstab 中移除。")
return False
uuid = device_details.get('uuid')
fstab_path = "/etc/fstab"
command = ["sed", "-i", f"/{re.escape(uuid)}/d", fstab_path]
success, _, stderr = self._execute_shell_command(
command,
f"{fstab_path} 中删除 UUID={uuid} 的条目失败",
root_privilege=True,
suppress_critical_dialog_on_stderr_match=(
f"sed: {fstab_path}: No such file or directory",
f"sed: {fstab_path}: 没有那个文件或目录",
"no changes were made"
)
)
if success:
logger.info(f"已从 {fstab_path} 中删除 UUID={uuid} 的条目。")
return True
else:
if any(s in stderr for s in (
f"sed: {fstab_path}: No such file or directory",
f"sed: {fstab_path}: 没有那个文件或目录",
"no changes were made",
"No such file or directory"
)):
logger.info(f"fstab 中未找到 UUID={uuid} 的条目,无需删除。")
return True
return False
def mount_partition(self, device_path, mount_point, add_to_fstab=False):
"""挂载指定设备到指定挂载点"""
if not os.path.exists(mount_point):
try:
os.makedirs(mount_point)
logger.info(f"创建挂载点目录: {mount_point}")
except OSError as e:
messagebox.showerror("错误", f"创建挂载点目录 {mount_point} 失败: {e}")
logger.error(f"创建挂载点目录 {mount_point} 失败: {e}")
return False
logger.info(f"尝试挂载设备 {device_path}{mount_point}")
success, _, stderr = self._execute_shell_command(
["mount", device_path, mount_point],
f"挂载设备 {device_path} 失败"
)
if success:
messagebox.showinfo("成功", f"设备 {device_path} 已成功挂载到 {mount_point}")
if add_to_fstab:
device_details = self.system_manager.get_device_details_by_path(device_path)
if device_details:
fstype = device_details.get('fstype')
uuid = device_details.get('uuid')
if fstype and uuid:
self._add_to_fstab(device_path, mount_point, fstype, uuid)
else:
logger.error(f"无法获取设备 {device_path} 的文件系统类型或 UUID 以添加到 fstab。")
messagebox.showwarning("警告", f"设备 {device_path} 已挂载,但无法获取文件系统类型或 UUID 以添加到 fstab。")
else:
logger.error(f"无法获取设备 {device_path} 的详细信息以添加到 fstab。")
messagebox.showwarning("警告", f"设备 {device_path} 已挂载,但无法获取详细信息以添加到 fstab。")
return True
else:
return False
def unmount_partition(self, device_path, show_dialog_on_error=True):
"""卸载指定设备"""
logger.info(f"尝试卸载设备 {device_path}")
already_unmounted_errors = ("not mounted", "未挂载", "未指定挂载点")
success, _, stderr = self._execute_shell_command(
["umount", device_path],
f"卸载设备 {device_path} 失败",
suppress_critical_dialog_on_stderr_match=already_unmounted_errors,
show_dialog=show_dialog_on_error
)
if success:
messagebox.showinfo("成功", f"设备 {device_path} 已成功卸载。")
return True
else:
is_already_unmounted_error = any(s in stderr for s in already_unmounted_errors)
if is_already_unmounted_error:
logger.info(f"设备 {device_path} 已经处于未挂载状态。")
return True
else:
return False
def get_disk_free_space_info_mib(self, disk_path, total_disk_mib):
"""获取磁盘上最大的空闲空间块的起始位置和大小"""
logger.debug(f"尝试获取磁盘 {disk_path} 的空闲空间信息 (MiB)。")
success, stdout, stderr = self._execute_shell_command(
["parted", "-s", disk_path, "unit", "MiB", "print", "free"],
f"获取磁盘 {disk_path} 分区信息失败",
root_privilege=True,
suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label")
)
if not success:
logger.error(f"获取磁盘 {disk_path} 空闲空间信息失败: {stderr}")
return None, None
free_spaces = []
lines = stdout.splitlines()
free_space_line_pattern = re.compile(r'^\s*(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(?:Free Space|空闲空间|可用空间)')
for line in lines:
match = free_space_line_pattern.match(line)
if match:
try:
start_mib = float(match.group(1))
size_mib = float(match.group(3))
free_spaces.append({'start_mib': start_mib, 'size_mib': size_mib})
except ValueError as ve:
logger.warning(f"解析 parted free space 行 '{line}' 失败: {ve}")
continue
if not free_spaces:
logger.warning(f"在磁盘 {disk_path} 上未找到空闲空间。")
return None, None
largest_free_space = max(free_spaces, key=lambda x: x['size_mib'])
start_mib = largest_free_space['start_mib']
size_mib = largest_free_space['size_mib']
logger.debug(f"磁盘 {disk_path} 的最大空闲空间块起始于 {start_mib:.2f} MiB大小为 {size_mib:.2f} MiB。")
return start_mib, size_mib
def create_partition(self, disk_path, partition_table_type, size_gb, total_disk_mib, use_max_space):
"""在指定磁盘上创建分区"""
if not isinstance(disk_path, str) or not isinstance(partition_table_type, str):
logger.error(f"尝试创建分区时传入无效参数。磁盘路径: {disk_path}, 分区表类型: {partition_table_type}")
messagebox.showerror("错误", "无效的磁盘路径或分区表类型。")
return False
# 1. 检查磁盘是否有分区表
has_partition_table = False
success_check, stdout_check, stderr_check = self._execute_shell_command(
["parted", "-s", disk_path, "print"],
f"检查磁盘 {disk_path} 分区表失败",
suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label"),
root_privilege=True
)
if not success_check:
if any(s in stderr_check for s in ("无法辨识的磁盘卷标", "unrecognized disk label")):
logger.info(f"磁盘 {disk_path} 没有可识别的分区表。")
has_partition_table = False
else:
logger.error(f"检查磁盘 {disk_path} 分区表时发生非预期的错误: {stderr_check}")
return False
else:
if "Partition Table: unknown" not in stdout_check and "分区表unknown" not in stdout_check:
has_partition_table = True
else:
logger.info(f"parted print 报告磁盘 {disk_path} 的分区表为 'unknown'")
has_partition_table = False
actual_start_mib_for_parted = 0.0
# 2. 如果没有分区表,则创建分区表
if not has_partition_table:
if not messagebox.askyesno("确认创建分区表",
f"磁盘 {disk_path} 没有分区表。您确定要创建 {partition_table_type} 分区表吗?\n"
f"此操作将擦除磁盘上的所有数据。",
default=messagebox.NO):
logger.info(f"用户取消了在 {disk_path} 上创建分区表的操作。")
return False
logger.info(f"尝试在 {disk_path} 上创建 {partition_table_type} 分区表。")
success, _, stderr = self._execute_shell_command(
["parted", "-s", disk_path, "mklabel", partition_table_type],
f"创建 {partition_table_type} 分区表失败"
)
if not success:
return False
actual_start_mib_for_parted = 1.0
else:
start_mib_from_parted, _ = self.get_disk_free_space_info_mib(disk_path, total_disk_mib)
if start_mib_from_parted is None:
messagebox.showerror("错误", f"无法确定磁盘 {disk_path} 的分区起始位置或没有空闲空间。")
return False
if start_mib_from_parted < 1.0:
actual_start_mib_for_parted = 1.0
else:
actual_start_mib_for_parted = start_mib_from_parted
# 3. 确定分区结束位置
if use_max_space:
end_pos = "100%"
size_for_log = "最大可用空间"
else:
end_mib = actual_start_mib_for_parted + size_gb * 1024
if end_mib > total_disk_mib:
messagebox.showwarning("警告", f"请求的分区大小 ({size_gb}GB) 超出了可用空间。将调整为最大可用空间。")
end_pos = "100%"
size_for_log = "最大可用空间"
else:
end_pos = f"{end_mib}MiB"
size_for_log = f"{size_gb}GB"
# 4. 创建分区
create_cmd = ["parted", "-s", disk_path, "mkpart", "primary", f"{actual_start_mib_for_parted}MiB", end_pos]
logger.info(f"尝试在 {disk_path} 上创建 {size_for_log} 的主分区。命令: {' '.join(create_cmd)}")
success, _, stderr = self._execute_shell_command(
create_cmd,
f"{disk_path} 上创建分区失败"
)
if success:
messagebox.showinfo("成功", f"{disk_path} 上成功创建了 {size_for_log} 的分区。")
return True
else:
return False
def delete_partition(self, device_path):
"""删除指定分区"""
if not messagebox.askyesno("确认删除分区",
f"您确定要删除分区 {device_path} 吗?此操作将擦除分区上的所有数据!",
default=messagebox.NO):
logger.info(f"用户取消了删除分区 {device_path} 的操作。")
return False
# 尝试卸载分区
self.unmount_partition(device_path, show_dialog_on_error=False)
# 从 fstab 中移除条目
self._remove_fstab_entry(device_path)
# 获取父磁盘和分区号
match = re.match(r'(/dev/[a-z]+)(\d+)', device_path)
if not match:
messagebox.showerror("错误", f"无法解析设备路径 {device_path}")
logger.error(f"无法解析设备路径 {device_path}")
return False
disk_path = match.group(1)
partition_number = match.group(2)
logger.info(f"尝试删除分区 {device_path} (磁盘: {disk_path}, 分区号: {partition_number})。")
success, _, stderr = self._execute_shell_command(
["parted", "-s", disk_path, "rm", partition_number],
f"删除分区 {device_path} 失败"
)
if success:
messagebox.showinfo("成功", f"分区 {device_path} 已成功删除。")
return True
else:
return False
def format_partition(self, device_path, fstype=None):
"""启动后台格式化线程"""
if fstype is None:
# 使用简单的输入对话框
fstype = simpledialog.askstring("选择文件系统",
f"请选择要使用的文件系统类型 for {device_path}:\n\next4, xfs, fat32, ntfs",
initialvalue="ext4")
if not fstype:
logger.info("用户取消了文件系统选择。")
return False
if fstype not in ["ext4", "xfs", "fat32", "ntfs"]:
messagebox.showerror("错误", f"不支持的文件系统类型: {fstype}")
return False
if not messagebox.askyesno("确认格式化",
f"您确定要格式化设备 {device_path}{fstype} 吗?此操作将擦除所有数据!",
default=messagebox.NO):
logger.info(f"用户取消了格式化 {device_path} 的操作。")
return False
# 检查是否已有格式化任务正在进行
if device_path in self.active_format_workers:
messagebox.showwarning("警告", f"设备 {device_path} 正在格式化中,请勿重复操作。")
return False
# 尝试卸载分区
self.unmount_partition(device_path, show_dialog_on_error=False)
# 从 fstab 中移除条目
self._remove_fstab_entry(device_path)
# 创建并启动后台线程
worker = FormatWorker(device_path, fstype, self._execute_shell_command, self.result_queue)
thread = threading.Thread(target=worker.run, daemon=True)
self.active_format_workers[device_path] = thread
thread.start()
messagebox.showinfo("开始格式化", f"设备 {device_path} 正在后台格式化为 {fstype}。完成后将通知您。")
return True
def _on_formatting_finished(self, success, device_path, stdout, stderr):
"""处理格式化工作线程完成后的结果"""
if device_path in self.active_format_workers:
del self.active_format_workers[device_path]
if success:
messagebox.showinfo("格式化成功", f"设备 {device_path} 已成功格式化。")
else:
messagebox.showerror("格式化失败", f"格式化设备 {device_path} 失败。\n错误详情: {stderr}")
# 调用回调函数通知主窗口
if self.formatting_callback:
self.formatting_callback(success, device_path, stdout, stderr)
# === Loop 设备挂载/卸载功能 ===
def mount_loop_device(self, device_path):
"""挂载 ROM 设备为 loop 设备"""
# 获取默认挂载点
default_mount = f"/mnt/{os.path.basename(device_path)}"
mount_point = simpledialog.askstring(
"挂载 ROM 设备",
f"请输入挂载点路径 for {device_path}:",
initialvalue=default_mount
)
if not mount_point:
logger.info("用户取消了挂载操作。")
return False
# 创建挂载点目录
if not os.path.exists(mount_point):
try:
os.makedirs(mount_point)
logger.info(f"创建挂载点目录: {mount_point}")
except OSError as e:
messagebox.showerror("错误", f"创建挂载点目录 {mount_point} 失败: {e}")
return False
# 使用 mount -o loop 挂载
logger.info(f"尝试以 loop 方式挂载 ROM 设备 {device_path}{mount_point}")
success, _, stderr = self._execute_shell_command(
["mount", "-o", "loop,ro", device_path, mount_point],
f"挂载 ROM 设备 {device_path} 失败"
)
if success:
messagebox.showinfo("成功", f"ROM 设备 {device_path} 已成功挂载到 {mount_point}")
return True
else:
# 检查是否是因为设备不是有效的文件系统
if "wrong fs type" in stderr.lower() or "bad option" in stderr.lower():
messagebox.showerror("错误",
f"无法挂载 {device_path}\n"
f"可能的原因:\n"
f"1. 设备中没有可读取的介质(如光盘未插入)\n"
f"2. 设备不支持 loop 挂载\n\n"
f"错误详情: {stderr}")
return False
def unmount_loop_device(self, device_path):
"""卸载 loop 设备"""
logger.info(f"尝试卸载 loop 设备 {device_path}")
# 首先尝试普通卸载
success, _, stderr = self._execute_shell_command(
["umount", device_path],
f"卸载设备 {device_path} 失败",
show_dialog=False
)
if success:
messagebox.showinfo("成功", f"设备 {device_path} 已成功卸载。")
return True
# 检查是否是因为设备未挂载
if "not mounted" in stderr.lower() or "未挂载" in stderr:
# 尝试查找关联的 loop 设备并卸载
try:
result = subprocess.run(
["losetup", "-a"],
capture_output=True, text=True, check=False
)
if result.returncode == 0:
for line in result.stdout.splitlines():
if device_path in line:
loop_dev = line.split(':')[0]
logger.info(f"找到关联的 loop 设备: {loop_dev}")
# 卸载 loop 设备
success, _, _ = self._execute_shell_command(
["losetup", "-d", loop_dev],
f"删除 loop 设备 {loop_dev} 失败",
show_dialog=False
)
if success:
messagebox.showinfo("成功", f"Loop 设备 {loop_dev} 已卸载。")
return True
except Exception as e:
logger.error(f"查找 loop 设备时出错: {e}")
messagebox.showerror("错误", f"卸载设备 {device_path} 失败:\n{stderr}")
return False

84
logger_config_tkinter.py Normal file
View File

@@ -0,0 +1,84 @@
# logger_config_tkinter.py
import logging
import tkinter as tk
from tkinter.scrolledtext import ScrolledText
import queue
class TkinterLogHandler(logging.Handler):
"""
自定义的 logging 处理器,将日志消息发送到 Tkinter 文本控件。
使用队列确保线程安全。
"""
def __init__(self, text_widget: ScrolledText):
super().__init__()
self.text_widget = text_widget
self.queue = queue.Queue()
self._schedule_check()
def _schedule_check(self):
"""定期检查队列并更新 UI"""
self._check_queue()
# 每 100ms 检查一次队列
self.text_widget.after(100, self._schedule_check)
def _check_queue(self):
"""处理队列中的日志消息"""
try:
while True:
msg = self.queue.get_nowait()
self._append_to_widget(msg)
except queue.Empty:
pass
def _append_to_widget(self, msg):
"""将消息追加到文本控件"""
try:
self.text_widget.configure(state=tk.NORMAL)
self.text_widget.insert(tk.END, msg + '\n')
self.text_widget.see(tk.END) # 滚动到底部
self.text_widget.configure(state=tk.DISABLED)
except Exception as e:
# 如果控件已销毁,忽略错误
pass
def emit(self, record):
"""
处理日志记录,将其格式化并放入队列。
"""
try:
msg = self.format(record)
self.queue.put(msg)
except Exception:
self.handleError(record)
def setup_logging(text_widget: ScrolledText):
"""
配置全局 logging使其输出到控制台和指定的 Tkinter 文本控件。
"""
root_logger = logging.getLogger()
# 设置日志级别
root_logger.setLevel(logging.DEBUG)
# 清除现有的处理器
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# 1. 控制台处理器
console_handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
# 2. Tkinter 文本控件处理器
text_handler = TkinterLogHandler(text_widget)
text_handler.setFormatter(formatter)
root_logger.addHandler(text_handler)
return root_logger
# 获取一个全局的 logger 实例
logger = logging.getLogger(__name__)

291
lvm_operations_tkinter.py Normal file
View File

@@ -0,0 +1,291 @@
# lvm_operations_tkinter.py
import logging
import subprocess
from tkinter import messagebox
from system_info import SystemInfoManager
logger = logging.getLogger(__name__)
class LvmOperations:
"""LVM 操作类 - Tkinter 版本"""
def __init__(self, system_manager: SystemInfoManager = None):
self.system_manager = system_manager
def _execute_shell_command(self, command_list, error_message, root_privilege=True,
suppress_critical_dialog_on_stderr_match=None, input_data=None,
show_dialog=True):
"""执行 shell 命令"""
full_cmd_str = ' '.join(command_list)
logger.info(f"执行命令: {full_cmd_str}")
try:
if self.system_manager:
stdout, stderr = self.system_manager._run_command(
command_list,
root_privilege=root_privilege,
check_output=True,
input_data=input_data
)
else:
result = subprocess.run(
["sudo"] + command_list if root_privilege else command_list,
capture_output=True, text=True, check=True,
input=input_data
)
stdout = result.stdout
stderr = result.stderr
if stdout:
logger.debug(f"命令 '{full_cmd_str}' 标准输出:\n{stdout.strip()}")
if stderr:
logger.debug(f"命令 '{full_cmd_str}' 标准错误:\n{stderr.strip()}")
return True, stdout, stderr
except subprocess.CalledProcessError as e:
stderr_output = e.stderr.strip() if e.stderr else ""
logger.error(f"{error_message} 命令: {full_cmd_str}")
logger.error(f"退出码: {e.returncode}")
logger.error(f"标准输出: {e.stdout.strip() if e.stdout else ''}")
logger.error(f"标准错误: {stderr_output}")
should_suppress_dialog = False
if suppress_critical_dialog_on_stderr_match:
if isinstance(suppress_critical_dialog_on_stderr_match, str):
if suppress_critical_dialog_on_stderr_match in stderr_output:
should_suppress_dialog = True
elif isinstance(suppress_critical_dialog_on_stderr_match, (list, tuple)):
for pattern in suppress_critical_dialog_on_stderr_match:
if pattern in stderr_output:
should_suppress_dialog = True
break
if should_suppress_dialog:
logger.info(f"特定错误 '{stderr_output}' 匹配抑制条件,已抑制错误对话框。")
elif show_dialog:
messagebox.showerror("错误", f"{error_message}\n错误详情: {stderr_output}")
return False, e.stdout if e.stdout else "", stderr_output
except FileNotFoundError:
logger.error(f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
if show_dialog:
messagebox.showerror("错误", f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
return False, "", "Command not found."
except Exception as e:
logger.error(f"执行命令时发生未知错误: {e}")
if show_dialog:
messagebox.showerror("错误", f"执行命令时发生未知错误: {e}")
return False, "", str(e)
def create_pv(self, device_path):
"""创建物理卷"""
if not device_path or not device_path.startswith('/dev/'):
messagebox.showerror("错误", "无效的设备路径。")
return False
if not messagebox.askyesno("确认创建物理卷",
f"您确定要在 {device_path} 上创建物理卷吗?\n"
"此操作将擦除设备上的数据!",
default=messagebox.NO):
logger.info(f"用户取消了在 {device_path} 上创建物理卷的操作。")
return False
logger.info(f"尝试在 {device_path} 上创建物理卷。")
success, stdout, stderr = self._execute_shell_command(
["pvcreate", "-y", device_path],
f"{device_path} 上创建物理卷失败"
)
if success:
messagebox.showinfo("成功", f"物理卷已在 {device_path} 上成功创建。")
return True
else:
if "partition" in stderr.lower() or "分区" in stderr:
if messagebox.askyesno("警告",
f"设备 {device_path} 似乎已有分区。\n"
"是否继续擦除并创建物理卷?",
default=messagebox.NO):
success_wipe, _, _ = self._execute_shell_command(
["wipefs", "-a", device_path],
f"擦除设备 {device_path} 失败"
)
if success_wipe:
success_pv, _, stderr_pv = self._execute_shell_command(
["pvcreate", "-y", device_path],
f"{device_path} 上创建物理卷失败"
)
if success_pv:
messagebox.showinfo("成功", f"物理卷已在 {device_path} 上成功创建。")
return True
else:
messagebox.showerror("错误", f"{device_path} 上创建物理卷失败: {stderr_pv}")
else:
messagebox.showinfo("信息", f"未在已分区设备 {device_path} 上创建物理卷。")
return False
def delete_pv(self, device_path):
"""删除物理卷"""
if not device_path or not device_path.startswith('/dev/'):
messagebox.showerror("错误", "无效的设备路径。")
return False
if not messagebox.askyesno("确认删除物理卷",
f"您确定要删除物理卷 {device_path} 吗?",
default=messagebox.NO):
logger.info(f"用户取消了删除物理卷 {device_path} 的操作。")
return False
logger.info(f"尝试删除物理卷 {device_path}")
success, stdout, stderr = self._execute_shell_command(
["pvremove", "-y", device_path],
f"删除物理卷 {device_path} 失败"
)
if success:
messagebox.showinfo("成功", f"物理卷 {device_path} 已成功删除。")
return True
return False
def create_vg(self, vg_name, pvs):
"""创建卷组"""
if not vg_name or not pvs:
messagebox.showerror("错误", "无效的卷组名称或物理卷路径。")
return False
if not messagebox.askyesno("确认创建卷组",
f"您确定要创建卷组 {vg_name} 吗?\n"
f"使用物理卷: {', '.join(pvs)}",
default=messagebox.NO):
logger.info(f"用户取消了创建卷组 {vg_name} 的操作。")
return False
logger.info(f"尝试创建卷组 {vg_name},使用物理卷: {pvs}")
cmd = ["vgcreate", vg_name] + pvs
success, stdout, stderr = self._execute_shell_command(
cmd,
f"创建卷组 {vg_name} 失败"
)
if success:
messagebox.showinfo("成功", f"卷组 {vg_name} 已成功创建。")
return True
return False
def delete_vg(self, vg_name):
"""删除卷组"""
if not vg_name:
messagebox.showerror("错误", "无效的卷组名称。")
return False
if not messagebox.askyesno("确认删除卷组",
f"您确定要删除卷组 {vg_name} 吗?\n"
"此操作将删除该卷组及其所有逻辑卷!",
default=messagebox.NO):
logger.info(f"用户取消了删除卷组 {vg_name} 的操作。")
return False
logger.info(f"尝试删除卷组 {vg_name}")
success, stdout, stderr = self._execute_shell_command(
["vgremove", "-y", vg_name],
f"删除卷组 {vg_name} 失败"
)
if success:
messagebox.showinfo("成功", f"卷组 {vg_name} 已成功删除。")
return True
return False
def create_lv(self, lv_name, vg_name, size_gb, use_max_space=False):
"""创建逻辑卷"""
if not lv_name or not vg_name:
messagebox.showerror("错误", "无效的逻辑卷名称或卷组名称。")
return False
if not use_max_space and size_gb <= 0:
messagebox.showerror("错误", "逻辑卷大小必须大于0。")
return False
size_str = "100%FREE" if use_max_space else f"{size_gb}G"
if not messagebox.askyesno("确认创建逻辑卷",
f"您确定要在卷组 {vg_name} 中创建逻辑卷 {lv_name} 吗?\n"
f"大小: {size_str}",
default=messagebox.NO):
logger.info(f"用户取消了创建逻辑卷 {lv_name} 的操作。")
return False
logger.info(f"尝试在卷组 {vg_name} 中创建逻辑卷 {lv_name},大小: {size_str}")
cmd = ["lvcreate", "-y", "-n", lv_name, "-L", size_str, vg_name] if not use_max_space else \
["lvcreate", "-y", "-n", lv_name, "-l", "100%FREE", vg_name]
success, stdout, stderr = self._execute_shell_command(
cmd,
f"创建逻辑卷 {lv_name} 失败"
)
if success:
messagebox.showinfo("成功", f"逻辑卷 {lv_name} 已在卷组 {vg_name} 中成功创建。")
return True
return False
def delete_lv(self, lv_name, vg_name):
"""删除逻辑卷"""
if not lv_name or not vg_name:
messagebox.showerror("错误", "无效的逻辑卷名称或卷组名称。")
return False
if not messagebox.askyesno("确认删除逻辑卷",
f"您确定要删除逻辑卷 {vg_name}/{lv_name} 吗?\n"
"此操作将删除该逻辑卷及其所有数据!",
default=messagebox.NO):
logger.info(f"用户取消了删除逻辑卷 {lv_name} 的操作。")
return False
logger.info(f"尝试删除逻辑卷 {vg_name}/{lv_name}")
success, stdout, stderr = self._execute_shell_command(
["lvremove", "-y", f"{vg_name}/{lv_name}"],
f"删除逻辑卷 {vg_name}/{lv_name} 失败"
)
if success:
messagebox.showinfo("成功", f"逻辑卷 {vg_name}/{lv_name} 已成功删除。")
return True
return False
def activate_lv(self, lv_name, vg_name):
"""激活逻辑卷"""
if not lv_name or not vg_name:
messagebox.showerror("错误", "无效的逻辑卷名称或卷组名称。")
return False
logger.info(f"尝试激活逻辑卷 {vg_name}/{lv_name}")
success, stdout, stderr = self._execute_shell_command(
["lvchange", "-ay", f"{vg_name}/{lv_name}"],
f"激活逻辑卷 {vg_name}/{lv_name} 失败"
)
if success:
messagebox.showinfo("成功", f"逻辑卷 {vg_name}/{lv_name} 已成功激活。")
return True
return False
def deactivate_lv(self, lv_name, vg_name):
"""停用逻辑卷"""
if not lv_name or not vg_name:
messagebox.showerror("错误", "无效的逻辑卷名称或卷组名称。")
return False
logger.info(f"尝试停用逻辑卷 {vg_name}/{lv_name}")
success, stdout, stderr = self._execute_shell_command(
["lvchange", "-an", f"{vg_name}/{lv_name}"],
f"停用逻辑卷 {vg_name}/{lv_name} 失败"
)
if success:
messagebox.showinfo("成功", f"逻辑卷 {vg_name}/{lv_name} 已成功停用。")
return True
return False

43
main_tkinter.py Normal file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Linux 存储管理工具 - Tkinter 版本
适配低版本系统(如 CentOS 8
"""
import tkinter as tk
from tkinter import ttk
import sys
import os
# 将当前目录添加到路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from mainwindow_tkinter import MainWindow
def main():
"""主程序入口"""
root = tk.Tk()
root.title("Linux 存储管理工具")
root.geometry("1200x800")
# 设置最小窗口大小
root.minsize(800, 600)
# 尝试设置 DPI 感知(仅在 Windows 上有效)
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except Exception:
pass
# 创建应用
app = MainWindow(root)
# 运行主循环
root.mainloop()
if __name__ == "__main__":
main()

1053
mainwindow_tkinter.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,179 @@
# occupation_resolver_tkinter.py
import logging
import subprocess
import os
from tkinter import messagebox
logger = logging.getLogger(__name__)
class OccupationResolver:
"""设备占用解除类 - Tkinter 版本"""
def __init__(self, shell_executor_func, mount_info_getter_func):
self.shell_executor_func = shell_executor_func
self.mount_info_getter_func = mount_info_getter_func
def resolve_occupation(self, device_path: str) -> bool:
"""处理设备占用问题"""
logger.info(f"开始处理设备 {device_path} 的占用问题。")
# 1. 获取挂载点
mount_point = self.mount_info_getter_func(device_path)
# 2. 如果设备已挂载,尝试卸载
if mount_point:
logger.info(f"设备 {device_path} 已挂载在 {mount_point}。检查是否有嵌套挂载...")
# 卸载嵌套挂载点
if not self._unmount_nested_mounts(mount_point):
messagebox.showerror("错误", f"未能卸载设备 {device_path} 下的所有嵌套挂载点。")
# 嵌套挂载卸载失败,可能是被进程占用,继续检查进程
else:
# 嵌套挂载卸载成功,尝试卸载设备本身
if self._unmount_device(device_path):
messagebox.showinfo("成功", f"设备 {device_path} 及其所有挂载点已成功卸载。")
return True
# 设备卸载失败,可能是被进程占用,继续检查进程
logger.warning(f"设备 {device_path} 卸载失败,可能存在进程占用。")
else:
logger.info(f"设备 {device_path} 当前未挂载。")
# 3. 检查是否有进程占用设备(无论是否已挂载,都检查进程占用)
if self._is_device_busy(device_path):
return self._kill_processes_using_device(device_path)
# 4. 如果之前有挂载点但卸载失败,且没有进程占用,说明是其他原因
if mount_point:
messagebox.showwarning("警告",
f"设备 {device_path} 卸载失败,但未检测到进程占用。\n"
f"可能是其他原因导致,请检查日志。")
return False
# 5. 无任何占用
messagebox.showinfo("信息", f"设备 {device_path} 未被任何进程占用。")
return True
def _unmount_nested_mounts(self, mount_point: str) -> bool:
"""卸载指定挂载点下的所有嵌套挂载"""
logger.info(f"尝试卸载 {mount_point} 下的所有嵌套挂载点。")
try:
with open('/proc/mounts', 'r') as f:
mounts = f.readlines()
nested_mounts = []
for line in mounts:
parts = line.split()
if len(parts) >= 2:
mp = parts[1]
if mp.startswith(mount_point + '/') or mp == mount_point:
nested_mounts.append(mp)
# 按深度排序,先卸载深层挂载
nested_mounts.sort(key=lambda x: x.count('/'), reverse=True)
for nm in nested_mounts:
logger.info(f"尝试卸载嵌套挂载点: {nm}")
success, _, _ = self.shell_executor_func(
["umount", nm],
f"卸载嵌套挂载点 {nm} 失败",
show_dialog=False
)
if not success:
logger.warning(f"卸载嵌套挂载点 {nm} 失败,但将继续尝试其他挂载点。")
return True
except Exception as e:
logger.error(f"卸载嵌套挂载点时发生错误: {e}")
return False
def _unmount_device(self, device_path: str) -> bool:
"""卸载设备"""
logger.info(f"尝试卸载设备 {device_path}")
success, _, stderr = self.shell_executor_func(
["umount", device_path],
f"卸载设备 {device_path} 失败",
show_dialog=False
)
if success:
logger.info(f"设备 {device_path} 已成功卸载。")
return True
else:
# 检查是否是因为设备已经未挂载
if "未挂载" in stderr or "not mounted" in stderr.lower():
logger.info(f"设备 {device_path} 已经处于未挂载状态。")
return True
logger.error(f"卸载设备 {device_path} 失败: {stderr}")
return False
def _is_device_busy(self, device_path: str) -> bool:
"""检查设备是否被进程占用"""
logger.info(f"检查设备 {device_path} 是否被进程占用。")
# 尝试使用 lsof
try:
result = subprocess.run(
["sudo", "lsof", device_path],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0 and result.stdout.strip():
logger.info(f"设备 {device_path} 被以下进程占用:\n{result.stdout}")
return True
except Exception as e:
logger.debug(f"lsof 检查失败: {e}")
# 尝试使用 fuser
try:
result = subprocess.run(
["sudo", "fuser", device_path],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0 and result.stdout.strip():
logger.info(f"设备 {device_path} 被以下进程占用: {result.stdout.strip()}")
return True
except Exception as e:
logger.debug(f"fuser 检查失败: {e}")
return False
def _kill_processes_using_device(self, device_path: str) -> bool:
"""终止占用设备的进程"""
logger.info(f"尝试终止占用设备 {device_path} 的进程。")
if messagebox.askyesno("确认终止进程",
f"设备 {device_path} 当前被进程占用。\n"
"是否终止这些进程?",
default=messagebox.NO):
try:
# 使用 fuser -k 终止进程
result = subprocess.run(
["sudo", "fuser", "-k", device_path],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0 or result.returncode == 1: # 1 表示没有进程被终止
messagebox.showinfo("成功", f"已尝试终止占用设备 {device_path} 的所有进程。")
# 再次尝试卸载
if self._unmount_device(device_path):
messagebox.showinfo("成功", f"设备 {device_path} 已成功卸载。")
return True
else:
# 设备可能已经没有挂载
mount_point = self.mount_info_getter_func(device_path)
if not mount_point:
messagebox.showinfo("成功", f"设备 {device_path} 已成功卸载。")
return True
messagebox.showwarning("警告", f"进程已终止,但未能成功卸载设备 {device_path}。可能仍有其他问题。")
return False
else:
messagebox.showerror("错误", f"未能终止占用设备 {device_path} 的进程。")
return False
except Exception as e:
logger.error(f"终止进程时发生错误: {e}")
messagebox.showerror("错误", f"终止进程时发生错误: {e}")
return False
else:
messagebox.showinfo("信息", f"已取消终止占用设备 {device_path} 的进程。")
return False

410
raid_operations_tkinter.py Normal file
View File

@@ -0,0 +1,410 @@
# raid_operations_tkinter.py
import logging
import subprocess
from tkinter import messagebox, simpledialog
from system_info import SystemInfoManager
import re
try:
import pexpect
PEXPECT_AVAILABLE = True
except ImportError:
PEXPECT_AVAILABLE = False
pexpect = None
import sys
logger = logging.getLogger(__name__)
class RaidOperations:
def __init__(self, system_manager: SystemInfoManager):
self.system_manager = system_manager
def _execute_shell_command(self, command_list, error_msg_prefix, suppress_critical_dialog_on_stderr_match=None, input_to_command=None):
"""执行一个shell命令并返回stdout和stderr"""
full_cmd_str = ' '.join(command_list)
logger.info(f"执行命令: {full_cmd_str}")
try:
stdout, stderr = self.system_manager._run_command(
command_list,
root_privilege=True,
check_output=True,
input_data=input_to_command
)
if stdout: logger.debug(f"命令 '{full_cmd_str}' 标准输出:\n{stdout.strip()}")
if stderr: logger.debug(f"命令 '{full_cmd_str}' 标准错误:\n{stderr.strip()}")
return True, stdout, stderr
except subprocess.CalledProcessError as e:
stderr_output = e.stderr.strip()
logger.error(f"{error_msg_prefix} 命令: {full_cmd_str}")
logger.error(f"退出码: {e.returncode}")
logger.error(f"标准输出: {e.stdout.strip()}")
logger.error(f"标准错误: {stderr_output}")
should_suppress_dialog = False
if suppress_critical_dialog_on_stderr_match:
if isinstance(suppress_critical_dialog_on_stderr_match, str):
if suppress_critical_dialog_on_stderr_match in stderr_output:
should_suppress_dialog = True
elif isinstance(suppress_critical_dialog_on_stderr_match, (list, tuple)):
for pattern in suppress_critical_dialog_on_stderr_match:
if pattern in stderr_output:
should_suppress_dialog = True
break
if should_suppress_dialog:
logger.info(f"特定错误 '{stderr_output}' 匹配抑制条件,已抑制错误对话框。")
else:
messagebox.showerror("错误", f"{error_msg_prefix}\n详细信息: {stderr_output}")
return False, e.stdout, stderr_output
except FileNotFoundError:
logger.error(f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
messagebox.showerror("错误", f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。")
return False, "", "Command not found."
except Exception as e:
logger.error(f"{error_msg_prefix} 命令: {full_cmd_str} 发生未知错误: {e}")
messagebox.showerror("错误", f"{error_msg_prefix}\n未知错误: {e}")
return False, "", str(e)
def _execute_interactive_mdadm_command(self, command_list, error_msg_prefix):
"""专门处理 mdadm --create 命令的交互式执行"""
if not PEXPECT_AVAILABLE:
logger.warning("pexpect 模块未安装,将尝试使用 --run 参数避免交互")
# 添加 --run 参数来避免交互式提示(自动回答 yes
if "--create" in command_list:
# 在 --create 后面插入 --run
create_idx = command_list.index("--create")
command_list.insert(create_idx + 1, "--run")
logger.info("已添加 --run 参数以自动确认创建")
# 回退到普通命令执行
return self._execute_shell_command(command_list, error_msg_prefix)
full_cmd_str = "sudo " + ' '.join(command_list)
logger.info(f"执行交互式命令: {full_cmd_str}")
stdout_buffer = []
try:
child = pexpect.spawn(full_cmd_str, encoding='utf-8', timeout=300)
child.logfile = sys.stdout
try:
index = child.expect(['[pP]assword for', pexpect.TIMEOUT], timeout=5)
if index == 0:
stdout_buffer.append(child.before)
password = simpledialog.askstring("Sudo 密码", "请输入您的 sudo 密码:", show='*')
if not password:
logger.error("sudo 密码未提供或取消。")
messagebox.showerror("错误", "sudo 密码未提供或取消。")
child.close(force=True)
return False, "", "Sudo password not provided."
child.sendline(password)
try:
auth_index = child.expect(['Authentication failure', pexpect.TIMEOUT], timeout=5)
if auth_index == 0:
logger.error("sudo 密码验证失败。")
messagebox.showerror("错误", "sudo 密码验证失败。请检查密码并重试。")
child.close(force=True)
return False, "", "Sudo authentication failed."
except pexpect.TIMEOUT:
logger.info("sudo 密码验证成功或无需密码。继续...")
except pexpect.TIMEOUT:
logger.info("未在 5 秒内收到密码提示,假定命令无需密码或已在缓存中。继续...")
interaction_patterns = [
r'Continue creating array.*\?',
r'partition table exists.*Continue',
r'write-indent bitmap.*enable it now.*\?',
r'bitmap.*\[y/N\]\?',
]
pattern_descriptions = {
0: "mdadm 询问是否继续创建阵列(分区表将被清除)",
1: "mdadm 提示分区表存在,询问是否继续",
2: "mdadm 询问是否启用 write-intent bitmap",
3: "mdadm 询问是否启用 bitmap",
}
while True:
try:
index = child.expect(interaction_patterns + [pexpect.EOF, pexpect.TIMEOUT], timeout=300)
if index < len(interaction_patterns):
# 匹配到交互式提示
matched_text = child.after
logger.info(f"检测到交互式提示 (index={index}): {matched_text}")
# 根据不同的提示给出不同的处理
if index == 0 or index == 1:
# 继续创建阵列(分区表相关)
if messagebox.askyesno("mdadm 提示",
"设备上存在分区表,创建 RAID 将清除分区表。\n"
"是否继续创建 RAID 阵列?",
default=messagebox.YES):
child.sendline('y')
logger.info("用户确认继续创建 RAID 阵列。")
else:
child.sendline('n')
logger.info("用户选择不继续创建 RAID 阵列。中止操作。")
return False, "", "User aborted RAID creation."
elif index == 2 or index == 3:
# bitmap 相关提示
if messagebox.askyesno("mdadm 提示",
"mdadm 建议启用 write-intent bitmap 以优化恢复速度。\n"
"是否启用 bitmap",
default=messagebox.NO):
child.sendline('y')
logger.info("用户确认启用 bitmap。")
else:
child.sendline('n')
logger.info("用户选择不启用 bitmap继续创建阵列。")
elif index == len(interaction_patterns):
# EOF
break
elif index == len(interaction_patterns) + 1:
# TIMEOUT
logger.warning(f"等待 mdadm 命令输出时超时。已收集输出:\n{''.join(stdout_buffer)}")
messagebox.showerror("错误", f"{error_msg_prefix}\n详细信息: mdadm 命令在交互完成后未正常退出,等待 EOF 超时。")
return False, "".join(stdout_buffer), "Timeout waiting for EOF."
except pexpect.exceptions.EOF:
break
final_output = child.before
stdout_buffer.append(final_output)
child.close()
if child.exitstatus == 0:
logger.info(f"交互式命令成功完成: {full_cmd_str}")
return True, "".join(stdout_buffer), ""
else:
logger.error(f"交互式命令失败,退出码: {child.exitstatus}")
messagebox.showerror("错误", f"{error_msg_prefix}\n详细信息: 命令执行失败,退出码 {child.exitstatus}\n输出: {final_output}")
return False, "".join(stdout_buffer), f"Command failed with exit code {child.exitstatus}"
except pexpect.exceptions.ExceptionPexpect as e:
logger.error(f"交互式命令执行过程中发生 pexpect 错误: {e}")
messagebox.showerror("错误", f"{error_msg_prefix}\n详细信息: pexpect 内部错误: {e}")
return False, "", str(e)
except Exception as e:
logger.error(f"交互式命令执行时发生未知错误: {e}")
messagebox.showerror("错误", f"{error_msg_prefix}\n未知错误: {e}")
return False, "", str(e)
def create_raid_array(self, devices, level, chunk_size):
"""创建 RAID 阵列"""
if not isinstance(devices, list):
logger.error(f"传入的设备列表类型不是 list: {type(devices)}")
messagebox.showerror("错误", f"不支持的 RAID 级别类型: {type(level)}")
return False
if not devices:
messagebox.showerror("错误", "创建 RAID 阵列至少需要一个设备RAID0")
return False
num_devices = len(devices)
if level == "1" and num_devices < 2:
messagebox.showerror("错误", "RAID1 至少需要两个设备。")
return False
elif level == "5" and num_devices < 3:
messagebox.showerror("错误", "RAID5 至少需要三个设备。")
return False
elif level == "6" and num_devices < 4:
messagebox.showerror("错误", "RAID6 至少需要四个设备。")
return False
elif level == "10" and num_devices < 2:
messagebox.showerror("错误", "RAID10 至少需要两个设备。")
return False
if not messagebox.askyesno("确认创建 RAID 阵列",
f"您确定要创建 RAID {level} 阵列吗?\n"
f"这将使用设备: {', '.join(devices)}\n"
f"此操作会擦除这些设备上的数据!",
default=messagebox.NO):
logger.info("用户取消了创建 RAID 阵列的操作。")
return False
array_name = None
try:
mdadm_detail_result = self.system_manager._run_command(["mdadm", "--detail", "--scan"], root_privilege=True)
existing_arrays = re.findall(r'ARRAY\s+/dev/(md\d+)', mdadm_detail_result[0])
if existing_arrays:
max_num = max([int(re.search(r'\d+', name).group()) for name in existing_arrays])
array_name = f"/dev/md{max_num + 1}"
else:
array_name = "/dev/md0"
except Exception as e:
logger.warning(f"检测现有 RAID 阵列时出错: {e},将默认使用 /dev/md0。")
array_name = "/dev/md0"
create_cmd = [
"mdadm", "--create", array_name, "--level=" + level,
"--raid-devices=" + str(num_devices)
]
if chunk_size:
create_cmd.append("--chunk=" + str(chunk_size))
create_cmd.extend(devices)
logger.info(f"尝试创建 RAID {level} 阵列 {array_name},使用设备: {devices}")
success, stdout, stderr = self._execute_interactive_mdadm_command(create_cmd, f"创建 RAID {level} 阵列失败")
if success:
if "already exists" in stderr.lower() or "already exists" in stdout.lower():
messagebox.showerror("错误", f"创建 RAID 阵列失败:阵列名称 {array_name} 已被占用。请尝试停止或删除现有阵列。")
return False
messagebox.showinfo("成功", f"成功创建 RAID {level} 阵列 {array_name}")
try:
mdadm_conf_path = "/etc/mdadm.conf"
success_scan, stdout_scan, stderr_scan = self._execute_shell_command(
["mdadm", "--detail", "--scan"],
"生成 mdadm.conf 扫描信息失败",
suppress_critical_dialog_on_stderr_match="No arrays"
)
if success_scan:
success_mkdir, _, _ = self._execute_shell_command(
["mkdir", "-p", "/etc"],
"创建 /etc 目录失败"
)
if not success_mkdir:
messagebox.showerror("错误", f"无法创建 /etc 目录,更新 mdadm.conf 失败。\n详细信息: {stderr_mkdir}")
return False
success_conf, _, stderr_conf = self._execute_shell_command(
["sh", "-c", f"echo '{stdout_scan.strip()}' >> {mdadm_conf_path}"],
f"更新 {mdadm_conf_path} 失败"
)
if success_conf:
logger.info(f"已将新的 RAID 阵列信息追加到 {mdadm_conf_path}")
else:
logger.warning(f"无法追加到 {mdadm_conf_path}。这可能需要手动修复。错误: {stderr_conf}")
else:
logger.warning("mdadm --detail --scan 失败,无法自动更新 mdadm.conf。")
except Exception as e:
logger.error(f"更新 mdadm.conf 时发生错误: {e}")
return True
return False
def stop_raid_array(self, array_path):
"""停止 RAID 阵列"""
if not messagebox.askyesno("确认停止 RAID 阵列",
f"您确定要停止 RAID 阵列 {array_path} 吗?\n"
f"这将卸载该阵列(如果已挂载)并将其置为停止状态。",
default=messagebox.NO):
logger.info(f"用户取消了停止 RAID 阵列 {array_path} 的操作。")
return False
logger.info(f"尝试停止 RAID 阵列 {array_path}")
try:
stdout, stderr = self.system_manager._run_command(
["mdadm", "--stop", array_path],
root_privilege=True, check_output=True
)
messagebox.showinfo("成功", f"成功停止 RAID 阵列 {array_path}")
return True
except subprocess.CalledProcessError as e:
error_msg = e.stderr.strip() if e.stderr else str(e)
logger.error(f"停止 RAID 阵列 {array_path} 失败: {error_msg}")
messagebox.showerror("错误", f"停止 RAID 阵列 {array_path} 失败。\n错误: {error_msg}")
return False
def delete_active_raid_array(self, array_path, member_devices, array_uuid):
"""删除活动的 RAID 阵列"""
if not messagebox.askyesno("确认删除 RAID 阵列",
f"您确定要删除 RAID 阵列 {array_path} 吗?\n"
f"此操作将停止阵列并清除所有成员设备的超级块!",
default=messagebox.NO):
logger.info(f"用户取消了删除 RAID 阵列 {array_path} 的操作。")
return False
if not self.stop_raid_array(array_path):
messagebox.showerror("错误", f"删除 RAID 阵列 {array_path} 失败,因为无法停止阵列。")
return False
all_wiped_successfully = True
if member_devices:
for dev in member_devices:
if dev:
logger.info(f"尝试清除设备 {dev} 上的 RAID 超级块。")
wipe_success, _, wipe_stderr = self._execute_shell_command(
["mdadm", "--zero-superblock", dev],
f"清除设备 {dev} 的 RAID 超级块失败"
)
if not wipe_success:
logger.error(f"清除设备 {dev} 的 RAID 超级块失败: {wipe_stderr}")
all_wiped_successfully = False
try:
mdadm_conf_path = "/etc/mdadm.conf"
if array_uuid:
success_conf_rm, _, stderr_conf_rm = self._execute_shell_command(
["sed", "-i", f"/{re.escape(array_uuid)}/d", mdadm_conf_path],
f"{mdadm_conf_path} 中删除 RAID 条目失败",
suppress_critical_dialog_on_stderr_match=("No such file", "没有那个文件")
)
if not success_conf_rm:
logger.warning(f"无法从 {mdadm_conf_path} 中删除条目: {stderr_conf_rm}")
except Exception as e:
logger.error(f"更新 {mdadm_conf_path} 时发生错误: {e}")
if all_wiped_successfully:
messagebox.showinfo("成功", f"成功删除 RAID 阵列 {array_path}")
return True
else:
messagebox.showerror("错误", f"删除 RAID 阵列 {array_path} 完成,但部分成员设备超级块未能完全清除。")
return False
def delete_configured_raid_array(self, uuid):
"""删除停止状态 RAID 阵列的配置文件条目"""
if not messagebox.askyesno("确认删除 RAID 阵列配置",
f"您确定要删除 UUID 为 {uuid} 的 RAID 阵列配置吗?\n"
f"这将从 mdadm.conf 中删除该阵列的配置。",
default=messagebox.NO):
logger.info(f"用户取消了删除 RAID 阵列配置 (UUID: {uuid}) 的操作。")
return False
mdadm_conf_path = "/etc/mdadm.conf"
success, _, stderr = self._execute_shell_command(
["sed", "-i", f"/{re.escape(uuid)}/d", mdadm_conf_path],
f"{mdadm_conf_path} 中删除 RAID 条目失败"
)
if success:
messagebox.showinfo("成功", f"成功删除 UUID 为 {uuid} 的 RAID 阵列配置。")
return True
else:
messagebox.showerror("错误", f"删除 RAID 阵列配置失败: {e}")
return False
def activate_raid_array(self, array_path, array_uuid):
"""激活已停止的 RAID 阵列"""
if not array_uuid or array_uuid == 'N/A':
messagebox.showerror("错误", f"无法激活 RAID 阵列 {array_path}:缺少 UUID 信息。")
return False
if not messagebox.askyesno("确认激活 RAID 阵列",
f"您确定要激活 RAID 阵列 {array_path} (UUID: {array_uuid}) 吗?\n"
"这将尝试重新组装阵列。",
default=messagebox.NO):
logger.info(f"用户取消了激活 RAID 阵列 {array_path} 的操作。")
return False
logger.info(f"尝试激活 RAID 阵列 {array_path} (UUID: {array_uuid})。")
success, stdout, stderr = self._execute_shell_command(
["mdadm", "--assemble", "--uuid", array_uuid, array_path],
f"激活 RAID 阵列 {array_path} 失败"
)
if success:
messagebox.showinfo("成功", f"成功激活 RAID 阵列 {array_path}")
return True
return False

View File

@@ -1 +1,17 @@
PySide6 # Linux 存储管理工具 - Tkinter 版本
# 核心依赖Python 内置):
# - tkinter (GUI)
# - threading (后台任务)
# - queue (线程通信)
# 可选依赖:
# pexpect - 用于交互式命令(如 RAID 创建时的密码输入)
# 安装: pip3 install pexpect
# CentOS 8 安装命令:
# sudo yum install python3 python3-tkinter parted mdadm lvm2
# sudo pip3 install pexpect
# Ubuntu/Debian 安装命令:
# sudo apt-get install python3 python3-tk parted mdadm lvm2
# sudo pip3 install pexpect

41
run_tkinter.sh Normal file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
# Linux 存储管理工具 - Tkinter 版本启动脚本
# 适配低版本系统(如 CentOS 8
# 检查 Python 版本
python3 --version > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "错误: 未找到 Python3请安装 Python 3.6 或更高版本。"
exit 1
fi
# 检查 tkinter
python3 -c "import tkinter" > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "错误: Python 未安装 tkinter 模块。"
echo "请安装 tkinter:"
echo " CentOS/RHEL: sudo yum install python3-tkinter"
echo " Ubuntu/Debian: sudo apt-get install python3-tk"
exit 1
fi
# 检查必要的系统工具
for tool in lsblk parted mdadm lvm; do
which $tool > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "警告: 未找到工具 '$tool',某些功能可能无法使用。"
fi
done
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# 以 root 权限运行(存储管理需要)
if [ "$EUID" -ne 0 ]; then
echo "注意: 存储管理工具需要 root 权限才能正常工作。"
echo "正在尝试使用 sudo 启动..."
sudo python3 "${SCRIPT_DIR}/main_tkinter.py" "$@"
else
python3 "${SCRIPT_DIR}/main_tkinter.py" "$@"
fi

106
verify_tkinter.py Normal file
View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""
验证 Tkinter 版本的存储管理工具是否可以正确导入
"""
import sys
import os
def check_python_version():
"""检查 Python 版本"""
print(f"Python 版本: {sys.version}")
if sys.version_info < (3, 6):
print("错误: 需要 Python 3.6 或更高版本")
return False
return True
def check_tkinter():
"""检查 tkinter 是否可用"""
try:
import tkinter as tk
print(f"Tkinter 版本: {tk.Tcl().eval('info patch')}")
return True
except ImportError:
print("错误: 未找到 tkinter 模块")
print("请安装 tkinter:")
print(" CentOS/RHEL: sudo yum install python3-tkinter")
print(" Ubuntu/Debian: sudo apt-get install python3-tk")
return False
def check_modules():
"""检查自定义模块"""
modules_to_check = [
('logger_config_tkinter', '日志配置'),
('dialogs_tkinter', '对话框'),
('disk_operations_tkinter', '磁盘操作'),
('raid_operations_tkinter', 'RAID 操作'),
('lvm_operations_tkinter', 'LVM 操作'),
('occupation_resolver_tkinter', '设备占用解除'),
('mainwindow_tkinter', '主窗口'),
]
all_ok = True
for module_name, desc in modules_to_check:
try:
__import__(module_name)
print(f"{desc}模块 ({module_name}) 加载成功")
except Exception as e:
print(f"{desc}模块 ({module_name}) 加载失败: {e}")
all_ok = False
return all_ok
def check_system_modules():
"""检查系统信息模块"""
try:
from system_info import SystemInfoManager
print("✓ 系统信息模块 (system_info) 加载成功")
return True
except Exception as e:
print(f"✗ 系统信息模块 (system_info) 加载失败: {e}")
return False
def main():
"""主函数"""
print("=" * 60)
print("Linux 存储管理工具 - Tkinter 版本验证")
print("=" * 60)
print()
checks = [
("Python 版本", check_python_version),
("Tkinter 模块", check_tkinter),
("系统模块", check_system_modules),
("自定义模块", check_modules),
]
results = []
for name, check_func in checks:
print(f"\n检查 {name}...")
print("-" * 40)
result = check_func()
results.append((name, result))
print()
print("=" * 60)
print("验证结果汇总")
print("=" * 60)
all_passed = True
for name, result in results:
status = "通过" if result else "失败"
symbol = "" if result else ""
print(f"{symbol} {name}: {status}")
if not result:
all_passed = False
print()
if all_passed:
print("✓ 所有检查通过!可以运行 main_tkinter.py 启动程序。")
return 0
else:
print("✗ 部分检查失败,请根据上述提示修复问题。")
return 1
if __name__ == "__main__":
sys.exit(main())