Files
diskmanager2/mainwindow_tkinter.py
2026-02-09 17:59:50 +08:00

1311 lines
60 KiB
Python

# mainwindow_tkinter.py
import sys
import logging
import re
import os
import tkinter as tk
from tkinter import ttk, messagebox, Menu
from tkinter.scrolledtext import ScrolledText
# 导入系统信息管理模块
from system_info import SystemInfoManager
# 导入日志配置
from logger_config_tkinter import setup_logging, logger
# 导入磁盘操作模块
from disk_operations_tkinter import DiskOperations
# 导入 RAID 操作模块
from raid_operations_tkinter import RaidOperations
# 导入 LVM 操作模块
from lvm_operations_tkinter import LvmOperations
# 导入新的 resolver 类
from occupation_resolver_tkinter import OccupationResolver
# 导入自定义对话框
from dialogs_tkinter import (CreatePartitionDialog, MountDialog, CreateRaidDialog,
CreatePvDialog, CreateVgDialog, CreateLvDialog)
# 导入 SMART 对话框
from dialogs_smart import SmartInfoDialog, SmartOverviewDialog
from smart_monitor import SmartMonitor
# 导入增强对话框
from dialogs_enhanced import EnhancedFormatDialog, EnhancedRaidDeleteDialog, EnhancedPartitionDialog
class MainWindow:
"""主窗口类 - Tkinter 版本"""
def __init__(self, root):
self.root = root
self.root.title("Linux 存储管理工具")
self.root.geometry("1200x800")
# 创建主框架
self.main_frame = ttk.Frame(self.root, padding="5")
self.main_frame.pack(fill=tk.BOTH, expand=True)
# 初始化管理器和操作类
self.system_manager = SystemInfoManager()
self.lvm_ops = LvmOperations()
self.disk_ops = DiskOperations(self.system_manager, self.lvm_ops)
self.raid_ops = RaidOperations(self.system_manager)
# 实例化 OccupationResolver
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._current_context_menu = None
self._menu_close_binding = None
# 创建界面
self._create_widgets()
# 设置日志
setup_logging(self.log_text)
logger.info("应用程序启动。")
# 连接磁盘操作的回调
self.disk_ops.set_formatting_callback(self.on_disk_formatting_finished)
# 启动队列检查
self.disk_ops.start_queue_check(self.root)
# 初始化时刷新所有数据
self.refresh_all_info()
logger.info("所有设备信息已初始化加载。")
def _create_widgets(self):
"""创建界面元素"""
# 顶部按钮栏
btn_frame = ttk.Frame(self.main_frame)
btn_frame.pack(fill=tk.X, pady=(0, 5))
# 刷新按钮
self.refresh_btn = ttk.Button(btn_frame, text="刷新数据", command=self.refresh_all_info)
self.refresh_btn.pack(side=tk.LEFT, padx=(0, 5))
# SMART 监控按钮
self.smart_btn = ttk.Button(btn_frame, text="SMART 监控", command=self._show_smart_overview)
self.smart_btn.pack(side=tk.LEFT, padx=(0, 5))
# 菜单栏
self._create_menu()
# Notebook (Tab 控件)
self.notebook = ttk.Notebook(self.main_frame)
self.notebook.pack(fill=tk.BOTH, expand=True)
# === 块设备 Tab ===
self.tab_block = ttk.Frame(self.notebook)
self.notebook.add(self.tab_block, text="块设备概览")
# 块设备 Treeview
self._create_block_devices_tree()
# === RAID 管理 Tab ===
self.tab_raid = ttk.Frame(self.notebook)
self.notebook.add(self.tab_raid, text="RAID 管理")
# RAID Treeview
self._create_raid_tree()
# === LVM 管理 Tab ===
self.tab_lvm = ttk.Frame(self.notebook)
self.notebook.add(self.tab_lvm, text="LVM 管理")
# LVM Treeview
self._create_lvm_tree()
# 日志区域
log_frame = ttk.LabelFrame(self.main_frame, text="日志输出", padding="5")
log_frame.pack(fill=tk.X, pady=(5, 0))
self.log_text = ScrolledText(log_frame, height=8, state=tk.DISABLED)
self.log_text.pack(fill=tk.BOTH, expand=True)
def _create_menu(self):
"""创建菜单栏"""
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
# 工具菜单
tools_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="工具", menu=tools_menu)
tools_menu.add_command(label="SMART 监控", command=self._show_smart_overview)
tools_menu.add_separator()
tools_menu.add_command(label="刷新数据", command=self.refresh_all_info)
# 帮助菜单
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="帮助", menu=help_menu)
help_menu.add_command(label="关于", command=self._show_about)
def _show_about(self):
"""显示关于对话框"""
messagebox.showinfo(
"关于",
"Linux 存储管理工具\n\n"
"版本: 2.0 (Tkinter 版)\n"
"适配低版本系统 (CentOS 8+)\n\n"
"功能:\n"
"- 块设备管理\n"
"- RAID 管理\n"
"- LVM 管理\n"
"- SMART 监控\n\n"
"使用 Python 3.6+ 和 Tkinter 构建"
)
def _create_block_devices_tree(self):
"""创建块设备 Treeview"""
# 创建框架
tree_frame = ttk.Frame(self.tab_block)
tree_frame.pack(fill=tk.BOTH, expand=True)
# 定义列
self.block_columns = [
("设备名", 'name'), ("类型", 'type'), ("大小", 'size'), ("挂载点", 'mountpoint'),
("文件系统", 'fstype'), ("只读", 'ro'), ("UUID", 'uuid'), ("PARTUUID", 'partuuid'),
("厂商", 'vendor'), ("型号", 'model'), ("序列号", 'serial'),
("主次号", 'maj:min'), ("父设备名", 'pkname'),
]
# 创建 Treeview
self.tree_block = ttk.Treeview(tree_frame, columns=[c[1] for c in self.block_columns],
show='tree headings')
# 设置表头
self.tree_block.heading('#0', text='设备结构')
self.tree_block.column('#0', width=150)
for col_name, col_id in self.block_columns:
self.tree_block.heading(col_id, text=col_name)
self.tree_block.column(col_id, width=80, anchor=tk.W)
# 滚动条
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree_block.yview)
hsb = ttk.Scrollbar(tree_frame, orient=tk.HORIZONTAL, command=self.tree_block.xview)
self.tree_block.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
# 布局
self.tree_block.grid(row=0, column=0, sticky='nsew')
vsb.grid(row=0, column=1, sticky='ns')
hsb.grid(row=1, column=0, sticky='ew')
tree_frame.grid_rowconfigure(0, weight=1)
tree_frame.grid_columnconfigure(0, weight=1)
# 右键菜单绑定
self.tree_block.bind("<Button-3>", self._on_block_device_right_click)
def _create_raid_tree(self):
"""创建 RAID Treeview"""
tree_frame = ttk.Frame(self.tab_raid)
tree_frame.pack(fill=tk.BOTH, expand=True)
# 定义列
self.raid_columns = [
("阵列设备", 'device'), ("级别", 'level'), ("状态", 'state'), ("大小", 'array_size'),
("活动设备", 'active_devices'), ("失败设备", 'failed_devices'),
("备用设备", 'spare_devices'), ("总设备数", 'total_devices'),
("UUID", 'uuid'), ("名称", 'name'), ("Chunk Size", 'chunk_size'), ("挂载点", 'mountpoint')
]
# 创建 Treeview
self.tree_raid = ttk.Treeview(tree_frame, columns=[c[1] for c in self.raid_columns],
show='tree headings')
# 设置表头
self.tree_raid.heading('#0', text='RAID 结构')
self.tree_raid.column('#0', width=150)
for col_name, col_id in self.raid_columns:
self.tree_raid.heading(col_id, text=col_name)
self.tree_raid.column(col_id, width=80, anchor=tk.W)
# 滚动条
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree_raid.yview)
hsb = ttk.Scrollbar(tree_frame, orient=tk.HORIZONTAL, command=self.tree_raid.xview)
self.tree_raid.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
# 布局
self.tree_raid.grid(row=0, column=0, sticky='nsew')
vsb.grid(row=0, column=1, sticky='ns')
hsb.grid(row=1, column=0, sticky='ew')
tree_frame.grid_rowconfigure(0, weight=1)
tree_frame.grid_columnconfigure(0, weight=1)
# 右键菜单绑定
self.tree_raid.bind("<Button-3>", self._on_raid_right_click)
def _create_lvm_tree(self):
"""创建 LVM Treeview"""
tree_frame = ttk.Frame(self.tab_lvm)
tree_frame.pack(fill=tk.BOTH, expand=True)
# 定义列
self.lvm_columns = [
("名称", 'name'), ("大小", 'size'), ("属性", 'attr'), ("UUID", 'uuid'),
("关联", 'relation'), ("空闲/已用", 'usage'), ("路径/格式", 'path_fmt'), ("挂载点", 'mountpoint')
]
# 创建 Treeview
self.tree_lvm = ttk.Treeview(tree_frame, columns=[c[1] for c in self.lvm_columns],
show='tree headings')
# 设置表头
self.tree_lvm.heading('#0', text='LVM 结构')
self.tree_lvm.column('#0', width=150)
for col_name, col_id in self.lvm_columns:
self.tree_lvm.heading(col_id, text=col_name)
self.tree_lvm.column(col_id, width=80, anchor=tk.W)
# 滚动条
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree_lvm.yview)
hsb = ttk.Scrollbar(tree_frame, orient=tk.HORIZONTAL, command=self.tree_lvm.xview)
self.tree_lvm.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
# 布局
self.tree_lvm.grid(row=0, column=0, sticky='nsew')
vsb.grid(row=0, column=1, sticky='ns')
hsb.grid(row=1, column=0, sticky='ew')
tree_frame.grid_rowconfigure(0, weight=1)
tree_frame.grid_columnconfigure(0, weight=1)
# 右键菜单绑定
self.tree_lvm.bind("<Button-3>", self._on_lvm_right_click)
def refresh_all_info(self):
"""刷新所有设备信息"""
logger.info("开始刷新所有设备信息...")
self.refresh_block_devices_info()
self.refresh_raid_info()
self.refresh_lvm_info()
logger.info("所有设备信息刷新完成。")
def refresh_block_devices_info(self):
"""刷新块设备信息"""
# 清空树
for item in self.tree_block.get_children():
self.tree_block.delete(item)
try:
devices = self.system_manager.get_block_devices()
for dev in devices:
self._add_device_to_tree(self.tree_block, '', dev)
logger.info("块设备信息刷新成功。")
except Exception as e:
messagebox.showerror("错误", f"刷新块设备信息失败: {e}")
logger.error(f"刷新块设备信息失败: {e}")
def _add_device_to_tree(self, tree, parent, dev_data):
"""添加设备到树"""
# 获取显示值
values = []
for col_name, col_id in self.block_columns:
value = dev_data.get(col_id)
if col_id == 'ro':
value = "" if value else ""
elif value is None:
value = ""
values.append(value)
# 插入节点
item_id = tree.insert(parent, tk.END, text=dev_data.get('name', ''), values=values)
# 保存完整数据
tree.item(item_id, tags=(str(dev_data),))
# 递归添加子设备
if 'children' in dev_data:
for child in dev_data['children']:
self._add_device_to_tree(tree, item_id, child)
tree.item(item_id, open=True)
def _get_item_data(self, tree, item_id):
"""获取树项关联的数据"""
tags = tree.item(item_id, 'tags')
if tags:
try:
# 解析字符串为字典
return eval(tags[0])
except:
pass
return None
def _close_context_menu(self):
"""关闭当前右键菜单"""
if self._current_context_menu is not None:
try:
self._current_context_menu.unpost()
except:
pass
self._current_context_menu = None
# 解绑全局点击事件
if self._menu_close_binding is not None:
try:
self.root.unbind('<Button-1>', self._menu_close_binding)
self._menu_close_binding = None
except:
pass
def _bind_menu_close(self):
"""绑定点击事件来关闭菜单"""
if self._current_context_menu is not None:
self._menu_close_binding = self.root.bind('<Button-1>', self._on_click_outside_menu)
def _on_click_outside_menu(self, event):
"""点击菜单外部时关闭菜单"""
# tk.Menu 会拦截自己的点击事件,所以如果这里被调用,说明点击在菜单外部
self._close_context_menu()
def _show_context_menu(self, menu, x, y):
"""显示右键菜单并记录引用"""
# 先关闭旧菜单
self._close_context_menu()
# 记录新菜单
self._current_context_menu = menu
# 显示菜单
menu.post(x, y)
# 绑定全局点击事件来关闭菜单(延迟绑定避免立即触发)
self.root.after(100, self._bind_menu_close)
# === 块设备右键菜单 ===
def _on_block_device_right_click(self, event):
"""块设备右键点击事件"""
# 先关闭任何已打开的菜单
self._close_context_menu()
item = self.tree_block.identify_row(event.y)
if item:
self.tree_block.selection_set(item)
else:
return
# 创建菜单
menu = tk.Menu(self.root, tearoff=0)
# 创建子菜单
create_menu = tk.Menu(menu, tearoff=0)
create_menu.add_command(label="创建 RAID 阵列...", command=self._handle_create_raid_array)
create_menu.add_command(label="创建物理卷 (PV)...", command=self._handle_create_pv)
menu.add_cascade(label="创建...", menu=create_menu)
menu.add_separator()
if item:
dev_data = self._get_item_data(self.tree_block, item)
if dev_data:
device_name = dev_data.get('name')
device_type = dev_data.get('type')
mount_point = dev_data.get('mountpoint')
device_path = dev_data.get('path', f"/dev/{device_name}")
# 磁盘操作
if device_type == 'disk':
menu.add_command(label=f"创建分区 {device_path}...",
command=lambda: self._handle_create_partition(device_path, dev_data))
menu.add_command(label=f"擦除分区表 {device_path}...",
command=lambda: self._handle_wipe_partition_table(device_path))
menu.add_separator()
# SMART 信息
menu.add_command(label=f"查看 SMART 信息 {device_path}",
command=lambda: self._handle_show_smart(device_path))
menu.add_separator()
# 分区操作
if device_type == 'part':
if not mount_point or mount_point == '' or mount_point == 'N/A':
menu.add_command(label=f"挂载 {device_path}...",
command=lambda: self._handle_mount(device_path))
elif mount_point != '[SWAP]':
menu.add_command(label=f"卸载 {device_path}",
command=lambda: self._unmount_and_refresh(device_path))
menu.add_separator()
menu.add_command(label=f"删除分区 {device_path}",
command=lambda: self._handle_delete_partition(device_path))
menu.add_command(label=f"格式化分区 {device_path}...",
command=lambda dp=device_path, dd=dev_data: self._handle_format_partition(dp, dd))
# 文件系统检查和修复
fstype = dev_data.get('fstype', '')
if fstype:
fs_menu = tk.Menu(menu, tearoff=0)
# 根据文件系统类型添加不同的检查/修复选项
if fstype.startswith('ext'):
fs_menu.add_command(label=f"检查文件系统 (fsck) {device_path}",
command=lambda dp=device_path, ft=fstype: self._handle_fsck(dp, ft))
fs_menu.add_command(label=f"调整文件系统大小 (resize2fs) {device_path}",
command=lambda dp=device_path, ft=fstype: self._handle_resize2fs(dp, ft))
elif fstype == 'xfs':
fs_menu.add_command(label=f"修复文件系统 (xfs_repair) {device_path}",
command=lambda dp=device_path: self._handle_xfs_repair(dp))
if fs_menu.index(tk.END) is not None:
menu.add_cascade(label=f"文件系统工具 {device_path}", menu=fs_menu)
menu.add_separator()
# Loop 设备操作
if device_type == 'loop':
if not mount_point or mount_point == '' or mount_point == 'N/A':
menu.add_command(label=f"挂载 {device_path}...",
command=lambda: self._handle_mount(device_path))
else:
menu.add_command(label=f"卸载 {device_path}",
command=lambda: self._unmount_and_refresh(device_path))
menu.add_separator()
# 解除占用
if device_type in ['disk', 'part', 'loop']:
menu.add_command(label=f"解除占用 {device_path}",
command=lambda: self._handle_resolve_device_occupation(device_path))
# 关闭交换分区
if device_type == 'part' and mount_point == '[SWAP]':
menu.add_command(label=f"关闭交换分区 {device_path}",
command=lambda: self._handle_deactivate_swap(device_path))
# ROM 设备操作
if device_type == 'rom':
menu.add_separator()
if not mount_point or mount_point == '' or mount_point == 'N/A':
menu.add_command(label=f"挂载为 Loop {device_path}...",
command=lambda: self._handle_mount_loop(device_path))
else:
menu.add_command(label=f"卸载 Loop {device_path}",
command=lambda: self._handle_unmount_loop(device_path))
if menu.index(tk.END) is not None:
self._show_context_menu(menu, event.x_root, event.y_root)
else:
menu.destroy()
logger.info("右键点击了空白区域或设备没有可用的操作。")
# === RAID 右键菜单 ===
def _on_raid_right_click(self, event):
"""RAID 右键点击事件"""
# 先关闭任何已打开的菜单
self._close_context_menu()
item = self.tree_raid.identify_row(event.y)
if item:
self.tree_raid.selection_set(item)
menu = tk.Menu(self.root, tearoff=0)
menu.add_command(label="创建 RAID 阵列...", command=self._handle_create_raid_array)
menu.add_separator()
if item:
parent = self.tree_raid.parent(item)
if not parent: # 顶层项
array_data = self._get_item_data(self.tree_raid, item)
if array_data:
array_path = array_data.get('device')
array_state = array_data.get('state', 'N/A')
member_devices = [m.get('device_path') for m in array_data.get('member_devices', [])]
array_uuid = array_data.get('uuid')
if array_state == 'Stopped (Configured)':
menu.add_command(label=f"激活阵列 {array_path}",
command=lambda: self._handle_activate_raid_array(array_path))
menu.add_command(label=f"删除配置文件条目 (UUID: {array_uuid})",
command=lambda: self._handle_delete_configured_raid_array(array_uuid))
else:
current_mount_point = self.system_manager.get_mountpoint_for_device(array_path)
if current_mount_point and current_mount_point != '[SWAP]' and current_mount_point != '':
menu.add_command(label=f"卸载 {array_path} ({current_mount_point})",
command=lambda: self._unmount_and_refresh(array_path))
else:
menu.add_command(label=f"挂载 {array_path}...",
command=lambda: self._handle_mount(array_path))
menu.add_separator()
menu.add_command(label=f"解除占用 {array_path}",
command=lambda: self._handle_resolve_device_occupation(array_path))
menu.add_separator()
menu.add_command(label=f"停止阵列 {array_path}",
command=lambda: self._handle_stop_raid_array(array_path))
menu.add_command(label=f"删除阵列 {array_path}",
command=lambda ap=array_path, md=member_devices, au=array_uuid, ad=array_data:
self._handle_delete_active_raid_array(ap, md, au))
menu.add_command(label=f"格式化阵列 {array_path}...",
command=lambda: self._handle_format_raid_array(array_path))
# 文件系统检查和修复
dev_details = self.system_manager.get_device_details_by_path(array_path)
fstype = dev_details.get('fstype', '') if dev_details else ''
if fstype:
fs_menu = tk.Menu(menu, tearoff=0)
if fstype.startswith('ext'):
fs_menu.add_command(label=f"检查文件系统 (fsck) {array_path}",
command=lambda dp=array_path, ft=fstype: self._handle_fsck(dp, ft))
fs_menu.add_command(label=f"调整文件系统大小 (resize2fs) {array_path}",
command=lambda dp=array_path, ft=fstype: self._handle_resize2fs(dp, ft))
elif fstype == 'xfs':
fs_menu.add_command(label=f"修复文件系统 (xfs_repair) {array_path}",
command=lambda dp=array_path: self._handle_xfs_repair(dp))
if fs_menu.index(tk.END) is not None:
menu.add_cascade(label=f"文件系统工具 {array_path}", menu=fs_menu)
if menu.index(tk.END) is not None:
self._show_context_menu(menu, event.x_root, event.y_root)
else:
menu.destroy()
# === LVM 右键菜单 ===
def _on_lvm_right_click(self, event):
"""LVM 右键点击事件"""
# 先关闭任何已打开的菜单
self._close_context_menu()
item = self.tree_lvm.identify_row(event.y)
if item:
self.tree_lvm.selection_set(item)
menu = tk.Menu(self.root, tearoff=0)
create_menu = tk.Menu(menu, tearoff=0)
create_menu.add_command(label="创建物理卷 (PV)...", command=self._handle_create_pv)
create_menu.add_command(label="创建卷组 (VG)...", command=self._handle_create_vg)
create_menu.add_command(label="创建逻辑卷 (LV)...", command=self._handle_create_lv)
menu.add_cascade(label="创建...", menu=create_menu)
menu.add_separator()
if item:
item_data = self._get_item_data(self.tree_lvm, item)
if item_data:
item_type = item_data.get('type')
data = item_data.get('data', {})
if item_type == 'pv':
pv_name = data.get('pv_name')
if pv_name and pv_name != 'N/A':
menu.add_command(label=f"删除物理卷 {pv_name}",
command=lambda: self._handle_delete_pv(pv_name))
elif item_type == 'vg':
vg_name = data.get('vg_name')
if vg_name and vg_name != 'N/A':
menu.add_command(label=f"删除卷组 {vg_name}",
command=lambda: self._handle_delete_vg(vg_name))
elif item_type == 'lv':
lv_name = data.get('lv_name')
vg_name = data.get('vg_name')
lv_attr = data.get('lv_attr', '')
lv_path = data.get('lv_path')
if lv_name and vg_name and lv_path and lv_path != 'N/A':
if 'a' in lv_attr:
menu.add_command(label=f"停用逻辑卷 {lv_name}",
command=lambda: self._handle_deactivate_lv(lv_name, vg_name))
else:
menu.add_command(label=f"激活逻辑卷 {lv_name}",
command=lambda: self._handle_activate_lv(lv_name, vg_name))
current_mount_point = self.system_manager.get_mountpoint_for_device(lv_path)
if current_mount_point and current_mount_point != '[SWAP]' and current_mount_point != '':
menu.add_command(label=f"卸载 {lv_name} ({current_mount_point})",
command=lambda: self._unmount_and_refresh(lv_path))
else:
menu.add_command(label=f"挂载 {lv_name}...",
command=lambda: self._handle_mount(lv_path))
menu.add_separator()
menu.add_command(label=f"解除占用 {lv_path}",
command=lambda: self._handle_resolve_device_occupation(lv_path))
menu.add_separator()
menu.add_command(label=f"删除逻辑卷 {lv_name}",
command=lambda: self._handle_delete_lv(lv_name, vg_name))
menu.add_command(label=f"格式化逻辑卷 {lv_name}...",
command=lambda lp=lv_path, ln=lv_name: self._handle_format_partition(
lp, {'name': ln, 'path': lp}))
# 文件系统检查和修复
dev_details = self.system_manager.get_device_details_by_path(lv_path)
fstype = dev_details.get('fstype', '') if dev_details else ''
if fstype:
fs_menu = tk.Menu(menu, tearoff=0)
if fstype.startswith('ext'):
fs_menu.add_command(label=f"检查文件系统 (fsck) {lv_path}",
command=lambda dp=lv_path, ft=fstype: self._handle_fsck(dp, ft))
fs_menu.add_command(label=f"调整文件系统大小 (resize2fs) {lv_path}",
command=lambda dp=lv_path, ft=fstype: self._handle_resize2fs(dp, ft))
elif fstype == 'xfs':
fs_menu.add_command(label=f"修复文件系统 (xfs_repair) {lv_path}",
command=lambda dp=lv_path: self._handle_xfs_repair(dp))
if fs_menu.index(tk.END) is not None:
menu.add_cascade(label=f"文件系统工具 {lv_path}", menu=fs_menu)
if menu.index(tk.END) is not None:
self._show_context_menu(menu, event.x_root, event.y_root)
else:
menu.destroy()
# === 块设备处理函数 ===
def _handle_resolve_device_occupation(self, device_path: str) -> bool:
"""处理设备占用问题"""
success = self.occupation_resolver.resolve_occupation(device_path)
if success:
self.refresh_all_info()
return success
def _handle_deactivate_swap(self, device_path):
"""处理关闭交换分区"""
if messagebox.askyesno("确认关闭交换分区",
f"您确定要关闭交换分区 {device_path} 吗?",
default=messagebox.NO):
logger.info(f"尝试关闭交换分区 {device_path}")
success, _, stderr = self.lvm_ops._execute_shell_command(
["swapoff", device_path],
f"关闭交换分区 {device_path} 失败",
show_dialog=True
)
if success:
messagebox.showinfo("成功", f"交换分区 {device_path} 已成功关闭。")
self.refresh_all_info()
return True
else:
logger.error(f"关闭交换分区 {device_path} 失败: {stderr}")
else:
logger.info(f"用户取消了关闭交换分区 {device_path}")
return False
def _handle_mount_loop(self, device_path):
"""处理挂载 ROM 设备为 loop"""
if self.disk_ops.mount_loop_device(device_path):
self.refresh_all_info()
def _handle_unmount_loop(self, device_path):
"""处理卸载 loop 设备"""
if self.disk_ops.unmount_loop_device(device_path):
self.refresh_all_info()
def _handle_show_smart(self, device_path):
"""显示指定设备的 SMART 信息"""
monitor = SmartMonitor()
if not monitor.is_available():
messagebox.showwarning("警告",
"smartctl 工具未安装。\n"
"请安装 smartmontools:\n"
" CentOS/RHEL: sudo yum install smartmontools\n"
" Ubuntu/Debian: sudo apt-get install smartmontools")
return
smart_info = monitor.get_disk_smart_info(device_path)
if smart_info:
SmartInfoDialog(self.root, device_path, smart_info)
else:
messagebox.showerror("错误",
f"无法获取 {device_path} 的 SMART 信息。\n"
f"可能的原因:\n"
f"1. 设备不支持 SMART\n"
f"2. 需要 root 权限\n"
f"3. 设备未正确连接")
def _show_smart_overview(self):
"""显示 SMART 总览"""
monitor = SmartMonitor()
if not monitor.is_available():
messagebox.showwarning("警告",
"smartctl 工具未安装。\n"
"请安装 smartmontools:\n"
" CentOS/RHEL: sudo yum install smartmontools\n"
" Ubuntu/Debian: sudo apt-get install smartmontools")
return
SmartOverviewDialog(self.root)
def _handle_wipe_partition_table(self, device_path):
"""处理擦除物理盘分区表"""
if messagebox.askyesno("确认擦除分区表",
f"您确定要擦除设备 {device_path} 上的所有分区表吗?\n"
f"**此操作将永久删除设备上的所有分区和数据,且不可恢复!**\n"
f"请谨慎操作!",
default=messagebox.NO):
logger.warning(f"尝试擦除 {device_path} 上的分区表。")
success, _, stderr = self.lvm_ops._execute_shell_command(
["parted", "-s", device_path, "mklabel", "gpt"],
f"擦除 {device_path} 上的分区表失败"
)
if success:
messagebox.showinfo("成功", f"设备 {device_path} 上的分区表已成功擦除。")
self.refresh_all_info()
def _handle_create_partition(self, disk_path, dev_data):
"""处理创建分区"""
total_disk_mib = 0.0
total_size_str = dev_data.get('size')
logger.debug(f"尝试为磁盘 {disk_path} 创建分区。原始大小字符串: '{total_size_str}'")
if total_size_str:
match = re.match(r'(\d+(\.\d+)?)\s*([KMGT]?B?)', total_size_str, re.IGNORECASE)
if match:
value = float(match.group(1))
unit = match.group(3).upper() if match.group(3) else ''
if unit == 'KB' or unit == 'K':
total_disk_mib = value / 1024
elif unit == 'MB' or unit == 'M':
total_disk_mib = value
elif unit == 'GB' or unit == 'G':
total_disk_mib = value * 1024
elif unit == 'TB' or unit == 'T':
total_disk_mib = value * 1024 * 1024
elif unit == 'B':
total_disk_mib = value / (1024 * 1024)
else:
logger.warning(f"无法识别磁盘 {disk_path} 的大小单位: '{unit}'")
total_disk_mib = 0.0
logger.debug(f"解析后的磁盘总大小 (MiB): {total_disk_mib}")
else:
logger.warning(f"无法解析磁盘 {disk_path} 的大小字符串 '{total_size_str}'")
total_disk_mib = 0.0
else:
logger.warning(f"获取磁盘 {disk_path} 的大小字符串为空或None。")
total_disk_mib = 0.0
if total_disk_mib <= 0.0:
messagebox.showerror("错误", f"无法获取磁盘 {disk_path} 的有效总大小。")
return
start_position_mib = 0.0
max_available_mib = total_disk_mib
if dev_data.get('children'):
logger.debug(f"磁盘 {disk_path} 存在现有分区,尝试计算下一个分区起始位置。")
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:
messagebox.showerror("错误", f"无法确定磁盘 {disk_path} 的分区起始位置。")
return
start_position_mib = calculated_start_mib
max_available_mib = largest_free_space_mib
if max_available_mib < 0:
max_available_mib = 0.0
else:
logger.debug(f"磁盘 {disk_path} 没有现有分区。")
max_available_mib = max(0.0, total_disk_mib - 1.0)
start_position_mib = 1.0
# 使用增强型分区创建对话框(带滑块可视化)
dialog = EnhancedPartitionDialog(self.root, disk_path, total_disk_mib, max_available_mib)
info = dialog.wait_for_result()
if info:
if self.disk_ops.create_partition(
info['disk_path'],
info['partition_table_type'],
info['size_gb'],
info['total_disk_mib'],
info['use_max_space']
):
self.refresh_all_info()
def _handle_mount(self, device_path):
"""处理挂载"""
dialog = MountDialog(self.root, device_path)
info = dialog.wait_for_result()
if info:
if self.disk_ops.mount_partition(device_path, info['mount_point'], info['add_to_fstab']):
self.refresh_all_info()
def _unmount_and_refresh(self, device_path):
"""处理卸载并刷新"""
if self.disk_ops.unmount_partition(device_path, show_dialog_on_error=True):
self.refresh_all_info()
def _handle_delete_partition(self, device_path):
"""处理删除分区"""
if self.disk_ops.delete_partition(device_path):
self.refresh_all_info()
def _handle_format_partition(self, device_path, dev_data=None):
"""处理格式化分区 - 使用增强型对话框"""
# 获取设备信息
if dev_data is None:
dev_data = self.system_manager.get_device_details_by_path(device_path)
# 使用增强型格式化对话框
dialog = EnhancedFormatDialog(self.root, device_path, dev_data or {})
fs_type = dialog.wait_for_result()
if fs_type:
# 执行格式化
self.disk_ops.format_partition(device_path, fs_type)
def _handle_fsck(self, device_path, fstype):
"""检查/修复 ext 文件系统"""
# 检查设备是否已挂载
mount_point = self.system_manager.get_mountpoint_for_device(device_path)
if mount_point and mount_point != 'N/A':
messagebox.showwarning("警告", f"设备 {device_path} 已挂载到 {mount_point}\n请先卸载后再执行文件系统检查。")
return
if not messagebox.askyesno("确认",
f"即将对 {device_path} ({fstype}) 执行文件系统检查。\n"
f"命令: fsck -f -y {device_path}\n\n"
f"注意:此操作可能需要较长时间,请勿中断。",
default=messagebox.NO):
return
logger.info(f"开始检查文件系统: {device_path} ({fstype})")
success, stdout, stderr = self.lvm_ops._execute_shell_command(
["fsck", "-f", "-y", device_path],
f"检查文件系统 {device_path} 失败"
)
if success:
messagebox.showinfo("成功", f"文件系统检查完成:\n{stdout[:500] if stdout else '无输出'}")
else:
messagebox.showerror("错误", f"文件系统检查失败:\n{stderr[:500] if stderr else '未知错误'}")
self.refresh_all_info()
def _handle_xfs_repair(self, device_path):
"""修复 XFS 文件系统"""
# 检查设备是否已挂载
mount_point = self.system_manager.get_mountpoint_for_device(device_path)
if mount_point and mount_point != 'N/A':
messagebox.showwarning("警告", f"设备 {device_path} 已挂载到 {mount_point}\n请先卸载后再执行文件系统修复。")
return
if not messagebox.askyesno("确认",
f"即将对 {device_path} (XFS) 执行文件系统修复。\n"
f"命令: xfs_repair {device_path}\n\n"
f"警告:此操作会尝试修复文件系统错误,但可能无法恢复所有数据。\n"
f"建议先备份重要数据。",
default=messagebox.NO):
return
logger.info(f"开始修复 XFS 文件系统: {device_path}")
success, stdout, stderr = self.lvm_ops._execute_shell_command(
["xfs_repair", device_path],
f"修复 XFS 文件系统 {device_path} 失败"
)
if success:
messagebox.showinfo("成功", f"XFS 文件系统修复完成:\n{stdout[:500] if stdout else '无输出'}")
else:
messagebox.showerror("错误", f"XFS 文件系统修复失败:\n{stderr[:500] if stderr else '未知错误'}")
self.refresh_all_info()
def _handle_resize2fs(self, device_path, fstype):
"""调整 ext 文件系统大小"""
# 检查设备是否已挂载
mount_point = self.system_manager.get_mountpoint_for_device(device_path)
if mount_point and mount_point != 'N/A':
messagebox.showwarning("警告", f"设备 {device_path} 已挂载到 {mount_point}\n请先卸载后再执行调整大小操作。")
return
if not messagebox.askyesno("确认",
f"即将对 {device_path} ({fstype}) 强制调整文件系统大小以匹配分区大小。\n"
f"命令: resize2fs -f {device_path}\n\n"
f"注意:此操作会强制调整文件系统大小,请确保分区大小已正确设置。",
default=messagebox.NO):
return
logger.info(f"开始调整文件系统大小: {device_path} ({fstype})")
success, stdout, stderr = self.lvm_ops._execute_shell_command(
["resize2fs", "-f", device_path],
f"调整文件系统大小 {device_path} 失败"
)
if success:
messagebox.showinfo("成功", f"文件系统大小调整完成:\n{stdout[:500] if stdout else '无输出'}")
else:
messagebox.showerror("错误", f"调整文件系统大小失败:\n{stderr[:500] if stderr else '未知错误'}")
self.refresh_all_info()
def on_disk_formatting_finished(self, success, device_path, stdout, stderr):
"""接收格式化完成回调并刷新界面"""
logger.info(f"格式化完成信号接收: 设备 {device_path}, 成功: {success}")
self.refresh_all_info()
# === RAID 处理函数 ===
def refresh_raid_info(self):
"""刷新 RAID 信息"""
for item in self.tree_raid.get_children():
self.tree_raid.delete(item)
try:
raid_arrays = self.system_manager.get_mdadm_arrays()
if not raid_arrays:
self.tree_raid.insert('', tk.END, text='未找到RAID阵列。', values=[''] * len(self.raid_columns))
logger.info("未找到RAID阵列。")
return
for array in raid_arrays:
array_path = array.get('device', 'N/A')
if not array_path or array_path == 'N/A':
continue
current_mount_point = ""
if array.get('state') != 'Stopped (Configured)':
current_mount_point = self.system_manager.get_mountpoint_for_device(array_path)
values = [
array_path,
array.get('level', 'N/A'),
array.get('state', 'N/A'),
array.get('array_size', 'N/A'),
array.get('active_devices', 'N/A'),
array.get('failed_devices', 'N/A'),
array.get('spare_devices', 'N/A'),
array.get('total_devices', 'N/A'),
array.get('uuid', 'N/A'),
array.get('name', 'N/A'),
array.get('chunk_size', 'N/A'),
current_mount_point if current_mount_point else ""
]
item_id = self.tree_raid.insert('', tk.END, text=array_path, values=values)
self.tree_raid.item(item_id, tags=(str(array),))
if array.get('state') != 'Stopped (Configured)':
for member in array.get('member_devices', []):
member_values = [
f" {member.get('device_path', 'N/A')}",
f"成员: {member.get('raid_device', 'N/A')}",
member.get('state', 'N/A')
] + [''] * (len(self.raid_columns) - 3)
self.tree_raid.insert(item_id, tk.END, text='', values=member_values)
self.tree_raid.item(item_id, open=True)
logger.info("RAID阵列信息刷新成功。")
except Exception as e:
messagebox.showerror("错误", f"刷新RAID阵列信息失败: {e}")
logger.error(f"刷新RAID阵列信息失败: {e}")
def _handle_activate_raid_array(self, array_path):
"""处理激活 RAID 阵列"""
selected = self.tree_raid.selection()
if not selected:
return
array_data = self._get_item_data(self.tree_raid, selected[0])
if not array_data:
return
array_uuid = array_data.get('uuid')
if not array_uuid or array_uuid == 'N/A':
messagebox.showerror("错误", f"无法激活 RAID 阵列 {array_path}:缺少 UUID 信息。")
return
if messagebox.askyesno("确认激活 RAID 阵列",
f"您确定要激活 RAID 阵列 {array_path} (UUID: {array_uuid}) 吗?\n"
"这将尝试重新组装阵列。",
default=messagebox.NO):
if self.raid_ops.activate_raid_array(array_path, array_uuid):
self.refresh_all_info()
def _handle_create_raid_array(self):
"""处理创建 RAID 阵列"""
available_devices = self.system_manager.get_unallocated_partitions()
if not available_devices:
messagebox.showwarning("警告", "没有可用于创建 RAID 阵列的设备。请确保有未挂载、未被LVM或RAID使用的磁盘或分区。")
return
dialog = CreateRaidDialog(self.root, available_devices)
info = dialog.wait_for_result()
if info:
if self.raid_ops.create_raid_array(info['devices'], info['level'], info['chunk_size']):
self.refresh_all_info()
def _handle_stop_raid_array(self, array_path):
"""处理停止 RAID 阵列"""
if self.raid_ops.stop_raid_array(array_path):
self.refresh_all_info()
def _handle_delete_active_raid_array(self, array_path, member_devices, uuid, array_data=None):
"""处理删除活动的 RAID 阵列 - 使用增强型对话框"""
# 获取阵列详情(如果未提供)
if array_data is None:
array_data = {}
try:
raid_arrays = self.system_manager.get_mdadm_arrays()
for array in raid_arrays:
if array.get('device') == array_path:
array_data = array
break
except:
pass
# 使用增强型删除对话框
dialog = EnhancedRaidDeleteDialog(
self.root, array_path, array_data, member_devices
)
if dialog.wait_for_result():
# 执行删除
if self.raid_ops.delete_active_raid_array(array_path, member_devices, uuid):
self.refresh_all_info()
def _handle_delete_configured_raid_array(self, uuid):
"""处理删除停止状态 RAID 阵列的配置文件条目"""
if self.raid_ops.delete_configured_raid_array(uuid):
self.refresh_all_info()
def _handle_format_raid_array(self, array_path):
"""处理格式化 RAID 阵列"""
self.disk_ops.format_partition(array_path)
# === LVM 处理函数 ===
def refresh_lvm_info(self):
"""刷新 LVM 信息"""
for item in self.tree_lvm.get_children():
self.tree_lvm.delete(item)
try:
lvm_data = self.system_manager.get_lvm_info()
if not lvm_data.get('pvs') and not lvm_data.get('vgs') and not lvm_data.get('lvs'):
self.tree_lvm.insert('', tk.END, text='未找到LVM信息。', values=[''] * len(self.lvm_columns))
logger.info("未找到LVM信息。")
return
# 物理卷 (PVs)
pv_root = self.tree_lvm.insert('', tk.END, text="物理卷 (PVs)", values=[''] * len(self.lvm_columns))
self.tree_lvm.item(pv_root, tags=(str({'type': 'pv_root'}),))
if lvm_data.get('pvs'):
for pv in lvm_data['pvs']:
pv_name = pv.get('pv_name', 'N/A')
pv_path = pv_name if pv_name.startswith('/dev/') else f"/dev/{pv_name}"
values = [
pv_name,
pv.get('pv_size', 'N/A'),
pv.get('pv_attr', 'N/A'),
pv.get('pv_uuid', 'N/A'),
f"VG: {pv.get('vg_name', 'N/A')}",
f"空闲: {pv.get('pv_free', 'N/A')}",
pv.get('pv_fmt', 'N/A'),
""
]
item_id = self.tree_lvm.insert(pv_root, tk.END, text=pv_name, values=values)
pv_data = pv.copy()
pv_data['pv_name'] = pv_path
self.tree_lvm.item(item_id, tags=(str({'type': 'pv', 'data': pv_data}),))
else:
self.tree_lvm.insert(pv_root, tk.END, text='未找到物理卷。')
# 卷组 (VGs)
vg_root = self.tree_lvm.insert('', tk.END, text="卷组 (VGs)", values=[''] * len(self.lvm_columns))
self.tree_lvm.item(vg_root, tags=(str({'type': 'vg_root'}),))
if lvm_data.get('vgs'):
for vg in lvm_data['vgs']:
vg_name = vg.get('vg_name', 'N/A')
values = [
vg_name,
vg.get('vg_size', 'N/A'),
vg.get('vg_attr', 'N/A'),
vg.get('vg_uuid', 'N/A'),
f"PVs: {vg.get('pv_count', 'N/A')}, LVs: {vg.get('lv_count', 'N/A')}",
f"空闲: {vg.get('vg_free', 'N/A')}, 已分配: {vg.get('vg_alloc_percent', 'N/A')}%",
vg.get('vg_fmt', 'N/A'),
""
]
item_id = self.tree_lvm.insert(vg_root, tk.END, text=vg_name, values=values)
vg_data = vg.copy()
vg_data['vg_name'] = vg_name
self.tree_lvm.item(item_id, tags=(str({'type': 'vg', 'data': vg_data}),))
else:
self.tree_lvm.insert(vg_root, tk.END, text='未找到卷组。')
# 逻辑卷 (LVs)
lv_root = self.tree_lvm.insert('', tk.END, text="逻辑卷 (LVs)", values=[''] * len(self.lvm_columns))
self.tree_lvm.item(lv_root, tags=(str({'type': 'lv_root'}),))
if lvm_data.get('lvs'):
for lv in lvm_data['lvs']:
lv_name = lv.get('lv_name', 'N/A')
vg_name = lv.get('vg_name', 'N/A')
lv_attr = lv.get('lv_attr', '')
lv_path = lv.get('lv_path')
if not lv_path or lv_path == 'N/A':
lv_path = f"/dev/{vg_name}/{lv_name}" if vg_name != 'N/A' and lv_name != 'N/A' else 'N/A'
current_mount_point = self.system_manager.get_mountpoint_for_device(lv_path)
values = [
lv_name,
lv.get('lv_size', 'N/A'),
lv_attr,
lv.get('lv_uuid', 'N/A'),
f"VG: {vg_name}, Origin: {lv.get('origin', 'N/A')}",
f"快照: {lv.get('snap_percent', 'N/A')}%",
lv_path,
current_mount_point if current_mount_point else ""
]
item_id = self.tree_lvm.insert(lv_root, tk.END, text=lv_name, values=values)
lv_data = lv.copy()
lv_data['lv_path'] = lv_path
lv_data['lv_name'] = lv_name
lv_data['vg_name'] = vg_name
lv_data['lv_attr'] = lv_attr
self.tree_lvm.item(item_id, tags=(str({'type': 'lv', 'data': lv_data}),))
else:
self.tree_lvm.insert(lv_root, tk.END, text='未找到逻辑卷。')
# 展开所有根节点
self.tree_lvm.item(pv_root, open=True)
self.tree_lvm.item(vg_root, open=True)
self.tree_lvm.item(lv_root, open=True)
logger.info("LVM信息刷新成功。")
except Exception as e:
messagebox.showerror("错误", f"刷新LVM信息失败: {e}")
logger.error(f"刷新LVM信息失败: {e}")
def _handle_create_pv(self):
"""处理创建物理卷"""
available_partitions = self.system_manager.get_unallocated_partitions()
if not available_partitions:
messagebox.showwarning("警告", "没有可用于创建物理卷的未分配分区。")
return
dialog = CreatePvDialog(self.root, available_partitions)
info = dialog.wait_for_result()
if info:
if self.lvm_ops.create_pv(info['device_path']):
self.refresh_all_info()
def _handle_delete_pv(self, device_path):
"""处理删除物理卷"""
if self.lvm_ops.delete_pv(device_path):
self.refresh_all_info()
def _handle_create_vg(self):
"""处理创建卷组"""
lvm_info = self.system_manager.get_lvm_info()
available_pvs = []
for pv in lvm_info.get('pvs', []):
pv_name = pv.get('pv_name')
if pv_name and pv_name != 'N/A' and not pv.get('vg_name'):
if pv_name.startswith('/dev/'):
available_pvs.append(pv_name)
else:
available_pvs.append(f"/dev/{pv_name}")
if not available_pvs:
messagebox.showwarning("警告", "没有可用于创建卷组的物理卷。")
return
dialog = CreateVgDialog(self.root, available_pvs)
info = dialog.wait_for_result()
if info:
if self.lvm_ops.create_vg(info['vg_name'], info['pvs']):
self.refresh_all_info()
def _handle_create_lv(self):
"""处理创建逻辑卷"""
lvm_info = self.system_manager.get_lvm_info()
available_vgs = []
vg_sizes = {}
for vg in lvm_info.get('vgs', []):
vg_name = vg.get('vg_name')
if vg_name and vg_name != 'N/A':
available_vgs.append(vg_name)
free_size_str = vg.get('vg_free', '0B').strip().lower()
current_vg_size_gb = 0.0
match = re.match(r'<?(\d+\.?\d*)\s*([gmktb])?', free_size_str)
if match:
value = float(match.group(1))
unit = match.group(2)
if unit == 'k':
current_vg_size_gb = value / (1024 * 1024)
elif unit == 'm':
current_vg_size_gb = value / 1024
elif unit == 'g' or unit is None:
current_vg_size_gb = value
elif unit == 't':
current_vg_size_gb = value * 1024
elif unit == 'b':
current_vg_size_gb = value / (1024 * 1024 * 1024)
vg_sizes[vg_name] = current_vg_size_gb
if not available_vgs:
messagebox.showwarning("警告", "没有可用于创建逻辑卷的卷组。")
return
dialog = CreateLvDialog(self.root, available_vgs, vg_sizes)
info = dialog.wait_for_result()
if info:
if self.lvm_ops.create_lv(info['lv_name'], info['vg_name'], info['size_gb'], info['use_max_space']):
self.refresh_all_info()
def _handle_delete_lv(self, lv_name, vg_name):
"""处理删除逻辑卷"""
lv_path = f"/dev/{vg_name}/{lv_name}"
self.disk_ops.unmount_partition(lv_path, show_dialog_on_error=False)
self.disk_ops._remove_fstab_entry(lv_path)
if self.lvm_ops.delete_lv(lv_name, vg_name):
self.refresh_all_info()
def _handle_activate_lv(self, lv_name, vg_name):
"""处理激活逻辑卷"""
if self.lvm_ops.activate_lv(lv_name, vg_name):
self.refresh_all_info()
def _handle_deactivate_lv(self, lv_name, vg_name):
"""处理停用逻辑卷"""
if self.lvm_ops.deactivate_lv(lv_name, vg_name):
self.refresh_all_info()
def _handle_delete_vg(self, vg_name):
"""处理删除卷组"""
logger.info(f"尝试删除卷组 (VG) {vg_name}")
if not messagebox.askyesno("确认删除卷组",
f"您确定要删除卷组 {vg_name} 吗?此操作将永久删除该卷组及其所有逻辑卷和数据!",
default=messagebox.NO):
logger.info(f"用户取消了删除卷组 {vg_name} 的操作。")
return
lvm_info = self.system_manager.get_lvm_info()
vg_info = next((vg for vg in lvm_info.get('vgs', []) if vg.get('vg_name') == vg_name), None)
lv_count_raw = vg_info.get('lv_count', 0) if vg_info else 0
try:
lv_count = int(float(lv_count_raw))
except (ValueError, TypeError):
lv_count = 0
if lv_count > 0:
messagebox.showerror("删除失败", f"卷组 {vg_name} 中仍包含逻辑卷。请先删除所有逻辑卷。")
logger.error(f"尝试删除包含逻辑卷的卷组 {vg_name}。操作被阻止。")
return
success = self.lvm_ops.delete_vg(vg_name)
if success:
self.refresh_all_info()
else:
messagebox.showerror("删除卷组失败", f"删除卷组 {vg_name} 失败,请检查日志。")
if __name__ == "__main__":
# 创建主窗口
root = tk.Tk()
app = MainWindow(root)
root.mainloop()